From 06f00988d0c0fb00407dc7ed4f5f1987fd8bae2d Mon Sep 17 00:00:00 2001 From: Eirik Dahlen Date: Thu, 8 Aug 2024 14:58:22 +0200 Subject: [PATCH] IS-2577: Add pdlClient to get name in api for journalforing --- .nais/naiserator-dev.yaml | 4 + .nais/naiserator-prod.yaml | 4 + .../no/nav/syfo/ApplicationEnvironment.kt | 4 + .../clients/ClientsEnvironment.kt | 1 + .../infrastructure/clients/pdl/PdlClient.kt | 92 +++++++++++++++++ .../clients/pdl/dto/PdlError.kt | 22 +++++ .../clients/pdl/dto/PdlHentPersonRequest.kt | 11 +++ .../clients/pdl/dto/PdlPersonResponse.kt | 51 ++++++++++ src/main/resources/pdl/hentPerson.graphql | 15 +++ .../kotlin/no/nav/syfo/TestEnvironment.kt | 4 + src/test/kotlin/no/nav/syfo/UserConstants.kt | 10 ++ .../no/nav/syfo/generator/PdlGenerator.kt | 35 +++++++ .../infrastructure/mock/MockHttpClient.kt | 1 + .../nav/syfo/infrastructure/mock/PdlMock.kt | 29 ++++++ .../syfo/infrastructure/pdl/PdlClientSpek.kt | 98 +++++++++++++++++++ 15 files changed, 381 insertions(+) create mode 100644 src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/PdlClient.kt create mode 100644 src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlError.kt create mode 100644 src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlHentPersonRequest.kt create mode 100644 src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlPersonResponse.kt create mode 100644 src/main/resources/pdl/hentPerson.graphql create mode 100644 src/test/kotlin/no/nav/syfo/generator/PdlGenerator.kt create mode 100644 src/test/kotlin/no/nav/syfo/infrastructure/mock/PdlMock.kt create mode 100644 src/test/kotlin/no/nav/syfo/infrastructure/pdl/PdlClientSpek.kt diff --git a/.nais/naiserator-dev.yaml b/.nais/naiserator-dev.yaml index 6fa5ac1..13abb01 100644 --- a/.nais/naiserator-dev.yaml +++ b/.nais/naiserator-dev.yaml @@ -74,3 +74,7 @@ spec: value: "dev-gcp.teamsykefravr.istilgangskontroll" - name: ISTILGANGSKONTROLL_URL value: "http://istilgangskontroll" + - name: PDL_CLIENT_ID + value: "dev-fss.pdl.pdl-api" + - name: PDL_URL + value: "https://pdl-api.dev-fss-pub.nais.io/graphql" diff --git a/.nais/naiserator-prod.yaml b/.nais/naiserator-prod.yaml index 9201b69..7c3b2bd 100644 --- a/.nais/naiserator-prod.yaml +++ b/.nais/naiserator-prod.yaml @@ -74,3 +74,7 @@ spec: value: "prod-gcp.teamsykefravr.istilgangskontroll" - name: ISTILGANGSKONTROLL_URL value: "http://istilgangskontroll" + - name: PDL_CLIENT_ID + value: "prod-fss.pdl.pdl-api" + - name: PDL_URL + value: "https://pdl-api.prod-fss-pub.nais.io/graphql" diff --git a/src/main/kotlin/no/nav/syfo/ApplicationEnvironment.kt b/src/main/kotlin/no/nav/syfo/ApplicationEnvironment.kt index 1994505..5dd2262 100644 --- a/src/main/kotlin/no/nav/syfo/ApplicationEnvironment.kt +++ b/src/main/kotlin/no/nav/syfo/ApplicationEnvironment.kt @@ -30,6 +30,10 @@ data class Environment( baseUrl = getEnvVar("ISTILGANGSKONTROLL_URL"), clientId = getEnvVar("ISTILGANGSKONTROLL_CLIENT_ID") ), + pdl = ClientEnvironment( + baseUrl = getEnvVar("PDL_URL"), + clientId = getEnvVar("PDL_CLIENT_ID") + ), ), ) diff --git a/src/main/kotlin/no/nav/syfo/infrastructure/clients/ClientsEnvironment.kt b/src/main/kotlin/no/nav/syfo/infrastructure/clients/ClientsEnvironment.kt index 98f294a..4359df9 100644 --- a/src/main/kotlin/no/nav/syfo/infrastructure/clients/ClientsEnvironment.kt +++ b/src/main/kotlin/no/nav/syfo/infrastructure/clients/ClientsEnvironment.kt @@ -2,6 +2,7 @@ package no.nav.syfo.infrastructure.clients data class ClientsEnvironment( val istilgangskontroll: ClientEnvironment, + val pdl: ClientEnvironment, ) data class ClientEnvironment( diff --git a/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/PdlClient.kt b/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/PdlClient.kt new file mode 100644 index 0000000..08c9e9d --- /dev/null +++ b/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/PdlClient.kt @@ -0,0 +1,92 @@ +package no.nav.syfo.infrastructure.clients.pdl + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.micrometer.core.instrument.Counter +import no.nav.syfo.domain.Personident +import no.nav.syfo.infrastructure.clients.ClientEnvironment +import no.nav.syfo.infrastructure.clients.azuread.AzureAdClient +import no.nav.syfo.infrastructure.bearerHeader +import no.nav.syfo.infrastructure.clients.pdl.dto.* +import no.nav.syfo.infrastructure.clients.httpClientDefault +import no.nav.syfo.infrastructure.metric.METRICS_NS +import no.nav.syfo.infrastructure.metric.METRICS_REGISTRY +import org.slf4j.LoggerFactory + +class PdlClient( + private val azureAdClient: AzureAdClient, + private val pdlEnvironment: ClientEnvironment, + private val httpClient: HttpClient = httpClientDefault(), +) { + + suspend fun getPerson(personident: Personident): PdlPerson { + val token = azureAdClient.getSystemToken(pdlEnvironment.clientId) + ?: throw RuntimeException("Failed to send request to PDL: No token was found") + val request = PdlHentPersonRequest(getPdlQuery(), PdlHentPersonRequestVariables(personident.value)) + + val response: HttpResponse = httpClient.post(pdlEnvironment.baseUrl) { + setBody(request) + header(HttpHeaders.ContentType, "application/json") + header(HttpHeaders.Authorization, bearerHeader(token.accessToken)) + header(BEHANDLINGSNUMMER_HEADER_KEY, BEHANDLINGSNUMMER_HEADER_VALUE) + } + + val person = when (response.status) { + HttpStatusCode.OK -> { + val pdlPersonReponse = response.body() + if (!pdlPersonReponse.errors.isNullOrEmpty()) { + Metrics.COUNT_CALL_PDL_PERSON_FAIL.increment() + pdlPersonReponse.errors.forEach { + logger.error("Error while requesting person from PersonDataLosningen: ${it.errorMessage()}") + } + null + } else { + Metrics.COUNT_CALL_PDL_PERSON_SUCCESS.increment() + pdlPersonReponse.data?.hentPerson + } + } + + else -> { + Metrics.COUNT_CALL_PDL_PERSON_FAIL.increment() + logger.error("Request with url: ${pdlEnvironment.baseUrl} failed with reponse code ${response.status.value}") + null + } + } + + return person ?: throw RuntimeException("PDL did not return a person for given fnr") + } + + private fun getPdlQuery(): String = + this::class.java.getResource(PDL_QUERY_PATH)!! + .readText() + .replace("[\n\r]", "") + + companion object { + private const val PDL_QUERY_PATH = "/pdl/hentPerson.graphql" + + // Se behandlingskatalog https://behandlingskatalog.intern.nav.no/ + // Behandling: Sykefraværsoppfølging: Vurdere behov for oppfølging og rett til sykepenger etter §§ 8-4 og 8-8 + private const val BEHANDLINGSNUMMER_HEADER_KEY = "behandlingsnummer" + private const val BEHANDLINGSNUMMER_HEADER_VALUE = "B426" + + private val logger = LoggerFactory.getLogger(PdlClient::class.java) + } +} + +private class Metrics { + companion object { + const val CALL_PDL_PERSON_BASE = "${METRICS_NS}_call_pdl_person" + const val CALL_PDL_PERSON_SUCCESS = "${CALL_PDL_PERSON_BASE}_success_count" + const val CALL_PDL_PERSON_FAIL = "${CALL_PDL_PERSON_BASE}_fail_count" + + val COUNT_CALL_PDL_PERSON_SUCCESS: Counter = Counter.builder(CALL_PDL_PERSON_SUCCESS) + .description("Counts the number of successful calls to pdl - person") + .register(METRICS_REGISTRY) + val COUNT_CALL_PDL_PERSON_FAIL: Counter = Counter.builder(CALL_PDL_PERSON_FAIL) + .description("Counts the number of failed calls to pdl - person") + .register(METRICS_REGISTRY) + } +} diff --git a/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlError.kt b/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlError.kt new file mode 100644 index 0000000..1023867 --- /dev/null +++ b/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlError.kt @@ -0,0 +1,22 @@ +package no.nav.syfo.infrastructure.clients.pdl.dto + +data class PdlError( + val message: String, + val locations: List, + val path: List?, + val extensions: PdlErrorExtension, +) + +data class PdlErrorLocation( + val line: Int?, + val column: Int?, +) + +data class PdlErrorExtension( + val code: String?, + val classification: String, +) + +fun PdlError.errorMessage(): String { + return "${this.message} with code: ${extensions.code} and classification: ${extensions.classification}" +} diff --git a/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlHentPersonRequest.kt b/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlHentPersonRequest.kt new file mode 100644 index 0000000..d6222ff --- /dev/null +++ b/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlHentPersonRequest.kt @@ -0,0 +1,11 @@ +package no.nav.syfo.infrastructure.clients.pdl.dto + +data class PdlHentPersonRequest( + val query: String, + val variables: PdlHentPersonRequestVariables +) + +data class PdlHentPersonRequestVariables( + val ident: String, + val navnHistorikk: Boolean = false +) diff --git a/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlPersonResponse.kt b/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlPersonResponse.kt new file mode 100644 index 0000000..bf816ce --- /dev/null +++ b/src/main/kotlin/no/nav/syfo/infrastructure/clients/pdl/dto/PdlPersonResponse.kt @@ -0,0 +1,51 @@ +package no.nav.syfo.infrastructure.clients.pdl.dto + +import java.util.* + +data class PdlPersonResponse( + val errors: List?, + val data: PdlHentPerson? +) + +data class PdlHentPerson( + val hentPerson: PdlPerson? +) + +data class PdlPerson( + val navn: List, +) { + val fullName: String = navn.firstOrNull()?.fullName() + ?: throw RuntimeException("PDL returned empty navn for given fnr") +} + +data class PdlPersonNavn( + val fornavn: String, + val mellomnavn: String?, + val etternavn: String +) { + fun fullName(): String { + val fornavn = fornavn.lowerCapitalize() + val etternavn = etternavn.lowerCapitalize() + + return if (mellomnavn.isNullOrBlank()) { + "$fornavn $etternavn" + } else { + "$fornavn ${mellomnavn.lowerCapitalize()} $etternavn" + } + } +} + +fun String.lowerCapitalize() = + this.split(" ").joinToString(" ") { name -> + val nameWithDash = name.split("-") + if (nameWithDash.size > 1) { + nameWithDash.joinToString("-") { it.capitalizeName() } + } else { + name.capitalizeName() + } + } + +private fun String.capitalizeName() = + this.lowercase(Locale.getDefault()).replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } diff --git a/src/main/resources/pdl/hentPerson.graphql b/src/main/resources/pdl/hentPerson.graphql new file mode 100644 index 0000000..0d27d59 --- /dev/null +++ b/src/main/resources/pdl/hentPerson.graphql @@ -0,0 +1,15 @@ +query($ident: ID!, $navnHistorikk: Boolean!){ + hentPerson(ident: $ident) { + navn(historikk: $navnHistorikk) { + fornavn + mellomnavn + etternavn + forkortetNavn + originaltNavn { + fornavn + mellomnavn + etternavn + } + } + } +} diff --git a/src/test/kotlin/no/nav/syfo/TestEnvironment.kt b/src/test/kotlin/no/nav/syfo/TestEnvironment.kt index beff663..ab6b918 100644 --- a/src/test/kotlin/no/nav/syfo/TestEnvironment.kt +++ b/src/test/kotlin/no/nav/syfo/TestEnvironment.kt @@ -25,6 +25,10 @@ fun testEnvironment() = Environment( baseUrl = "isTilgangskontrollUrl", clientId = "dev-gcp.teamsykefravr.istilgangskontroll", ), + pdl = ClientEnvironment( + baseUrl = "pdlUrl", + clientId = "pdlClientId", + ), ), electorPath = "electorPath", ) diff --git a/src/test/kotlin/no/nav/syfo/UserConstants.kt b/src/test/kotlin/no/nav/syfo/UserConstants.kt index bf359cc..d5b0ce6 100644 --- a/src/test/kotlin/no/nav/syfo/UserConstants.kt +++ b/src/test/kotlin/no/nav/syfo/UserConstants.kt @@ -5,5 +5,15 @@ import no.nav.syfo.domain.Personident object UserConstants { val ARBEIDSTAKER_PERSONIDENT = Personident("12345678910") val ARBEIDSTAKER_PERSONIDENT_VEILEDER_NO_ACCESS = Personident("11111111111") + val ARBEIDSTAKER_PERSONIDENT_NAME_WITH_DASH = Personident("11111111234") + val ARBEIDSTAKER_PERSONIDENT_NO_NAME = Personident("11111111222") + val ARBEIDSTAKER_PERSONIDENT_PDL_FAILS = Personident("11111111666") const val VEILEDER_IDENT = "Z999999" + + const val PERSON_FORNAVN = "Fornavn" + const val PERSON_MELLOMNAVN = "Mellomnavn" + const val PERSON_ETTERNAVN = "Etternavnesen" + const val PERSON_FULLNAME = "Fornavn Mellomnavn Etternavnesen" + const val PERSON_FORNAVN_DASH = "For-Navn" + const val PERSON_FULLNAME_DASH = "For-Navn Mellomnavn Etternavnesen" } diff --git a/src/test/kotlin/no/nav/syfo/generator/PdlGenerator.kt b/src/test/kotlin/no/nav/syfo/generator/PdlGenerator.kt new file mode 100644 index 0000000..afb7df0 --- /dev/null +++ b/src/test/kotlin/no/nav/syfo/generator/PdlGenerator.kt @@ -0,0 +1,35 @@ +package no.nav.syfo.generator + +import no.nav.syfo.UserConstants +import no.nav.syfo.infrastructure.clients.pdl.dto.* + +fun generatePdlPersonResponse(pdlPersonNavn: PdlPersonNavn? = null, errors: List? = null) = PdlPersonResponse( + errors = errors, + data = generatePdlHentPerson(pdlPersonNavn) +) + +fun generatePdlPersonNavn(): PdlPersonNavn = PdlPersonNavn( + fornavn = UserConstants.PERSON_FORNAVN, + mellomnavn = UserConstants.PERSON_MELLOMNAVN, + etternavn = UserConstants.PERSON_ETTERNAVN, +) + +fun generatePdlHentPerson( + pdlPersonNavn: PdlPersonNavn?, +): PdlHentPerson = PdlHentPerson( + hentPerson = PdlPerson( + navn = if (pdlPersonNavn != null) listOf(pdlPersonNavn) else emptyList(), + ) +) + +fun generatePdlError() = listOf( + PdlError( + message = "Error in PDL", + locations = emptyList(), + path = null, + extensions = PdlErrorExtension( + code = null, + classification = "", + ) + ) +) diff --git a/src/test/kotlin/no/nav/syfo/infrastructure/mock/MockHttpClient.kt b/src/test/kotlin/no/nav/syfo/infrastructure/mock/MockHttpClient.kt index 67f93b8..ee4bec3 100644 --- a/src/test/kotlin/no/nav/syfo/infrastructure/mock/MockHttpClient.kt +++ b/src/test/kotlin/no/nav/syfo/infrastructure/mock/MockHttpClient.kt @@ -15,6 +15,7 @@ fun mockHttpClient(environment: Environment) = HttpClient(MockEngine) { requestUrl.startsWith("/${environment.clients.istilgangskontroll.baseUrl}") -> tilgangskontrollResponse( request ) + requestUrl.startsWith("/${environment.clients.pdl.baseUrl}") -> pdlMockResponse(request) else -> error("Unhandled ${request.url.encodedPath}") } } diff --git a/src/test/kotlin/no/nav/syfo/infrastructure/mock/PdlMock.kt b/src/test/kotlin/no/nav/syfo/infrastructure/mock/PdlMock.kt new file mode 100644 index 0000000..59eb98c --- /dev/null +++ b/src/test/kotlin/no/nav/syfo/infrastructure/mock/PdlMock.kt @@ -0,0 +1,29 @@ +package no.nav.syfo.infrastructure.mock + +import io.ktor.client.engine.mock.* +import io.ktor.client.request.* +import no.nav.syfo.UserConstants +import no.nav.syfo.domain.Personident +import no.nav.syfo.generator.generatePdlError +import no.nav.syfo.generator.generatePdlPersonNavn +import no.nav.syfo.generator.generatePdlPersonResponse +import no.nav.syfo.infrastructure.clients.pdl.dto.PdlHentPersonRequest +import no.nav.syfo.infrastructure.clients.pdl.dto.PdlPersonNavn + +suspend fun MockRequestHandleScope.pdlMockResponse(request: HttpRequestData): HttpResponseData { + val pdlRequest = request.receiveBody() + return when (Personident(pdlRequest.variables.ident)) { + UserConstants.ARBEIDSTAKER_PERSONIDENT_NO_NAME -> respond(generatePdlPersonResponse(pdlPersonNavn = null)) + UserConstants.ARBEIDSTAKER_PERSONIDENT_NAME_WITH_DASH -> respond( + generatePdlPersonResponse( + PdlPersonNavn( + fornavn = UserConstants.PERSON_FORNAVN_DASH, + mellomnavn = UserConstants.PERSON_MELLOMNAVN, + etternavn = UserConstants.PERSON_ETTERNAVN, + ) + ) + ) + UserConstants.ARBEIDSTAKER_PERSONIDENT_PDL_FAILS -> respond(generatePdlPersonResponse(errors = generatePdlError())) + else -> respond(generatePdlPersonResponse(generatePdlPersonNavn())) + } +} diff --git a/src/test/kotlin/no/nav/syfo/infrastructure/pdl/PdlClientSpek.kt b/src/test/kotlin/no/nav/syfo/infrastructure/pdl/PdlClientSpek.kt new file mode 100644 index 0000000..b8f6bad --- /dev/null +++ b/src/test/kotlin/no/nav/syfo/infrastructure/pdl/PdlClientSpek.kt @@ -0,0 +1,98 @@ +package no.nav.syfo.infrastructure.pdl + +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import no.nav.syfo.ExternalMockEnvironment +import no.nav.syfo.UserConstants +import no.nav.syfo.infrastructure.clients.azuread.AzureAdClient +import no.nav.syfo.infrastructure.clients.pdl.PdlClient +import no.nav.syfo.infrastructure.clients.pdl.dto.PdlPerson +import no.nav.syfo.infrastructure.clients.pdl.dto.PdlPersonNavn +import org.amshove.kluent.internal.assertFailsWith +import org.amshove.kluent.shouldBeEqualTo +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.specification.describe + +object PdlClientSpek : Spek({ + val externalMockEnvironment = ExternalMockEnvironment.instance + val pdlClient = PdlClient( + azureAdClient = externalMockEnvironment.azureAdClient, + pdlEnvironment = externalMockEnvironment.environment.clients.pdl, + httpClient = externalMockEnvironment.mockHttpClient, + ) + + describe(PdlClient::class.java.simpleName) { + describe("Happy case") { + it("returns person from pdl") { + runBlocking { + val person = pdlClient.getPerson(UserConstants.ARBEIDSTAKER_PERSONIDENT) + person.navn.size shouldBeEqualTo 1 + person.navn[0].fornavn shouldBeEqualTo UserConstants.PERSON_FORNAVN + } + } + + it("returns fullname from person") { + val pdlPerson = PdlPerson( + navn = listOf( + PdlPersonNavn( + fornavn = UserConstants.PERSON_FORNAVN, + mellomnavn = UserConstants.PERSON_MELLOMNAVN, + etternavn = UserConstants.PERSON_ETTERNAVN, + ) + ) + ) + runBlocking { + val person = pdlClient.getPerson(UserConstants.ARBEIDSTAKER_PERSONIDENT) + person.fullName shouldBeEqualTo pdlPerson.fullName + } + } + + it("returns full name when person has name with dashes") { + runBlocking { + val fullname = pdlClient.getPerson(UserConstants.ARBEIDSTAKER_PERSONIDENT_NAME_WITH_DASH).fullName + fullname shouldBeEqualTo UserConstants.PERSON_FULLNAME_DASH + } + } + } + + describe("Unhappy case") { + afterEachTest { + clearAllMocks() + } + + it("throws exception when person is missing name") { + runBlocking { + assertFailsWith(RuntimeException::class) { + pdlClient.getPerson(UserConstants.ARBEIDSTAKER_PERSONIDENT_NO_NAME) + } + } + } + + it("throws exception when pdl has error") { + runBlocking { + assertFailsWith(RuntimeException::class) { + pdlClient.getPerson(UserConstants.ARBEIDSTAKER_PERSONIDENT_PDL_FAILS) + } + } + } + + it("throws exception when AzureAdClient has error") { + val azureAdMock = mockk(relaxed = true) + val pdlClientMockedAzure = PdlClient( + azureAdClient = azureAdMock, + pdlEnvironment = externalMockEnvironment.environment.clients.pdl, + ) + + coEvery { azureAdMock.getSystemToken(any()) } returns null + + runBlocking { + assertFailsWith(RuntimeException::class) { + pdlClientMockedAzure.getPerson(UserConstants.ARBEIDSTAKER_PERSONIDENT) + } + } + } + } + } +})