diff --git a/apps/api-start-stopp-perioder/build.gradle.kts b/apps/api-start-stopp-perioder/build.gradle.kts index 58e81338..53c7fd3d 100644 --- a/apps/api-start-stopp-perioder/build.gradle.kts +++ b/apps/api-start-stopp-perioder/build.gradle.kts @@ -39,7 +39,7 @@ dependencies { implementation(libs.paw.pdl.client) implementation(libs.logbackClassic) implementation(libs.logstashLogbackEncoder) - implementation(libs.paw.kafkaClients) + implementation(libs.kafka.clients) implementation(libs.ktor.client.contentNegotiation) implementation(libs.ktor.client.core) implementation(libs.ktor.client.cio) diff --git a/apps/bekreftelse-api/build.gradle.kts b/apps/bekreftelse-api/build.gradle.kts index ddda2430..d9bfb148 100644 --- a/apps/bekreftelse-api/build.gradle.kts +++ b/apps/bekreftelse-api/build.gradle.kts @@ -59,8 +59,16 @@ dependencies { implementation(libs.opentelemetry.api) implementation(libs.opentelemetry.annotations) + // Database + implementation(libs.exposed.jdbc) + implementation(libs.exposed.json) + implementation(libs.exposed.javaTime) + implementation(libs.database.hikari.connectionPool) + implementation(libs.database.postgres.driver) + implementation(libs.database.flyway.postgres) + // Kafka - implementation(libs.kafka.streams.core) + implementation(libs.kafka.clients) implementation(libs.avro.kafkaStreamsSerde) // Test @@ -68,6 +76,7 @@ dependencies { testImplementation(libs.ktor.client.mock) testImplementation(libs.bundles.testLibsWithUnitTesting) testImplementation(libs.test.mockOauth2Server) + testImplementation(libs.test.testContainers.postgresql) } java { diff --git a/apps/bekreftelse-api/nais/nais-dev.yaml b/apps/bekreftelse-api/nais/nais-dev.yaml index 73f7dcb1..58bdbd7e 100644 --- a/apps/bekreftelse-api/nais/nais-dev.yaml +++ b/apps/bekreftelse-api/nais/nais-dev.yaml @@ -21,8 +21,6 @@ spec: value: "paw.arbeidssoker-bekreftelse-hendelseslogg-beta-v2" - name: CORS_ALLOW_ORIGINS value: "www.intern.dev.nav.no" - ingresses: - - https://bekreftelse-arbeidssoekerregisteret.intern.dev.nav.no replicas: min: 2 max: 2 @@ -41,10 +39,6 @@ spec: claims: extra: - NAVident - idporten: - enabled: true - sidecar: - enabled: true liveness: path: /internal/isAlive initialDelay: 10 @@ -61,6 +55,12 @@ spec: kafka: pool: nav-dev streams: true + gcp: + sqlInstances: + - type: POSTGRES_16 + tier: db-f1-micro + databases: + - name: bekreftelser accessPolicy: inbound: rules: diff --git a/apps/bekreftelse-api/nais/nais-prod.yaml b/apps/bekreftelse-api/nais/nais-prod.yaml index 7b5ba1d0..05fdb897 100644 --- a/apps/bekreftelse-api/nais/nais-prod.yaml +++ b/apps/bekreftelse-api/nais/nais-prod.yaml @@ -39,10 +39,6 @@ spec: claims: extra: - NAVident - idporten: - enabled: true - sidecar: - enabled: true liveness: path: /internal/isAlive initialDelay: 10 @@ -59,6 +55,12 @@ spec: kafka: pool: nav-prod streams: true + gcp: + sqlInstances: + - type: POSTGRES_16 + tier: db-custom-1-6144 + databases: + - name: bekreftelser accessPolicy: inbound: rules: diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/Application.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/Application.kt index 3b708be8..f8fe9f99 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/Application.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/Application.kt @@ -7,6 +7,7 @@ import io.ktor.server.netty.Netty import io.ktor.server.routing.routing import no.nav.paw.bekreftelse.api.context.ApplicationContext import no.nav.paw.bekreftelse.api.plugins.configureAuthentication +import no.nav.paw.bekreftelse.api.plugins.configureDatabase import no.nav.paw.bekreftelse.api.plugins.configureHTTP import no.nav.paw.bekreftelse.api.plugins.configureKafka import no.nav.paw.bekreftelse.api.plugins.configureLogging @@ -49,6 +50,7 @@ fun Application.module(applicationContext: ApplicationContext) { configureLogging() configureSerialization() configureTracing() + configureDatabase(applicationContext) configureKafka(applicationContext) routing { diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/config/ApplicationConfig.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/config/ApplicationConfig.kt index 37715fd1..a5cc6dda 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/config/ApplicationConfig.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/config/ApplicationConfig.kt @@ -3,31 +3,25 @@ package no.nav.paw.bekreftelse.api.config import no.nav.paw.config.kafka.KafkaConfig import no.nav.paw.kafkakeygenerator.auth.AzureM2MConfig import no.nav.paw.kafkakeygenerator.client.KafkaKeyConfig +import java.time.Duration const val APPLICATION_CONFIG_FILE_NAME = "application_config.toml" data class ApplicationConfig( val autorisasjon: AutorisasjonConfig, - val kafkaTopology: KafkaTopologyConfig, - val authProviders: AuthProviders, + val authProviders: List, val azureM2M: AzureM2MConfig, val poaoClientConfig: ServiceClientConfig, val kafkaKeysClient: KafkaKeyConfig, - val kafkaClients: KafkaConfig + val kafkaClients: KafkaConfig, + val kafkaTopology: KafkaTopologyConfig, + val database: DatabaseConfig ) data class AutorisasjonConfig( val corsAllowOrigins: String? = null ) -data class KafkaTopologyConfig( - val applicationIdSuffix: String, - val producerId: String, - val bekreftelseTopic: String, - val bekreftelseHendelsesloggTopic: String, - val internStateStoreName: String -) - data class ServiceClientConfig( val url: String, val scope: String @@ -40,9 +34,27 @@ data class AuthProvider( val claims: AuthProviderClaims ) -typealias AuthProviders = List - data class AuthProviderClaims( val map: List, val combineWithOr: Boolean = false +) + +data class KafkaTopologyConfig( + val version: Int, + val antallPartitioner: Int, + val producerId: String, + val consumerId: String, + val consumerGroupId: String, + val bekreftelseTopic: String, + val bekreftelseHendelsesloggTopic: String +) + +data class DatabaseConfig( + val jdbcUrl: String, + val driverClassName: String, + val autoCommit: Boolean = false, + val maxPoolSize: Int = 10, + val connectionTimeout: Duration = Duration.ofSeconds(30), + val idleTimeout: Duration = Duration.ofMinutes(10), + val maxLifetime: Duration = Duration.ofMinutes(30) ) \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/consumer/BekreftelseHttpConsumer.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/consumer/BekreftelseHttpConsumer.kt deleted file mode 100644 index dc1d1ead..00000000 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/consumer/BekreftelseHttpConsumer.kt +++ /dev/null @@ -1,48 +0,0 @@ -package no.nav.paw.bekreftelse.api.consumer - -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.request.bearerAuth -import io.ktor.client.request.headers -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.append -import no.nav.paw.bekreftelse.api.model.BekreftelseRequest -import no.nav.paw.bekreftelse.api.model.TilgjengeligBekreftelserResponse -import no.nav.paw.bekreftelse.api.model.TilgjengeligeBekreftelserRequest - -class BekreftelseHttpConsumer(private val httpClient: HttpClient) { - - suspend fun finnTilgjengeligBekreftelser( - host: String, - bearerToken: String, - request: TilgjengeligeBekreftelserRequest - ): TilgjengeligBekreftelserResponse { - val url = "http://$host/api/v1/tilgjengelige-bekreftelser" - val response = httpClient.post(url) { - bearerAuth(bearerToken) - headers { - append(HttpHeaders.ContentType, ContentType.Application.Json) - } - setBody(request) - } - return response.body() - } - - suspend fun sendBekreftelse( - host: String, - bearerToken: String, - request: BekreftelseRequest, - ) { - val url = "http://$host/api/v1/bekreftelse" - httpClient.post(url) { - bearerAuth(bearerToken) - headers { - append(HttpHeaders.ContentType, ContentType.Application.Json) - } - setBody(request) - } - } -} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/context/ApplicationContext.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/context/ApplicationContext.kt index 256659e1..2134b611 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/context/ApplicationContext.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/context/ApplicationContext.kt @@ -1,39 +1,48 @@ package no.nav.paw.bekreftelse.api.context -import io.ktor.client.HttpClient -import io.ktor.client.plugins.HttpResponseValidator -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.serialization.jackson.jackson import io.micrometer.prometheusmetrics.PrometheusConfig import io.micrometer.prometheusmetrics.PrometheusMeterRegistry import no.nav.paw.bekreftelse.api.config.APPLICATION_CONFIG_FILE_NAME import no.nav.paw.bekreftelse.api.config.ApplicationConfig import no.nav.paw.bekreftelse.api.config.SERVER_CONFIG_FILE_NAME import no.nav.paw.bekreftelse.api.config.ServerConfig -import no.nav.paw.bekreftelse.api.consumer.BekreftelseHttpConsumer -import no.nav.paw.bekreftelse.api.plugins.buildKafkaStreams +import no.nav.paw.bekreftelse.api.handler.KafkaConsumerExceptionHandler import no.nav.paw.bekreftelse.api.producer.BekreftelseKafkaProducer +import no.nav.paw.bekreftelse.api.repository.BekreftelseRepository import no.nav.paw.bekreftelse.api.services.AuthorizationService import no.nav.paw.bekreftelse.api.services.BekreftelseService -import no.nav.paw.bekreftelse.api.topology.buildBekreftelseTopology -import no.nav.paw.bekreftelse.api.utils.configureJackson -import no.nav.paw.bekreftelse.api.utils.handleError +import no.nav.paw.bekreftelse.api.utils.BekreftelseAvroSerializer +import no.nav.paw.bekreftelse.api.utils.createDataSource +import no.nav.paw.bekreftelse.internehendelser.BekreftelseHendelse +import no.nav.paw.bekreftelse.internehendelser.BekreftelseHendelseDeserializer +import no.nav.paw.bekreftelse.melding.v1.Bekreftelse import no.nav.paw.config.hoplite.loadNaisOrLocalConfiguration +import no.nav.paw.config.kafka.KafkaFactory +import no.nav.paw.health.model.HealthStatus +import no.nav.paw.health.model.LivenessHealthIndicator +import no.nav.paw.health.model.ReadinessHealthIndicator import no.nav.paw.health.repository.HealthIndicatorRepository import no.nav.paw.kafkakeygenerator.auth.azureAdM2MTokenClient import no.nav.paw.kafkakeygenerator.client.KafkaKeysClient import no.nav.paw.kafkakeygenerator.client.kafkaKeysClient import no.nav.poao_tilgang.client.PoaoTilgangCachedClient import no.nav.poao_tilgang.client.PoaoTilgangHttpClient -import org.apache.kafka.streams.KafkaStreams +import org.apache.kafka.clients.consumer.KafkaConsumer +import org.apache.kafka.clients.producer.Producer +import org.apache.kafka.common.serialization.LongDeserializer +import org.apache.kafka.common.serialization.LongSerializer +import javax.sql.DataSource data class ApplicationContext( val serverConfig: ServerConfig, val applicationConfig: ApplicationConfig, + val dataSource: DataSource, val kafkaKeysClient: KafkaKeysClient, val prometheusMeterRegistry: PrometheusMeterRegistry, val healthIndicatorRepository: HealthIndicatorRepository, - val bekreftelseKafkaStreams: KafkaStreams, + val bekreftelseKafkaProducer: Producer, + val bekreftelseKafkaConsumer: KafkaConsumer, + val kafkaConsumerExceptionHandler: KafkaConsumerExceptionHandler, val authorizationService: AuthorizationService, val bekreftelseService: BekreftelseService ) { @@ -42,6 +51,8 @@ data class ApplicationContext( val serverConfig = loadNaisOrLocalConfiguration(SERVER_CONFIG_FILE_NAME) val applicationConfig = loadNaisOrLocalConfiguration(APPLICATION_CONFIG_FILE_NAME) + val dataSource = createDataSource(applicationConfig.database) + val azureM2MTokenClient = azureAdM2MTokenClient( serverConfig.runtimeEnvironment, applicationConfig.azureM2M ) @@ -54,17 +65,6 @@ data class ApplicationContext( val healthIndicatorRepository = HealthIndicatorRepository() - val httpClient = HttpClient { - install(ContentNegotiation) { - jackson { - configureJackson() - } - } - HttpResponseValidator { - validateResponse(::handleError) - } - } - val poaoTilgangClient = PoaoTilgangCachedClient( PoaoTilgangHttpClient( baseUrl = applicationConfig.poaoClientConfig.url, @@ -79,33 +79,48 @@ data class ApplicationContext( poaoTilgangClient ) - val bekreftelseTopology = buildBekreftelseTopology(applicationConfig, prometheusMeterRegistry) - val bekreftelseKafkaStreams = buildKafkaStreams( - serverConfig, - applicationConfig, - healthIndicatorRepository, - bekreftelseTopology + val kafkaConsumerExceptionHandler = KafkaConsumerExceptionHandler( + healthIndicatorRepository.addLivenessIndicator(LivenessHealthIndicator(HealthStatus.HEALTHY)), + healthIndicatorRepository.addReadinessIndicator(ReadinessHealthIndicator(HealthStatus.HEALTHY)) ) - val bekreftelseKafkaProducer = BekreftelseKafkaProducer(applicationConfig, prometheusMeterRegistry) + val kafkaFactory = KafkaFactory(applicationConfig.kafkaClients) - val bekreftelseHttpConsumer = BekreftelseHttpConsumer(httpClient) + val kafkaProducer = kafkaFactory.createProducer( + clientId = applicationConfig.kafkaTopology.producerId, + keySerializer = LongSerializer::class, + valueSerializer = BekreftelseAvroSerializer::class + ) + + val kafkaConsumer = kafkaFactory.createConsumer( + clientId = applicationConfig.kafkaTopology.consumerId, + groupId = applicationConfig.kafkaTopology.consumerGroupId, + keyDeserializer = LongDeserializer::class, + valueDeserializer = BekreftelseHendelseDeserializer::class, + autoCommit = false + ) + + val bekreftelseKafkaProducer = BekreftelseKafkaProducer(applicationConfig, kafkaProducer) + val bekreftelseRepository = BekreftelseRepository() val bekreftelseService = BekreftelseService( serverConfig, applicationConfig, - bekreftelseHttpConsumer, - bekreftelseKafkaStreams, - bekreftelseKafkaProducer + prometheusMeterRegistry, + bekreftelseKafkaProducer, + bekreftelseRepository ) return ApplicationContext( serverConfig, applicationConfig, + dataSource, kafkaKeysClient, prometheusMeterRegistry, healthIndicatorRepository, - bekreftelseKafkaStreams, + kafkaProducer, + kafkaConsumer, + kafkaConsumerExceptionHandler, authorizationService, bekreftelseService ) diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/context/JsonbColumnType.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/context/JsonbColumnType.kt new file mode 100644 index 00000000..25be83e4 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/context/JsonbColumnType.kt @@ -0,0 +1,31 @@ +package no.nav.paw.bekreftelse.api.context + +import no.nav.paw.bekreftelse.api.model.BekreftelserTable.registerColumn +import org.jetbrains.exposed.sql.IColumnType +import org.postgresql.util.PGobject + +fun jsonb(name: String) = registerColumn(name, JsonbColumnType(false)) + +class JsonbColumnType(override var nullable: Boolean) : IColumnType { + + override fun sqlType(): String = "JSONB" + + override fun valueFromDB(value: Any): String? { + if (value is PGobject) { + if (value.type.equals(sqlType(), true)) { + return value.value + } else { + throw IllegalArgumentException("Value is not a JSONB object: ${value.type}") + } + } else { + throw IllegalArgumentException("Value is not a PGobject: ${value.javaClass}") + } + } + + override fun valueToDB(value: String?): Any { + return PGobject().apply { + type = sqlType() + this.value = value + } + } +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/context/RequestContext.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/context/RequestContext.kt index 69a66acf..cfa42f13 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/context/RequestContext.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/context/RequestContext.kt @@ -21,7 +21,6 @@ data class RequestContext( val navConsumerId: String?, val bearerToken: String?, val identitetsnummer: String?, - val useMockData: Boolean, val principal: TokenValidationContextPrincipal? ) @@ -35,7 +34,6 @@ fun PipelineContext.resolveRequest( navConsumerId = call.request.headers[NavConsumerId.name], bearerToken = call.request.headers[HttpHeaders.Authorization], identitetsnummer = identitetsnummer, - useMockData = call.request.queryParameters["useMockData"].toBoolean(), principal = call.principal() ) } \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/handler/KafkaConsumerExceptionHandler.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/handler/KafkaConsumerExceptionHandler.kt new file mode 100644 index 00000000..77eafd6b --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/handler/KafkaConsumerExceptionHandler.kt @@ -0,0 +1,18 @@ +package no.nav.paw.bekreftelse.api.handler + +import no.nav.paw.bekreftelse.api.utils.buildErrorLogger +import no.nav.paw.health.model.LivenessHealthIndicator +import no.nav.paw.health.model.ReadinessHealthIndicator + +class KafkaConsumerExceptionHandler( + private val livenessIndicator: LivenessHealthIndicator, + private val readinessIndicator: ReadinessHealthIndicator +) { + private val errorLogger = buildErrorLogger + + fun handleException(throwable: Throwable) { + errorLogger.error("Kafka Consumer opplevde en uhåndterbar feil", throwable) + livenessIndicator.setUnhealthy() + readinessIndicator.setUnhealthy() + } +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/BekreftelseRequest.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/BekreftelseRequest.kt deleted file mode 100644 index 81c77fef..00000000 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/BekreftelseRequest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package no.nav.paw.bekreftelse.api.model - -import no.nav.paw.bekreftelse.melding.v1.Bekreftelse -import no.nav.paw.bekreftelse.melding.v1.vo.Metadata -import no.nav.paw.bekreftelse.melding.v1.vo.Svar -import no.nav.paw.config.env.RuntimeEnvironment -import no.nav.paw.config.env.appImageOrDefaultForLocal -import no.nav.paw.config.env.currentRuntimeEnvironment -import no.nav.paw.config.env.namespaceOrDefaultForLocal -import java.time.Instant -import java.util.* - -data class BekreftelseRequest( - val identitetsnummer: String?, - val bekreftelseId: UUID, - val harJobbetIDennePerioden: Boolean, - val vilFortsetteSomArbeidssoeker: Boolean -) - -fun BekreftelseRequest.asApi( - periodeId: UUID, - gjelderFra: Instant, - gjelderTil: Instant, - innloggetBruker: InnloggetBruker -): Bekreftelse { - val runtimeEnvironment = currentRuntimeEnvironment - return Bekreftelse.newBuilder() - .setNamespace(runtimeEnvironment.namespaceOrDefaultForLocal()) - .setId(bekreftelseId) - .setPeriodeId(periodeId) - .setSvar(asApi(gjelderFra, gjelderTil, innloggetBruker, runtimeEnvironment)) - .build() -} - -private fun BekreftelseRequest.asApi( - gjelderFra: Instant, - gjelderTil: Instant, - innloggetBruker: InnloggetBruker, - runtimeEnvironment: RuntimeEnvironment -): Svar { - return Svar.newBuilder() - .setSendtInn( - Metadata.newBuilder() - .setUtfoertAv(innloggetBruker.asApi()) - .setKilde(runtimeEnvironment.appImageOrDefaultForLocal()) - .setAarsak("Mottatt bekreftelse") // TODO Hva skal dette være - .setTidspunkt(Instant.now()) - .build() - ) - .setGjelderFra(gjelderFra) - .setGjelderTil(gjelderTil) - .setHarJobbetIDennePerioden(harJobbetIDennePerioden) - .setVilFortsetteSomArbeidssoeker(vilFortsetteSomArbeidssoeker) - .build() -} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/BekreftelseRow.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/BekreftelseRow.kt new file mode 100644 index 00000000..aedd4a94 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/BekreftelseRow.kt @@ -0,0 +1,47 @@ +package no.nav.paw.bekreftelse.api.model + +import no.nav.paw.bekreftelse.internehendelser.BekreftelseTilgjengelig +import org.jetbrains.exposed.sql.ResultRow +import java.util.* + +data class BekreftelseRow( + val version: Int, + val partition: Int, + val offset: Long, + val recordKey: Long, + val arbeidssoekerId: Long, + val periodeId: UUID, + val bekreftelseId: UUID, + val data: BekreftelseTilgjengelig +) { + fun harSammeOffset(row: BekreftelseRow): Boolean { + return version == row.version && partition == row.partition && offset == row.offset + } +} + +fun ResultRow.asBekreftelseRow() = BekreftelseRow( + version = this[BekreftelserTable.version], + partition = this[BekreftelserTable.partition], + offset = this[BekreftelserTable.offset], + recordKey = this[BekreftelserTable.recordKey], + arbeidssoekerId = this[BekreftelserTable.arbeidssoekerId], + periodeId = this[BekreftelserTable.periodeId], + bekreftelseId = this[BekreftelserTable.bekreftelseId], + data = this[BekreftelserTable.data] +) + +fun BekreftelseTilgjengelig.asBekreftelseRow( + version: Int, + partition: Int, + offset: Long, + recordKey: Long, +) = BekreftelseRow( + version = version, + partition = partition, + offset = offset, + recordKey = recordKey, + arbeidssoekerId = this.arbeidssoekerId, + periodeId = this.periodeId, + bekreftelseId = this.bekreftelseId, + data = this +) diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/BekreftelserTable.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/BekreftelserTable.kt new file mode 100644 index 00000000..00ecb034 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/BekreftelserTable.kt @@ -0,0 +1,17 @@ +package no.nav.paw.bekreftelse.api.model + +import no.nav.paw.bekreftelse.api.utils.JsonbSerde +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.json.jsonb + +object BekreftelserTable : Table("bekreftelser") { + val version = integer("version") + val partition = integer("kafka_partition") + val offset = long("kafka_offset") + val recordKey = long("record_key") + val arbeidssoekerId = long("arbeidssoeker_id") + val periodeId = uuid("periode_id") + val bekreftelseId = uuid("bekreftelse_id") + val data = jsonb("data", JsonbSerde::serialize, JsonbSerde::deserialize) + override val primaryKey: PrimaryKey = PrimaryKey(version, partition, offset) +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/Bruker.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/Bruker.kt index 8b910aa1..349112ad 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/Bruker.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/Bruker.kt @@ -4,6 +4,7 @@ import no.nav.paw.bekreftelse.melding.v1.vo.Bruker import java.util.* enum class BrukerType { + UKJENT_VERDI, SLUTTBRUKER, VEILEDER } @@ -16,17 +17,18 @@ data class Sluttbruker( val kafkaKey: Long ) -fun InnloggetBruker.asApi(): Bruker { +fun InnloggetBruker.asBruker(): Bruker { return Bruker.newBuilder() .setId(ident) - .setType(type.asApi()) + .setType(type.asBrukerType()) .build() } -fun BrukerType.asApi(): no.nav.paw.bekreftelse.melding.v1.vo.BrukerType { +fun BrukerType.asBrukerType(): no.nav.paw.bekreftelse.melding.v1.vo.BrukerType { return when (this) { BrukerType.SLUTTBRUKER -> no.nav.paw.bekreftelse.melding.v1.vo.BrukerType.SLUTTBRUKER BrukerType.VEILEDER -> no.nav.paw.bekreftelse.melding.v1.vo.BrukerType.VEILEDER + else -> no.nav.paw.bekreftelse.melding.v1.vo.BrukerType.UKJENT_VERDI } } diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/InternState.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/InternState.kt deleted file mode 100644 index f4dbad78..00000000 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/InternState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package no.nav.paw.bekreftelse.api.model - -import no.nav.paw.bekreftelse.internehendelser.BekreftelseTilgjengelig - -data class InternState( - val tilgjendeligeBekreftelser: List -) diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/MottaBekreftelseRequest.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/MottaBekreftelseRequest.kt new file mode 100644 index 00000000..9e754c47 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/MottaBekreftelseRequest.kt @@ -0,0 +1,63 @@ +package no.nav.paw.bekreftelse.api.model + +import no.nav.paw.bekreftelse.internehendelser.BekreftelseTilgjengelig +import no.nav.paw.bekreftelse.melding.v1.Bekreftelse +import no.nav.paw.bekreftelse.melding.v1.vo.Bruker +import no.nav.paw.bekreftelse.melding.v1.vo.Metadata +import no.nav.paw.bekreftelse.melding.v1.vo.Svar +import java.time.Instant +import java.util.* + +data class MottaBekreftelseRequest( + val identitetsnummer: String?, + val bekreftelseId: UUID, + val harJobbetIDennePerioden: Boolean, + val vilFortsetteSomArbeidssoeker: Boolean +) + +fun BekreftelseTilgjengelig.asBekreftelse( + harJobbetIDennePerioden: Boolean, + vilFortsetteSomArbeidssoeker: Boolean, + bruker: Bruker, + kilde: String, + aarsak: String, + namespace: String +): Bekreftelse { + return Bekreftelse.newBuilder() + .setNamespace(namespace) + .setId(bekreftelseId) + .setPeriodeId(periodeId) + .setSvar( + asSvar( + harJobbetIDennePerioden, + vilFortsetteSomArbeidssoeker, + bruker, + kilde, + aarsak + ) + ) + .build() +} + +private fun BekreftelseTilgjengelig.asSvar( + harJobbetIDennePerioden: Boolean, + vilFortsetteSomArbeidssoeker: Boolean, + bruker: Bruker, + kilde: String, + aarsak: String +): Svar { + return Svar.newBuilder() + .setSendtInn( + Metadata.newBuilder() + .setUtfoertAv(bruker) + .setKilde(kilde) + .setAarsak(aarsak) + .setTidspunkt(Instant.now()) + .build() + ) + .setGjelderFra(gjelderFra) + .setGjelderTil(gjelderTil) + .setHarJobbetIDennePerioden(harJobbetIDennePerioden) + .setVilFortsetteSomArbeidssoeker(vilFortsetteSomArbeidssoeker) + .build() +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/TilgjengeligBekreftelse.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/TilgjengeligBekreftelse.kt new file mode 100644 index 00000000..f6233927 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/TilgjengeligBekreftelse.kt @@ -0,0 +1,21 @@ +package no.nav.paw.bekreftelse.api.model + +import no.nav.paw.bekreftelse.internehendelser.BekreftelseTilgjengelig +import java.time.Instant +import java.util.* + +data class TilgjengeligBekreftelse( + val periodeId: UUID, + val bekreftelseId: UUID, + val gjelderFra: Instant, + val gjelderTil: Instant, +) + +fun BekreftelseRow.asTilgjengeligBekreftelse() = this.data.asTilgjengeligBekreftelse() + +private fun BekreftelseTilgjengelig.asTilgjengeligBekreftelse() = TilgjengeligBekreftelse( + periodeId = this.periodeId, + bekreftelseId = this.bekreftelseId, + gjelderFra = this.gjelderFra, + gjelderTil = this.gjelderTil +) diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/TilgjengeligeBekreftelserResponse.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/TilgjengeligeBekreftelserResponse.kt index b99b7599..6d752878 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/TilgjengeligeBekreftelserResponse.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/model/TilgjengeligeBekreftelserResponse.kt @@ -1,24 +1,3 @@ package no.nav.paw.bekreftelse.api.model -import java.time.Instant -import java.util.* - -data class TilgjengeligBekreftelse( - val periodeId: UUID, - val bekreftelseId: UUID, - val gjelderFra: Instant, - val gjelderTil: Instant, -) - typealias TilgjengeligBekreftelserResponse = List - -fun List.toResponse(): TilgjengeligBekreftelserResponse = - this.map { - TilgjengeligBekreftelse( - periodeId = it.periodeId, - bekreftelseId = it.bekreftelseId, - gjelderFra = it.gjelderFra, - gjelderTil = it.gjelderTil - ) - } - diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Database.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Database.kt new file mode 100644 index 00000000..d4cf5d21 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Database.kt @@ -0,0 +1,17 @@ +package no.nav.paw.bekreftelse.api.plugins + + +import io.ktor.server.application.Application +import io.ktor.server.application.install +import no.nav.paw.bekreftelse.api.context.ApplicationContext +import no.nav.paw.bekreftelse.api.plugins.custom.DataSourcePlugin +import no.nav.paw.bekreftelse.api.plugins.custom.FlywayPlugin + +fun Application.configureDatabase(applicationContext: ApplicationContext) { + install(DataSourcePlugin) { + dataSource = applicationContext.dataSource + } + install(FlywayPlugin) { + dataSource = applicationContext.dataSource + } +} diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Kafka.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Kafka.kt index 5a6a3b69..798c193a 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Kafka.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Kafka.kt @@ -4,11 +4,19 @@ package no.nav.paw.bekreftelse.api.plugins import io.ktor.server.application.Application import io.ktor.server.application.install import no.nav.paw.bekreftelse.api.context.ApplicationContext -import java.time.Duration +import no.nav.paw.bekreftelse.api.plugins.custom.KafkaConsumerPlugin +import no.nav.paw.bekreftelse.api.plugins.custom.KafkaProducerPlugin fun Application.configureKafka(applicationContext: ApplicationContext) { - install(KafkaStreamsPlugin) { - shutDownTimeout = Duration.ofSeconds(5) // TODO Legg i konfig - kafkaStreams = listOf(applicationContext.bekreftelseKafkaStreams) + with(applicationContext) { + install(KafkaProducerPlugin) { + kafkaProducers = listOf(bekreftelseKafkaProducer) + } + install(KafkaConsumerPlugin) { + consumeFunction = bekreftelseService::processBekreftelseHendelse + errorFunction = kafkaConsumerExceptionHandler::handleException + consumer = bekreftelseKafkaConsumer + topic = applicationConfig.kafkaTopology.bekreftelseHendelsesloggTopic + } } } diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/KafkaStreams.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/KafkaStreams.kt deleted file mode 100644 index 7566ed6e..00000000 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/KafkaStreams.kt +++ /dev/null @@ -1,71 +0,0 @@ -package no.nav.paw.bekreftelse.api.plugins - -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -import io.ktor.server.application.ApplicationPlugin -import io.ktor.server.application.ApplicationStarted -import io.ktor.server.application.ApplicationStopping -import io.ktor.server.application.createApplicationPlugin -import io.ktor.server.application.hooks.MonitoringEvent -import io.ktor.server.application.log -import io.ktor.util.KtorDsl -import no.nav.paw.bekreftelse.api.config.ApplicationConfig -import no.nav.paw.bekreftelse.api.config.ServerConfig -import no.nav.paw.config.kafka.streams.KafkaStreamsFactory -import no.nav.paw.error.handler.withApplicationTerminatingExceptionHandler -import no.nav.paw.health.listener.withHealthIndicatorStateListener -import no.nav.paw.health.model.LivenessHealthIndicator -import no.nav.paw.health.model.ReadinessHealthIndicator -import no.nav.paw.health.repository.HealthIndicatorRepository -import org.apache.kafka.common.serialization.Serdes -import org.apache.kafka.streams.KafkaStreams -import org.apache.kafka.streams.StreamsConfig -import org.apache.kafka.streams.Topology -import java.time.Duration - -@KtorDsl -class KafkaStreamsPluginConfig { - var shutDownTimeout: Duration? = null - var kafkaStreams: List? = null -} - -val KafkaStreamsPlugin: ApplicationPlugin = - createApplicationPlugin("KafkaStreams", ::KafkaStreamsPluginConfig) { - val shutDownTimeout = requireNotNull(pluginConfig.shutDownTimeout) { "ShutDownTimeout er null" } - val kafkaStreams = requireNotNull(pluginConfig.kafkaStreams) { "KafkaStreams er null" } - - on(MonitoringEvent(ApplicationStarted)) { application -> - application.log.info("Starter Kafka Streams") - kafkaStreams.forEach { stream -> stream.start() } - } - - on(MonitoringEvent(ApplicationStopping)) { application -> - application.log.info("Stopper Kafka Streams") - kafkaStreams.forEach { stream -> stream.close(shutDownTimeout) } - } - } - -fun buildKafkaStreams( - serverConfig: ServerConfig, - applicationConfig: ApplicationConfig, - healthIndicatorRepository: HealthIndicatorRepository, - topology: Topology -): KafkaStreams { - val livenessIndicator = healthIndicatorRepository.addLivenessIndicator(LivenessHealthIndicator()) - val readinessIndicator = healthIndicatorRepository.addReadinessIndicator(ReadinessHealthIndicator()) - - val streamsFactory = KafkaStreamsFactory( - applicationIdSuffix = applicationConfig.kafkaTopology.applicationIdSuffix, - config = applicationConfig.kafkaClients, - ) - .withDefaultKeySerde(Serdes.Long()::class) - .withDefaultValueSerde(SpecificAvroSerde::class) - .withServerConfig(serverConfig.ip, serverConfig.port) - - val kafkaStreams = KafkaStreams( - topology, - StreamsConfig(streamsFactory.properties) - ) - kafkaStreams.withHealthIndicatorStateListener(livenessIndicator, readinessIndicator) - kafkaStreams.withApplicationTerminatingExceptionHandler() - return kafkaStreams -} diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Serialization.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Serialization.kt index d9545f2e..c2faa4f1 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Serialization.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/Serialization.kt @@ -1,21 +1,15 @@ package no.nav.paw.bekreftelse.api.plugins -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.module.kotlin.registerKotlinModule import io.ktor.serialization.jackson.jackson import io.ktor.server.application.Application import io.ktor.server.application.install import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import no.nav.paw.bekreftelse.api.utils.configureJackson fun Application.configureSerialization() { install(ContentNegotiation) { jackson { - disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) - disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - registerModule(JavaTimeModule()) - registerKotlinModule() + configureJackson() } } } \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/DataSourcePlugin.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/DataSourcePlugin.kt new file mode 100644 index 00000000..69a97aec --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/DataSourcePlugin.kt @@ -0,0 +1,35 @@ +package no.nav.paw.bekreftelse.api.plugins.custom + +import io.ktor.events.EventDefinition +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationPlugin +import io.ktor.server.application.ApplicationStarted +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.application.hooks.MonitoringEvent +import io.ktor.server.application.log +import io.ktor.util.KtorDsl +import org.jetbrains.exposed.sql.Database +import javax.sql.DataSource + +val DataSourceReady: EventDefinition = EventDefinition() + +@KtorDsl +class DataSourcePluginConfig { + var dataSource: DataSource? = null + + companion object { + const val PLUGIN_NAME = "DataSourcePlugin" + } +} + +val DataSourcePlugin: ApplicationPlugin = + createApplicationPlugin(DataSourcePluginConfig.PLUGIN_NAME, ::DataSourcePluginConfig) { + application.log.info("Oppretter {}", DataSourcePluginConfig.PLUGIN_NAME) + val dataSource = requireNotNull(pluginConfig.dataSource) { "DataSource er null" } + + on(MonitoringEvent(ApplicationStarted)) { application -> + application.log.info("Initializing data source") + val database = Database.connect(dataSource) + application.environment.monitor.raise(DataSourceReady, application) + } + } diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/FlywayPlugin.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/FlywayPlugin.kt new file mode 100644 index 00000000..ca48a939 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/FlywayPlugin.kt @@ -0,0 +1,41 @@ +package no.nav.paw.bekreftelse.api.plugins.custom + +import io.ktor.events.EventDefinition +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationPlugin +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.application.hooks.MonitoringEvent +import io.ktor.server.application.log +import io.ktor.util.KtorDsl +import org.flywaydb.core.Flyway +import javax.sql.DataSource + +val FlywayMigrationCompleted: EventDefinition = EventDefinition() + +@KtorDsl +class FlywayPluginConfig { + var dataSource: DataSource? = null + var baselineOnMigrate: Boolean = true + + companion object { + const val PLUGIN_NAME = "FlywayPlugin" + } +} + +val FlywayPlugin: ApplicationPlugin = + createApplicationPlugin(FlywayPluginConfig.PLUGIN_NAME, ::FlywayPluginConfig) { + application.log.info("Oppretter {}", FlywayPluginConfig.PLUGIN_NAME) + val dataSource = requireNotNull(pluginConfig.dataSource) { "DataSource er null" } + val baselineOnMigrate = pluginConfig.baselineOnMigrate + + val flyway = Flyway.configure() + .dataSource(dataSource) + .baselineOnMigrate(baselineOnMigrate) + .load() + + on(MonitoringEvent(DataSourceReady)) { application -> + application.log.info("Running database migration") + flyway.migrate() + application.environment.monitor.raise(FlywayMigrationCompleted, application) + } + } diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaConsumerPlugin.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaConsumerPlugin.kt new file mode 100644 index 00000000..6e7c9583 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaConsumerPlugin.kt @@ -0,0 +1,87 @@ +package no.nav.paw.bekreftelse.api.plugins.custom + +import io.ktor.events.EventDefinition +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationPlugin +import io.ktor.server.application.ApplicationStarted +import io.ktor.server.application.ApplicationStopping +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.application.hooks.MonitoringEvent +import io.ktor.server.application.log +import io.ktor.util.KtorDsl +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import no.nav.paw.bekreftelse.internehendelser.BekreftelseHendelse +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.apache.kafka.clients.consumer.KafkaConsumer +import java.time.Duration +import java.util.concurrent.atomic.AtomicBoolean + +val KafkaConsumerReady: EventDefinition = EventDefinition() + +@KtorDsl +class KafkaConsumerPluginConfig { + var consumeFunction: ((ConsumerRecord) -> Unit)? = null + var errorFunction: ((throwable: Throwable) -> Unit)? = null + var consumer: KafkaConsumer? = null + var topic: String? = null + var pollTimeout: Duration = Duration.ofMillis(100) + var shutDownTimeout: Duration = Duration.ofMillis(500) + var rebalanceListener: ConsumerRebalanceListener? = null + val pollingFlag = AtomicBoolean(true) + + companion object { + const val PLUGIN_NAME = "KafkaConsumerPlugin" + } +} + +val KafkaConsumerPlugin: ApplicationPlugin = + createApplicationPlugin(KafkaConsumerPluginConfig.PLUGIN_NAME, ::KafkaConsumerPluginConfig) { + application.log.info("Oppretter {}", KafkaConsumerPluginConfig.PLUGIN_NAME) + val consumeFunction = requireNotNull(pluginConfig.consumeFunction) { "ConsumeFunction er null" } + val errorFunction = pluginConfig.errorFunction ?: { } + val consumer = requireNotNull(pluginConfig.consumer) { "KafkaConsumes er null" } + val topic = requireNotNull(pluginConfig.topic) { "Topic er null" } + val pollTimeout = requireNotNull(pluginConfig.pollTimeout) { "PollTimeout er null" } + val shutDownTimeout = requireNotNull(pluginConfig.shutDownTimeout) { "ShutDownTimeout er null" } + val rebalanceListener = pluginConfig.rebalanceListener + val pollingFlag = pluginConfig.pollingFlag + var consumeJob: Job? = null + + on(MonitoringEvent(ApplicationStarted)) { application -> + application.log.info("Starter Kafka Consumer") + if (rebalanceListener == null) { + consumer.subscribe(listOf(topic)) + } else { + consumer.subscribe(listOf(topic), rebalanceListener) + } + application.environment.monitor.raise(KafkaConsumerReady, application) + } + + on(MonitoringEvent(ApplicationStopping)) { application -> + application.log.info("Stopper Kafka Consumer") + pollingFlag.set(false) + consumeJob?.cancel() + consumer.close(shutDownTimeout) + } + + on(MonitoringEvent(KafkaConsumerReady)) { application -> + consumeJob = application.launch(Dispatchers.IO) { + try { + application.log.info("Starter Kafka Consumer polling") + while (pollingFlag.get()) { + application.log.trace("Polling Kafka Consumer for records") + val records = consumer.poll(pollTimeout) + records.forEach { consumeFunction(it) } + consumer.commitSync() + } + application.log.info("Kafka Consumer polling avsluttet") + } catch (e: Exception) { + application.log.error("Kafka Consumer polling avbrutt med feil", e) + errorFunction(e) + } + } + } + } diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaProducerPlugin.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaProducerPlugin.kt new file mode 100644 index 00000000..00adc9a9 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/plugins/custom/KafkaProducerPlugin.kt @@ -0,0 +1,34 @@ +package no.nav.paw.bekreftelse.api.plugins.custom + +import io.ktor.server.application.ApplicationPlugin +import io.ktor.server.application.ApplicationStopping +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.application.hooks.MonitoringEvent +import io.ktor.server.application.log +import io.ktor.util.KtorDsl +import org.apache.kafka.clients.producer.Producer +import java.time.Duration + +@KtorDsl +class KafkaProducerPluginConfig { + var kafkaProducers: List>? = null + var shutDownTimeout: Duration = Duration.ofMillis(250) + + companion object { + const val PLUGIN_NAME = "KafkaProducerPlugin" + } +} + +val KafkaProducerPlugin: ApplicationPlugin = + createApplicationPlugin(KafkaProducerPluginConfig.PLUGIN_NAME, ::KafkaProducerPluginConfig) { + application.log.info("Oppretter {}", KafkaProducerPluginConfig.PLUGIN_NAME) + val kafkaProducers = requireNotNull(pluginConfig.kafkaProducers) { "KafkaProducers er null" } + val shutDownTimeout = requireNotNull(pluginConfig.shutDownTimeout) { "ShutDownTimeout er null" } + + on(MonitoringEvent(ApplicationStopping)) { application -> + application.log.info("Stopper Kafka Producers") + kafkaProducers.forEach { producer -> + producer.close(shutDownTimeout) + } + } + } \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/producer/BekreftelseKafkaProducer.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/producer/BekreftelseKafkaProducer.kt index 8c212927..85b33854 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/producer/BekreftelseKafkaProducer.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/producer/BekreftelseKafkaProducer.kt @@ -1,43 +1,28 @@ package no.nav.paw.bekreftelse.api.producer -import io.micrometer.core.instrument.MeterRegistry +import kotlinx.coroutines.runBlocking import no.nav.paw.bekreftelse.api.config.ApplicationConfig -import no.nav.paw.bekreftelse.api.utils.buildBekreftelseSerde import no.nav.paw.bekreftelse.api.utils.buildLogger -import no.nav.paw.bekreftelse.api.utils.sendeBekreftelseKafkaCounter import no.nav.paw.bekreftelse.melding.v1.Bekreftelse -import no.nav.paw.config.kafka.KafkaFactory import no.nav.paw.config.kafka.sendDeferred import org.apache.kafka.clients.producer.Producer import org.apache.kafka.clients.producer.ProducerRecord -import org.apache.kafka.common.serialization.LongSerializer class BekreftelseKafkaProducer( private val applicationConfig: ApplicationConfig, - private val meterRegistry: MeterRegistry + private val producer: Producer ) { private val logger = buildLogger - private val bekreftelseSerde = buildBekreftelseSerde() - private var producer: Producer - init { - val kafkaFactory = KafkaFactory(applicationConfig.kafkaClients) - producer = kafkaFactory.createProducer( - clientId = applicationConfig.kafkaTopology.producerId, - keySerializer = LongSerializer::class, - valueSerializer = bekreftelseSerde.serializer()::class - ) - } - - suspend fun produceMessage(key: Long, message: Bekreftelse) { - meterRegistry.sendeBekreftelseKafkaCounter() + fun produceMessage(key: Long, message: Bekreftelse) = runBlocking { val topic = applicationConfig.kafkaTopology.bekreftelseTopic val record = ProducerRecord(topic, key, message) - val recordMetadata = producer.sendDeferred(record).await() - logger.debug("Sendte melding til kafka: offset={}", recordMetadata.offset()) - } - - fun closeProducer() { - producer.close() + val metadata = producer.sendDeferred(record).await() + logger.debug( + "Sender melding til Kafka topic {} (partition={}, offset={})", + topic, + metadata.partition(), + metadata.offset() + ) } -} +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/repository/BekreftelseRepository.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/repository/BekreftelseRepository.kt new file mode 100644 index 00000000..01c2a76d --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/repository/BekreftelseRepository.kt @@ -0,0 +1,63 @@ +package no.nav.paw.bekreftelse.api.repository + +import no.nav.paw.bekreftelse.api.model.BekreftelseRow +import no.nav.paw.bekreftelse.api.model.BekreftelserTable +import no.nav.paw.bekreftelse.api.model.asBekreftelseRow +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.update +import java.util.* + +class BekreftelseRepository { + + fun getByBekreftelseId(bekreftelseId: UUID): BekreftelseRow? { + return BekreftelserTable.selectAll() + .where { BekreftelserTable.bekreftelseId eq bekreftelseId } + .singleOrNull()?.asBekreftelseRow() + } + + fun findByArbeidssoekerId(arbeidssokerId: Long): List { + return BekreftelserTable.selectAll() + .where { BekreftelserTable.arbeidssoekerId eq arbeidssokerId } + .map { it.asBekreftelseRow() } + } + + fun insert(row: BekreftelseRow): Int { + val result = BekreftelserTable.insert { + it[version] = row.version + it[partition] = row.partition + it[offset] = row.offset + it[recordKey] = row.recordKey + it[arbeidssoekerId] = row.arbeidssoekerId + it[periodeId] = row.periodeId + it[bekreftelseId] = row.bekreftelseId + it[data] = row.data + } + return result.insertedCount + } + + fun update(row: BekreftelseRow): Int { + return BekreftelserTable.update(where = { + (BekreftelserTable.arbeidssoekerId eq row.arbeidssoekerId) and + (BekreftelserTable.periodeId eq row.periodeId) and + (BekreftelserTable.bekreftelseId eq row.bekreftelseId) + }) { + it[version] = row.version + it[partition] = row.partition + it[offset] = row.offset + it[recordKey] = row.recordKey + it[data] = row.data + } + } + + fun deleteByBekreftelseId(bekreftelseId: UUID): Int { + return BekreftelserTable.deleteWhere { BekreftelserTable.bekreftelseId eq bekreftelseId } + } + + fun deleteByPeriodeId(periodeId: UUID): Int { + return BekreftelserTable.deleteWhere { BekreftelserTable.periodeId eq periodeId } + } +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/routes/BekreftelseRoutes.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/routes/BekreftelseRoutes.kt index dd6f13f9..b1ccb8fd 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/routes/BekreftelseRoutes.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/routes/BekreftelseRoutes.kt @@ -10,48 +10,39 @@ import io.ktor.server.routing.post import io.ktor.server.routing.route import no.nav.paw.bekreftelse.api.context.ApplicationContext import no.nav.paw.bekreftelse.api.context.resolveRequest -import no.nav.paw.bekreftelse.api.model.BekreftelseRequest +import no.nav.paw.bekreftelse.api.model.Azure +import no.nav.paw.bekreftelse.api.model.MottaBekreftelseRequest import no.nav.paw.bekreftelse.api.model.TilgjengeligeBekreftelserRequest -import no.nav.paw.bekreftelse.api.utils.mottaBekreftelseHttpCounter +import no.nav.paw.bekreftelse.api.model.TokenX import no.nav.poao_tilgang.client.TilgangType fun Route.bekreftelseRoutes(applicationContext: ApplicationContext) { - val prometheusMeterRegistry = applicationContext.prometheusMeterRegistry val authorizationService = applicationContext.authorizationService val bekreftelseService = applicationContext.bekreftelseService route("/api/v1") { - authenticate("idporten", "tokenx", "azure") { + authenticate(TokenX.name, Azure.name) { get("/tilgjengelige-bekreftelser") { val requestContext = resolveRequest() val securityContext = authorizationService.authorize(requestContext, TilgangType.LESE) - val response = bekreftelseService.finnTilgjengeligBekreftelser( - securityContext, - TilgjengeligeBekreftelserRequest(securityContext.sluttbruker.identitetsnummer), - requestContext.useMockData - ) + val response = bekreftelseService.finnTilgjengeligBekreftelser(securityContext.sluttbruker) call.respond(HttpStatusCode.OK, response) } post("/tilgjengelige-bekreftelser") { request -> val requestContext = resolveRequest(request.identitetsnummer) val securityContext = authorizationService.authorize(requestContext, TilgangType.LESE) - val response = bekreftelseService.finnTilgjengeligBekreftelser( - securityContext, - request, - requestContext.useMockData - ) + val response = bekreftelseService.finnTilgjengeligBekreftelser(securityContext.sluttbruker) call.respond(HttpStatusCode.OK, response) } - post("/bekreftelse") { request -> + post("/bekreftelse") { request -> val requestContext = resolveRequest(request.identitetsnummer) val securityContext = authorizationService.authorize(requestContext, TilgangType.SKRIVE) - prometheusMeterRegistry.mottaBekreftelseHttpCounter() bekreftelseService.mottaBekreftelse( - securityContext, + securityContext.innloggetBruker, + securityContext.sluttbruker, request, - requestContext.useMockData ) call.respond(HttpStatusCode.OK) } diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/services/BekreftelseService.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/services/BekreftelseService.kt index 786cc939..63a373e3 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/services/BekreftelseService.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/services/BekreftelseService.kt @@ -1,172 +1,188 @@ package no.nav.paw.bekreftelse.api.services +import io.micrometer.core.instrument.MeterRegistry +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.instrumentation.annotations.WithSpan import no.nav.paw.bekreftelse.api.config.ApplicationConfig import no.nav.paw.bekreftelse.api.config.ServerConfig -import no.nav.paw.bekreftelse.api.consumer.BekreftelseHttpConsumer -import no.nav.paw.bekreftelse.api.context.SecurityContext import no.nav.paw.bekreftelse.api.exception.DataIkkeFunnetForIdException import no.nav.paw.bekreftelse.api.exception.DataTilhoererIkkeBrukerException -import no.nav.paw.bekreftelse.api.exception.SystemfeilException -import no.nav.paw.bekreftelse.api.model.BekreftelseRequest -import no.nav.paw.bekreftelse.api.model.InternState +import no.nav.paw.bekreftelse.api.model.InnloggetBruker +import no.nav.paw.bekreftelse.api.model.MottaBekreftelseRequest +import no.nav.paw.bekreftelse.api.model.Sluttbruker import no.nav.paw.bekreftelse.api.model.TilgjengeligBekreftelserResponse -import no.nav.paw.bekreftelse.api.model.TilgjengeligeBekreftelserRequest -import no.nav.paw.bekreftelse.api.model.asApi -import no.nav.paw.bekreftelse.api.model.toResponse +import no.nav.paw.bekreftelse.api.model.asBekreftelse +import no.nav.paw.bekreftelse.api.model.asBekreftelseRow +import no.nav.paw.bekreftelse.api.model.asBruker +import no.nav.paw.bekreftelse.api.model.asTilgjengeligBekreftelse import no.nav.paw.bekreftelse.api.producer.BekreftelseKafkaProducer +import no.nav.paw.bekreftelse.api.repository.BekreftelseRepository import no.nav.paw.bekreftelse.api.utils.buildLogger -import org.apache.kafka.common.serialization.Serdes -import org.apache.kafka.streams.KafkaStreams -import org.apache.kafka.streams.KeyQueryMetadata -import org.apache.kafka.streams.StoreQueryParameters -import org.apache.kafka.streams.state.HostInfo -import org.apache.kafka.streams.state.QueryableStoreTypes -import org.apache.kafka.streams.state.ReadOnlyKeyValueStore +import no.nav.paw.bekreftelse.api.utils.deleteBekreftelseHendelseCounter +import no.nav.paw.bekreftelse.api.utils.ignoreBekreftelseHendeleCounter +import no.nav.paw.bekreftelse.api.utils.insertBekreftelseHendelseCounter +import no.nav.paw.bekreftelse.api.utils.receiveBekreftelseCounter +import no.nav.paw.bekreftelse.api.utils.receiveBekreftelseHendelseCounter +import no.nav.paw.bekreftelse.api.utils.sendBekreftelseHendelseCounter +import no.nav.paw.bekreftelse.api.utils.updateBekreftelseHendelseCounter +import no.nav.paw.bekreftelse.internehendelser.BekreftelseHendelse +import no.nav.paw.bekreftelse.internehendelser.BekreftelseMeldingMottatt +import no.nav.paw.bekreftelse.internehendelser.BekreftelseTilgjengelig +import no.nav.paw.bekreftelse.internehendelser.PeriodeAvsluttet +import no.nav.paw.bekreftelse.internehendelser.meldingMottattHendelseType +import no.nav.paw.config.env.appImageOrDefaultForLocal +import no.nav.paw.config.env.namespaceOrDefaultForLocal +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.jetbrains.exposed.sql.transactions.transaction class BekreftelseService( private val serverConfig: ServerConfig, private val applicationConfig: ApplicationConfig, - private val bekreftelseHttpConsumer: BekreftelseHttpConsumer, - private val kafkaStreams: KafkaStreams, - private val bekreftelseKafkaProducer: BekreftelseKafkaProducer + private val meterRegistry: MeterRegistry, + private val bekreftelseKafkaProducer: BekreftelseKafkaProducer, + private val bekreftelseRepository: BekreftelseRepository, ) { private val logger = buildLogger - private val mockDataService = MockDataService() - private var internStateStore: ReadOnlyKeyValueStore? = null - - @WithSpan - suspend fun finnTilgjengeligBekreftelser( - securityContext: SecurityContext, - request: TilgjengeligeBekreftelserRequest, - useMockData: Boolean - ): TilgjengeligBekreftelserResponse { - // TODO Fjern når vi har ferdig Kafka-logikk - if (useMockData) { - return mockDataService.finnTilgjengeligBekreftelser(securityContext.sluttbruker.identitetsnummer) - } - - logger.info("Skal hente tilgjengelige bekreftelser") - - val internState = getInternStateStore().get(securityContext.sluttbruker.arbeidssoekerId) - if (internState != null) { - logger.info("Fant {} tilgjengelige bekreftelser i lokal state", internState.tilgjendeligeBekreftelser.size) - return internState.tilgjendeligeBekreftelser.toResponse() - } else { - return finnTilgjengeligBekreftelserFraAnnenNode(securityContext, request) + @WithSpan(value = "finnTilgjengeligBekreftelser") + fun finnTilgjengeligBekreftelser(sluttbruker: Sluttbruker): TilgjengeligBekreftelserResponse { + return transaction { + logger.info("Skal hente tilgjengelige bekreftelser") + val bekreftelser = bekreftelseRepository.findByArbeidssoekerId(sluttbruker.arbeidssoekerId) + bekreftelser.map { it.asTilgjengeligBekreftelse() } } } - @WithSpan - suspend fun mottaBekreftelse( - securityContext: SecurityContext, - request: BekreftelseRequest, - useMockData: Boolean + @WithSpan(value = "mottaBekreftelse") + fun mottaBekreftelse( + innloggetBruker: InnloggetBruker, + sluttbruker: Sluttbruker, + request: MottaBekreftelseRequest ) { - // TODO Fjern når vi har ferdig Kafka-logikk - if (useMockData) { - return mockDataService.mottaBekreftelse(securityContext.sluttbruker.identitetsnummer, request.bekreftelseId) - } + return transaction { + logger.info("Har mottatt bekreftelse") + meterRegistry.receiveBekreftelseCounter(meldingMottattHendelseType) - val internState = getInternStateStore().get(securityContext.sluttbruker.arbeidssoekerId) + val kilde = serverConfig.runtimeEnvironment.appImageOrDefaultForLocal() + val namespace = serverConfig.runtimeEnvironment.namespaceOrDefaultForLocal() - logger.info("Har mottatt bekreftelse") - - if (internState != null) { - val tilgjengeligBekreftelse = internState.tilgjendeligeBekreftelser - .firstOrNull { it.bekreftelseId == request.bekreftelseId } - if (tilgjengeligBekreftelse != null) { - logger.info("Mottok svar for bekreftelse som er i lokal state") - if (tilgjengeligBekreftelse.arbeidssoekerId != securityContext.sluttbruker.arbeidssoekerId) { + val bekreftelse = bekreftelseRepository.getByBekreftelseId(request.bekreftelseId) + if (bekreftelse != null) { + if (bekreftelse.arbeidssoekerId != sluttbruker.arbeidssoekerId) { throw DataTilhoererIkkeBrukerException("Bekreftelse tilhører ikke bruker") } - val bekreftelse = request.asApi( - periodeId = tilgjengeligBekreftelse.periodeId, - gjelderFra = tilgjengeligBekreftelse.gjelderFra, - gjelderTil = tilgjengeligBekreftelse.gjelderTil, - securityContext.innloggetBruker + val key = bekreftelse.recordKey + val message = bekreftelse.data.asBekreftelse( + request.harJobbetIDennePerioden, + request.vilFortsetteSomArbeidssoeker, + innloggetBruker.asBruker(), + kilde, + "Mottok bekreftelse", // TODO Hva skal dette være + namespace ) - bekreftelseKafkaProducer.produceMessage(securityContext.sluttbruker.kafkaKey, bekreftelse) + + logger.debug("Sletter bekreftelse fra database") + bekreftelseRepository.deleteByBekreftelseId(bekreftelse.bekreftelseId) + bekreftelseKafkaProducer.produceMessage(key, message) + meterRegistry.sendBekreftelseHendelseCounter(meldingMottattHendelseType) } else { throw DataIkkeFunnetForIdException("Fant ingen bekreftelse for gitt id") } - } else { - sendBekreftelseTilAnnenNode(securityContext, request) } } - private suspend fun finnTilgjengeligBekreftelserFraAnnenNode( - securityContext: SecurityContext, - request: TilgjengeligeBekreftelserRequest - ): TilgjengeligBekreftelserResponse { - val hostInfo = kafkaStreams.hentHostInfoFraKafka(securityContext.sluttbruker.arbeidssoekerId) - val hostInfoForKey = kafkaStreams.hentHostInfoFraKafka(securityContext.sluttbruker.kafkaKey) - logger.debug("Med store key: {} med stream key {}", hostInfo, hostInfoForKey) - val host = "${hostInfo.host()}:${hostInfo.port()}" - logger.debug("Sjekker om informasjon finnes på node {}", host) - - if (hostInfo.host() == serverConfig.ip) { - logger.info("Fant ingen tilgjengelige bekreftelser for arbeidssøker") - return emptyList() - } + @WithSpan(value = "processBekreftelseHendelse") + fun processBekreftelseHendelse(record: ConsumerRecord) { + transaction { + val hendelse = record.value() - logger.info("Må hente tilgjengelige bekreftelser fra node {}", host) - val tilgjendeligeBekreftelser = bekreftelseHttpConsumer.finnTilgjengeligBekreftelser( - host = host, - bearerToken = securityContext.accessToken.jwt, - request = request - ) - logger.info("Fant {} tilgjengelige bekreftelser på node {}", tilgjendeligeBekreftelser.size, host) - return tilgjendeligeBekreftelser - } + meterRegistry.receiveBekreftelseHendelseCounter(hendelse.hendelseType) - private suspend fun sendBekreftelseTilAnnenNode( - securityContext: SecurityContext, - request: BekreftelseRequest - ) { - val hostInfo = kafkaStreams.hentHostInfoFraKafka(securityContext.sluttbruker.arbeidssoekerId) - val host = "${hostInfo.host()}:${hostInfo.port()}" - logger.debug("Sjekker om informasjon finnes på node {}", host) + logger.debug("Mottok hendelse av type {}", hendelse.hendelseType) - if (hostInfo.host() == serverConfig.ip) { - throw DataIkkeFunnetForIdException("Fant ingen bekreftelse for gitt id") - } + when (hendelse) { + is BekreftelseTilgjengelig -> { + processBekreftelseTilgjengelig(record.partition(), record.offset(), record.key(), hendelse) + } - logger.info("Oversender svar for bekreftelse som er på node {}", host) - bekreftelseHttpConsumer.sendBekreftelse( - host = "${hostInfo.host()}:${hostInfo.port()}", - bearerToken = securityContext.accessToken.jwt, - request = request - ) + is BekreftelseMeldingMottatt -> { + processBekreftelseMeldingMottatt(hendelse) + } + + is PeriodeAvsluttet -> { + processPeriodeAvsluttet(hendelse) + } + + else -> { + processAnnenHendelse(hendelse) + } + } + } } - private fun KafkaStreams.hentHostInfoFraKafka(key: Long): HostInfo { - val metadata = queryMetadataForKey( - applicationConfig.kafkaTopology.internStateStoreName, - key, - Serdes.Long().serializer() + @WithSpan(value = "processBekreftelseTilgjengelig", kind = SpanKind.INTERNAL) + private fun processBekreftelseTilgjengelig( + partition: Int, + offset: Long, + key: Long, + hendelse: BekreftelseTilgjengelig + ) { + val currentSpan = Span.current() + currentSpan.setAttribute("paw.arbeidssoekerregisteret.hendelse.type", hendelse.hendelseType) + + val eksisterendeRow = bekreftelseRepository.getByBekreftelseId(hendelse.bekreftelseId) + val nyRow = hendelse.asBekreftelseRow( + applicationConfig.kafkaTopology.version, + partition, + offset, + key ) - if (metadata == null || metadata == KeyQueryMetadata.NOT_AVAILABLE) { - logger.error("Fant ikke metadata for arbeidsoeker, {}", metadata) - throw SystemfeilException("Fant ikke metadata for arbeidsøker i Kafka Streams") + + if (eksisterendeRow != null) { + if (!eksisterendeRow.harSammeOffset(nyRow)) { + logger.warn("Ny bekreftelse er lik eksisterende men har forskjellig Kafka offset") + } + val rowsAffected = bekreftelseRepository.update(nyRow) + currentSpan.setAttribute("paw.arbeidssoekerregisteret.aksjon", "update") + meterRegistry.updateBekreftelseHendelseCounter(hendelse.hendelseType, rowsAffected) + logger.debug("Oppdaterte bekreftelse av type {} (rows affected {})", hendelse.hendelseType, rowsAffected) } else { - return metadata.activeHost() + val rowsAffected = bekreftelseRepository.insert(nyRow) + currentSpan.setAttribute("paw.arbeidssoekerregisteret.aksjon", "insert") + meterRegistry.insertBekreftelseHendelseCounter(hendelse.hendelseType, rowsAffected) + logger.debug("Opprettet bekreftelse av type {} (rows affected {})", hendelse.hendelseType, rowsAffected) } } - private fun getInternStateStore(): ReadOnlyKeyValueStore { - if (!kafkaStreams.state().isRunningOrRebalancing) { - throw SystemfeilException("Kafka Streams kjører ikke") - } - if (internStateStore == null) { - internStateStore = kafkaStreams.store( - StoreQueryParameters.fromNameAndType( - applicationConfig.kafkaTopology.internStateStoreName, - QueryableStoreTypes.keyValueStore() - ) - ) - } - return internStateStore ?: throw SystemfeilException("Intern state store er ikke initiert") + @WithSpan(value = "processBekreftelseMeldingMottatt", kind = SpanKind.INTERNAL) + private fun processBekreftelseMeldingMottatt(hendelse: BekreftelseMeldingMottatt) { + val rowsAffected = bekreftelseRepository.deleteByBekreftelseId(hendelse.bekreftelseId) + + val currentSpan = Span.current() + currentSpan.setAttribute("paw.arbeidssoekerregisteret.hendelse.type", hendelse.hendelseType) + currentSpan.setAttribute("paw.arbeidssoekerregisteret.aksjon", "delete") + meterRegistry.deleteBekreftelseHendelseCounter(hendelse.hendelseType, rowsAffected) + logger.debug("Slettet bekreftelse(r) av type {} (rows affected {})", hendelse.hendelseType, rowsAffected) + } + + @WithSpan(value = "processPeriodeAvsluttet", kind = SpanKind.INTERNAL) + private fun processPeriodeAvsluttet(hendelse: PeriodeAvsluttet) { + val rowsAffected = bekreftelseRepository.deleteByPeriodeId(hendelse.periodeId) + + val currentSpan = Span.current() + currentSpan.setAttribute("paw.arbeidssoekerregisteret.hendelse.type", hendelse.hendelseType) + currentSpan.setAttribute("paw.arbeidssoekerregisteret.aksjon", "delete") + meterRegistry.deleteBekreftelseHendelseCounter(hendelse.hendelseType, rowsAffected) + logger.debug("Slettet bekreftelse(r) av type {} (rows affected {})", hendelse.hendelseType, rowsAffected) + } + + @WithSpan(value = "processAnnenHendelse", kind = SpanKind.INTERNAL) + private fun processAnnenHendelse(hendelse: BekreftelseHendelse) { + val currentSpan = Span.current() + currentSpan.setAttribute("paw.arbeidssoekerregisteret.hendelse.type", hendelse.hendelseType) + currentSpan.setAttribute("paw.arbeidssoekerregisteret.aksjon", "ignore") + meterRegistry.ignoreBekreftelseHendeleCounter(hendelse.hendelseType) + logger.debug("Ignorerer hendelse av type {}", hendelse.hendelseType) } } \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/services/MockDataService.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/services/MockDataService.kt deleted file mode 100644 index 585306f4..00000000 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/services/MockDataService.kt +++ /dev/null @@ -1,62 +0,0 @@ -package no.nav.paw.bekreftelse.api.services - -import no.nav.paw.bekreftelse.api.model.TilgjengeligBekreftelse -import no.nav.paw.bekreftelse.api.model.TilgjengeligBekreftelserResponse -import no.nav.paw.config.hoplite.loadConfigFromProvidedResource -import java.time.Duration -import java.time.Instant -import java.util.* - -data class MockBekreftelse( - val id: UUID -) - -data class MockPeriode( - val id: UUID, - val bekreftelser: List -) - -data class MockPerson( - val ident: String, - val perioder: List -) - -data class MockData( - val personer: List -) - -// TODO Fjern når vi har ferdig Kafka-logikk -class MockDataService { - - private var mockData = loadConfigFromProvidedResource("/test/mock-data.toml") - private var tilgjengeligBekreftelser = mutableMapOf() - - init { - mockData.personer.forEach { person -> - person.perioder.forEach { periode -> - val bekreftelser = periode.bekreftelser.map { bekreftelse -> - val days = Random().nextLong(1, 13) - val gjelderFra = Instant.now().minus(Duration.ofDays(days)) - val gjelserTil = gjelderFra.plus(Duration.ofDays(14)) - TilgjengeligBekreftelse(periode.id, bekreftelse.id, gjelderFra, gjelserTil) - } - tilgjengeligBekreftelser[person.ident] = bekreftelser - } - } - } - - fun finnTilgjengeligBekreftelser(identitetsnummer: String): TilgjengeligBekreftelserResponse { - return tilgjengeligBekreftelser[identitetsnummer] ?: emptyList() - } - - fun mottaBekreftelse( - identitetsnummer: String, - bekreftelseId: UUID - ) { - val eksisterende = tilgjengeligBekreftelser[identitetsnummer] - if (eksisterende != null) { - val oppdatert = eksisterende.filter { it.bekreftelseId != bekreftelseId } - tilgjengeligBekreftelser[identitetsnummer] = oppdatert - } - } -} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/topology/BekreftelseHendelseProcessor.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/topology/BekreftelseHendelseProcessor.kt deleted file mode 100644 index 32548900..00000000 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/topology/BekreftelseHendelseProcessor.kt +++ /dev/null @@ -1,187 +0,0 @@ -package no.nav.paw.bekreftelse.api.topology - -import io.micrometer.core.instrument.MeterRegistry -import io.opentelemetry.api.trace.Span -import io.opentelemetry.api.trace.SpanKind -import io.opentelemetry.instrumentation.annotations.WithSpan -import no.nav.paw.bekreftelse.api.model.InternState -import no.nav.paw.bekreftelse.api.utils.buildStreamsLogger -import no.nav.paw.bekreftelse.api.utils.ignorertBekreftelseHendeleCounter -import no.nav.paw.bekreftelse.api.utils.lagreBekreftelseHendelseCounter -import no.nav.paw.bekreftelse.api.utils.mottattBekreftelseHendelseKafkaCounter -import no.nav.paw.bekreftelse.api.utils.slettetBekreftelseHendelseCounter -import no.nav.paw.bekreftelse.internehendelser.BekreftelseHendelse -import no.nav.paw.bekreftelse.internehendelser.BekreftelseMeldingMottatt -import no.nav.paw.bekreftelse.internehendelser.BekreftelseTilgjengelig -import no.nav.paw.bekreftelse.internehendelser.PeriodeAvsluttet -import org.apache.kafka.streams.kstream.KStream -import org.apache.kafka.streams.kstream.Named -import org.apache.kafka.streams.processor.api.Processor -import org.apache.kafka.streams.processor.api.ProcessorContext -import org.apache.kafka.streams.processor.api.Record -import org.apache.kafka.streams.state.KeyValueStore - -private val logger = buildStreamsLogger - -fun KStream.oppdaterBekreftelseHendelseState( - stateStoreName: String, - meterRegistry: MeterRegistry -): KStream { - val processor = { - BekreftelseHendelseProcessor(stateStoreName, meterRegistry) - } - return process(processor, Named.`as`("bekreftelseHendelseProcessor"), stateStoreName) -} - -class BekreftelseHendelseProcessor( - private val stateStoreName: String, - private val meterRegistry: MeterRegistry -) : Processor { - private var stateStore: KeyValueStore? = null - private var context: ProcessorContext? = null - - override fun init(context: ProcessorContext) { - super.init(context) - this.context = context - stateStore = context.getStateStore(stateStoreName) - } - - // TODO Legg til metrics - @WithSpan - override fun process(record: Record?) { - val hendelse = record?.value() ?: return - - val internStateStore = requireNotNull(stateStore) { "Intern state store er ikke initiert" } - - meterRegistry.mottattBekreftelseHendelseKafkaCounter(hendelse.hendelseType) - logger.debug("Mottok hendelse av type {}", hendelse.hendelseType) - - when (hendelse) { - is BekreftelseTilgjengelig -> { - internStateStore.processBekreftelseTilgjengelig(meterRegistry, hendelse) - } - - is BekreftelseMeldingMottatt -> { - internStateStore.processBekreftelseMeldingMottatt(meterRegistry, hendelse) - } - - is PeriodeAvsluttet -> { - internStateStore.processPeriodeAvsluttet(meterRegistry, hendelse) - } - - else -> { - processAnnenHendelse(meterRegistry, hendelse) - } - } - } -} - -@WithSpan( - value = "processBekreftelseTilgjengelig", - kind = SpanKind.INTERNAL -) -fun KeyValueStore.processBekreftelseTilgjengelig( - meterRegistry: MeterRegistry, - hendelse: BekreftelseTilgjengelig -) { - val currentSpan = Span.current() - currentSpan.setAttribute("paw.arbeidssoekerregisteret.hendelse.type", hendelse.hendelseType) - currentSpan.setAttribute("paw.arbeidssoekerregisteret.aksjon", "lagrer_bekreftelse") - meterRegistry.lagreBekreftelseHendelseCounter(hendelse.hendelseType) - val internState = get(hendelse.arbeidssoekerId) - if (internState != null) { - val originalSize = internState.tilgjendeligeBekreftelser.size - val tilgjengeligeBekreftelser = internState.tilgjendeligeBekreftelser + hendelse - val nySize = tilgjengeligeBekreftelser.size - logger.debug("Lagrer bekreftelse i eksisterende state ({} -> {})", originalSize, nySize) - put(hendelse.arbeidssoekerId, InternState(tilgjengeligeBekreftelser)) - } else { - logger.debug("Lagrer bekreftelse i ny state ({} -> {})", 0, 1) - put(hendelse.arbeidssoekerId, InternState(listOf(hendelse))) - } -} - -@WithSpan( - value = "processBekreftelseMeldingMottatt", - kind = SpanKind.INTERNAL -) -fun KeyValueStore.processBekreftelseMeldingMottatt( - meterRegistry: MeterRegistry, - hendelse: BekreftelseMeldingMottatt -) { - val currentSpan = Span.current() - currentSpan.setAttribute("paw.arbeidssoekerregisteret.hendelse.type", hendelse.hendelseType) - - val internState = get(hendelse.arbeidssoekerId) - if (internState != null) { - val originalSize = internState.tilgjendeligeBekreftelser.size - internState.tilgjendeligeBekreftelser - .filterNot { it.bekreftelseId == hendelse.bekreftelseId } - .let { bekreftelser -> - if (bekreftelser.isEmpty()) { - currentSpan.setAttribute("paw.arbeidssoekerregisteret.aksjon", "sletter_intern_state") - meterRegistry.slettetBekreftelseHendelseCounter(hendelse.hendelseType) - logger.debug("Sletter all state ({} -> {})", originalSize, 0) - delete(hendelse.arbeidssoekerId) - } else { - val nySize = bekreftelser.size - currentSpan.setAttribute("paw.arbeidssoekerregisteret.aksjon", "sletter_bekreftelser") - meterRegistry.slettetBekreftelseHendelseCounter(hendelse.hendelseType, nySize.toLong()) - logger.debug("Sletter bekreftelser ({} -> {})", originalSize, nySize) - put(hendelse.arbeidssoekerId, InternState(bekreftelser)) - } - } - } else { - logger.warn("Mottok bekreftelsesmelding, men det finnes ingen state for arbeidssøker") - } -} - -@WithSpan( - value = "processPeriodeAvsluttet", - kind = SpanKind.INTERNAL -) -fun KeyValueStore.processPeriodeAvsluttet( - meterRegistry: MeterRegistry, - hendelse: PeriodeAvsluttet -) { - val currentSpan = Span.current() - currentSpan.setAttribute("paw.arbeidssoekerregisteret.hendelse.type", hendelse.hendelseType) - - val internState = get(hendelse.arbeidssoekerId) - if (internState != null) { - val originalSize = internState.tilgjendeligeBekreftelser.size - internState.tilgjendeligeBekreftelser - .filterNot { it.periodeId == hendelse.periodeId } - .let { bekreftelser -> - if (bekreftelser.isEmpty()) { - currentSpan.setAttribute("paw.arbeidssoekerregisteret.aksjon", "sletter_intern_state") - meterRegistry.slettetBekreftelseHendelseCounter(hendelse.hendelseType) - logger.debug("Sletter all state ({} -> {})", originalSize, 0) - delete(hendelse.arbeidssoekerId) - } else { - val nySize = bekreftelser.size - currentSpan.setAttribute("paw.arbeidssoekerregisteret.aksjon", "sletter_bekreftelser") - meterRegistry.slettetBekreftelseHendelseCounter(hendelse.hendelseType, nySize.toLong()) - logger.debug("Sletter bekreftelser for periode ({} -> {})", originalSize, nySize) - put(hendelse.arbeidssoekerId, InternState(bekreftelser)) - } - } - } else { - logger.warn("Mottok melding om avsluttet periode, men det finnes ingen state for arbeidssøker") - } -} - -@WithSpan( - value = "processAnnenHendelse", - kind = SpanKind.INTERNAL -) -fun processAnnenHendelse( - meterRegistry: MeterRegistry, - hendelse: BekreftelseHendelse -) { - val currentSpan = Span.current() - currentSpan.setAttribute("paw.arbeidssoekerregisteret.hendelse.type", hendelse.hendelseType) - currentSpan.setAttribute("paw.arbeidssoekerregisteret.aksjon", "ignorerer_hendelse") - meterRegistry.ignorertBekreftelseHendeleCounter(hendelse.hendelseType) - logger.debug("Ignorerer hendelse av type {}", hendelse.hendelseType) -} diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/topology/BekreftelseHendelseTopology.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/topology/BekreftelseHendelseTopology.kt deleted file mode 100644 index edffa623..00000000 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/topology/BekreftelseHendelseTopology.kt +++ /dev/null @@ -1,39 +0,0 @@ -package no.nav.paw.bekreftelse.api.topology - -import io.micrometer.core.instrument.MeterRegistry -import no.nav.paw.bekreftelse.api.config.ApplicationConfig -import no.nav.paw.bekreftelse.api.utils.buildInternStateSerde -import no.nav.paw.bekreftelse.internehendelser.BekreftelseHendelseSerde -import org.apache.kafka.common.serialization.Serdes -import org.apache.kafka.streams.StreamsBuilder -import org.apache.kafka.streams.Topology -import org.apache.kafka.streams.kstream.Consumed -import org.apache.kafka.streams.state.Stores - -fun buildBekreftelseTopology( - applicationConfig: ApplicationConfig, - meterRegistry: MeterRegistry -): Topology = StreamsBuilder().apply { - addInternStateStore(applicationConfig) - addBekreftelseKStream(applicationConfig, meterRegistry) -}.build() - -private fun StreamsBuilder.addInternStateStore(applicationConfig: ApplicationConfig) { - addStateStore( - Stores.keyValueStoreBuilder( - Stores.persistentKeyValueStore(applicationConfig.kafkaTopology.internStateStoreName), - Serdes.Long(), - buildInternStateSerde(), - ) - ) -} - -private fun StreamsBuilder.addBekreftelseKStream( - applicationConfig: ApplicationConfig, - meterRegistry: MeterRegistry -) { - with(applicationConfig.kafkaTopology) { - stream(bekreftelseHendelsesloggTopic, Consumed.with(Serdes.Long(), BekreftelseHendelseSerde())) - .oppdaterBekreftelseHendelseState(internStateStoreName, meterRegistry) - } -} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Database.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Database.kt new file mode 100644 index 00000000..e89636c9 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Database.kt @@ -0,0 +1,20 @@ +package no.nav.paw.bekreftelse.api.utils + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import no.nav.paw.bekreftelse.api.config.DatabaseConfig +import javax.sql.DataSource + +fun createDataSource(config: DatabaseConfig): DataSource { + return HikariDataSource( + HikariConfig().apply { + jdbcUrl = config.jdbcUrl + driverClassName = config.driverClassName + isAutoCommit = config.autoCommit + maximumPoolSize = config.maxPoolSize + connectionTimeout = config.connectionTimeout.toMillis() + idleTimeout = config.idleTimeout.toMillis() + maxLifetime = config.maxLifetime.toMillis() + } + ) +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/HttpClient.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/HttpClient.kt deleted file mode 100644 index 41a2bbfa..00000000 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/HttpClient.kt +++ /dev/null @@ -1,33 +0,0 @@ -package no.nav.paw.bekreftelse.api.utils - -import io.ktor.client.statement.HttpResponse -import io.ktor.client.statement.bodyAsText -import io.ktor.http.HttpStatusCode -import io.ktor.http.isSuccess -import no.nav.paw.error.exception.ClientResponseException - -private fun HttpStatusCode.hasBody(): Boolean { - return this == HttpStatusCode.BadRequest || - this == HttpStatusCode.Forbidden || - this == HttpStatusCode.NotFound || - this == HttpStatusCode.InternalServerError -} - -suspend fun handleError(response: HttpResponse) { - if (response.status.isSuccess()) { - return - } else if (response.status.hasBody()) { - val error = response.bodyAsText() - throw ClientResponseException( - HttpStatusCode.InternalServerError, - "PAW_HTTP_KLIENT_KALL_FEILET", - "HTTP-kall feilet med status: ${response.status} og body:\n$error" - ) - } else { - throw ClientResponseException( - HttpStatusCode.InternalServerError, - "PAW_HTTP_KLIENT_KALL_FEILET", - "HTTP-kall feilet med status: ${response.status}" - ) - } -} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Jackson.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Jackson.kt index 45e22f76..b8bb6dbc 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Jackson.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Jackson.kt @@ -28,4 +28,4 @@ fun ObjectMapper.configureJackson() { enable(KotlinFeature.NullToEmptyCollection) enable(KotlinFeature.NullToEmptyMap) } -} \ No newline at end of file +} diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/JsonSerialization.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/JsonSerialization.kt new file mode 100644 index 00000000..cde471c1 --- /dev/null +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/JsonSerialization.kt @@ -0,0 +1,11 @@ +package no.nav.paw.bekreftelse.api.utils + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.paw.bekreftelse.internehendelser.BekreftelseTilgjengelig + +private val objectMapper = buildObjectMapper + +object JsonbSerde { + fun serialize(data: BekreftelseTilgjengelig): String = objectMapper.writeValueAsString(data) + fun deserialize(data: String) = objectMapper.readValue(data) +} diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/KafkaSerialization.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/KafkaSerialization.kt index 76fd01c2..df60cb0e 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/KafkaSerialization.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/KafkaSerialization.kt @@ -1,56 +1,6 @@ package no.nav.paw.bekreftelse.api.utils -import com.fasterxml.jackson.core.JsonProcessingException -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -import no.nav.paw.bekreftelse.api.model.InternState +import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerializer import no.nav.paw.bekreftelse.melding.v1.Bekreftelse -import no.nav.paw.config.env.ProdGcp -import no.nav.paw.config.env.RuntimeEnvironment -import no.nav.paw.config.env.currentRuntimeEnvironment -import org.apache.kafka.common.serialization.Deserializer -import org.apache.kafka.common.serialization.Serde -import org.apache.kafka.common.serialization.Serializer -inline fun buildJsonSerializer(runtimeEnvironment: RuntimeEnvironment, objectMapper: ObjectMapper) = object : Serializer { - override fun serialize(topic: String?, data: T): ByteArray { - if (data == null) return byteArrayOf() - try { - return objectMapper.writeValueAsBytes(data) - } catch (e: Exception) { - if (runtimeEnvironment is ProdGcp && e is JsonProcessingException) e.clearLocation() - throw e - } - } -} - -inline fun buildJsonDeserializer(runtimeEnvironment: RuntimeEnvironment, objectMapper: ObjectMapper) = object : Deserializer { - override fun deserialize(topic: String?, data: ByteArray?): T? { - if (data == null) return null - try { - return objectMapper.readValue(data) - } catch (e: Exception) { - if (runtimeEnvironment is ProdGcp && e is JsonProcessingException) e.clearLocation() - throw e - } - } -} - -inline fun buildJsonSerde(runtimeEnvironment: RuntimeEnvironment, objectMapper: ObjectMapper) = object : Serde { - override fun serializer(): Serializer { - return buildJsonSerializer(runtimeEnvironment, objectMapper) - } - - override fun deserializer(): Deserializer { - return buildJsonDeserializer(runtimeEnvironment, objectMapper) - } -} - -inline fun buildJsonSerde(): Serde { - return buildJsonSerde(currentRuntimeEnvironment, buildObjectMapper) -} - -fun buildInternStateSerde() = buildJsonSerde() - -fun buildBekreftelseSerde() = SpecificAvroSerde() +class BekreftelseAvroSerializer : SpecificAvroSerializer() diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Logger.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Logger.kt index 7de5932d..dfcfff6c 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Logger.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Logger.kt @@ -13,8 +13,7 @@ import org.slf4j.LoggerFactory inline val T.buildLogger: Logger get() = LoggerFactory.getLogger(T::class.java) inline val buildApplicationLogger: Logger get() = LoggerFactory.getLogger("no.nav.paw.logger.application") -inline val buildStreamsLogger: Logger get() = LoggerFactory.getLogger("no.nav.paw.logger.application.streams") - +inline val buildErrorLogger: Logger get() = LoggerFactory.getLogger("no.nav.paw.logger.error") inline val buildAuditLogger: Logger get() = LoggerFactory.getLogger("AuditLogger") fun Logger.audit( diff --git a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Metrics.kt b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Metrics.kt index eb050590..b6f31e2b 100644 --- a/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Metrics.kt +++ b/apps/bekreftelse-api/src/main/kotlin/no/nav/paw/bekreftelse/api/utils/Metrics.kt @@ -7,68 +7,57 @@ import java.util.concurrent.atomic.AtomicLong private const val METRIC_PREFIX = "paw_arbeidssoekerregisteret_api_bekreftelse" -fun MeterRegistry.mottaBekreftelseHttpCounter() { +fun MeterRegistry.receiveBekreftelseCounter(type: String, amount: Int = 1) { counter( "${METRIC_PREFIX}_counter", Tags.of( - Tag.of("action", "mottatt_bekreftelse"), + Tag.of("action", "receive"), Tag.of("channel", "http") ) ).increment() + bekreftelseCounter(type, "receive", "http", "kafka", amount) } -fun MeterRegistry.sendeBekreftelseKafkaCounter() { - counter( - "${METRIC_PREFIX}_counter", - Tags.of( - Tag.of("action", "sendt_bekreftelse"), - Tag.of("channel", "kafka") - ) - ).increment() +fun MeterRegistry.sendBekreftelseHendelseCounter(type: String, amount: Int = 1) { + bekreftelseCounter(type, "send", "http", "kafka", amount) } -fun MeterRegistry.mottattBekreftelseHendelseKafkaCounter(hendelseType: String) { - counter( - "${METRIC_PREFIX}_counter", - Tags.of( - Tag.of("action", "mottatt_bekreftelse_hendelse"), - Tag.of("type", hendelseType), - Tag.of("channel", "kafka") - ) - ).increment() +fun MeterRegistry.receiveBekreftelseHendelseCounter(type: String, amount: Int = 1) { + bekreftelseCounter(type, "receive", "kafka", "database", amount) } -fun MeterRegistry.lagreBekreftelseHendelseCounter(hendelseType: String, amount: Long = 1) { - counter( - "${METRIC_PREFIX}_counter", - Tags.of( - Tag.of("action", "lagret_bekreftelse_hendelse"), - Tag.of("type", hendelseType), - Tag.of("channel", "state") - ) - ).increment(amount.toDouble()) +fun MeterRegistry.insertBekreftelseHendelseCounter(type: String, amount: Int = 1) { + bekreftelseCounter(type, "insert", "kafka", "database", amount) } -fun MeterRegistry.slettetBekreftelseHendelseCounter(hendelseType: String, amount: Long = 1) { - counter( - "${METRIC_PREFIX}_counter", - Tags.of( - Tag.of("action", "slettet_bekreftelse_hendelse"), - Tag.of("type", hendelseType), - Tag.of("channel", "state") - ) - ).increment(amount.toDouble()) +fun MeterRegistry.updateBekreftelseHendelseCounter(type: String, amount: Int = 1) { + bekreftelseCounter(type, "update", "kafka", "database", amount) } -fun MeterRegistry.ignorertBekreftelseHendeleCounter(hendelseType: String) { +fun MeterRegistry.deleteBekreftelseHendelseCounter(type: String, amount: Int = 1) { + bekreftelseCounter(type, "delete", "kafka", "database", amount) +} + +fun MeterRegistry.ignoreBekreftelseHendeleCounter(type: String, amount: Int = 1) { + bekreftelseCounter(type, "ignore", "kafka", "none", amount) +} + +fun MeterRegistry.bekreftelseCounter( + type: String, + action: String, + source: String, + target: String, + amount: Int = 1 +) { counter( "${METRIC_PREFIX}_counter", Tags.of( - Tag.of("action", "ignorert_bekreftelse_hendelse"), - Tag.of("type", hendelseType), - Tag.of("channel", "state") + Tag.of("type", type), + Tag.of("action", action), + Tag.of("source", source), + Tag.of("target", target) ) - ).increment() + ).increment(amount.toDouble()) } fun MeterRegistry.lagredeBekreftelserTotaltGauge(antallReference: AtomicLong) { diff --git a/apps/bekreftelse-api/src/main/resources/db/migration/V1__database_model.sql b/apps/bekreftelse-api/src/main/resources/db/migration/V1__database_model.sql new file mode 100644 index 00000000..81a1425b --- /dev/null +++ b/apps/bekreftelse-api/src/main/resources/db/migration/V1__database_model.sql @@ -0,0 +1,12 @@ +CREATE TABLE bekreftelser +( + version SMALLINT NOT NULL, + kafka_partition SMALLINT NOT NULL, + kafka_offset BIGINT NOT NULL, + record_key BIGINT NOT NULL, + arbeidssoeker_id BIGINT NOT NULL, + periode_id UUID NOT NULL, + bekreftelse_id UUID NOT NULL UNIQUE, + data JSONB NOT NULL, + primary key (version, kafka_partition, kafka_offset) +); diff --git a/apps/bekreftelse-api/src/main/resources/db/migration/V2__add_indexes.sql b/apps/bekreftelse-api/src/main/resources/db/migration/V2__add_indexes.sql new file mode 100644 index 00000000..81f61b2a --- /dev/null +++ b/apps/bekreftelse-api/src/main/resources/db/migration/V2__add_indexes.sql @@ -0,0 +1,3 @@ +CREATE index bekreftelser_arbeidssoeker_id_idx ON bekreftelser(arbeidssoeker_id); +CREATE index bekreftelser_periode_id_idx ON bekreftelser(periode_id); +CREATE index bekreftelser_bekreftelse_id_idx ON bekreftelser(bekreftelse_id); diff --git a/apps/bekreftelse-api/src/main/resources/local/application_config.toml b/apps/bekreftelse-api/src/main/resources/local/application_config.toml index 83d5c1e7..ff7b5f06 100644 --- a/apps/bekreftelse-api/src/main/resources/local/application_config.toml +++ b/apps/bekreftelse-api/src/main/resources/local/application_config.toml @@ -1,34 +1,22 @@ [autorisasjon] corsAllowOrigins = "localhost" -[kafkaTopology] -applicationIdSuffix = "v1" -producerId = "paw-arbeidssoekerregisteret-api-bekreftelse" -bekreftelseTopic = "paw.arbeidssoker-bekreftelse-v1" -bekreftelseHendelsesloggTopic = "paw.arbeidssoker-bekreftelse-hendelseslogg-v1" -internStateStoreName = "intern-state-store" - -[[authProviders]] -name = "idporten" -discoveryUrl = "http://localhost:8081/idporten/.well-known/openid-configuration" -clientId = "paw-arbeidssoekerregisteret-api-bekreftelse" -[authProviders.claims] -map = ["acr=idporten-loa-high"] - [[authProviders]] name = "tokenx" discoveryUrl = "http://localhost:8081/tokenx/.well-known/openid-configuration" clientId = "paw-arbeidssoekerregisteret-api-bekreftelse" -[authProviders.claims] -map = ["acr=Level4", "acr=idporten-loa-high"] -combineWithOr = true + + [authProviders.claims] + map = ["acr=Level4", "acr=idporten-loa-high"] + combineWithOr = true [[authProviders]] name = "azure" discoveryUrl = "http://localhost:8081/azure/.well-known/openid-configuration" clientId = "paw-arbeidssoekerregisteret-api-bekreftelse" -[authProviders.claims] -map = ["NAVident"] + + [authProviders.claims] + map = ["NAVident"] [azureM2M] tokenEndpointUrl = "http://localhost:8081/azure/token" @@ -45,5 +33,19 @@ scope = "api://test.test.kafka-keys/.default" [kafkaClients] brokers = "localhost:9092" applicationIdPrefix = "paw-arbeidssoekerregisteret-api-bekreftelse" -[kafkaClients.schemaRegistry] -url = "http://localhost:8082" + + [kafkaClients.schemaRegistry] + url = "http://localhost:8082" + +[kafkaTopology] +version = 1 +antallPartitioner = 1 +producerId = "paw-arbeidssoekerregisteret-api-bekreftelse-v1-producer" +consumerId = "paw-arbeidssoekerregisteret-api-bekreftelse-v1-consumer" +consumerGroupId = "paw-arbeidssoekerregisteret-api-bekreftelse-v1" +bekreftelseTopic = "paw.arbeidssoker-bekreftelse-v1" +bekreftelseHendelsesloggTopic = "paw.arbeidssoker-bekreftelse-hendelseslogg-v1" + +[database] +jdbcUrl = "jdbc:postgresql://localhost:5432/bekreftelser?user=bekreftelse_api&password=5up3r_53cr3t_p455w0rd" +driverClassName = "org.postgresql.Driver" diff --git a/apps/bekreftelse-api/src/main/resources/logback.xml b/apps/bekreftelse-api/src/main/resources/logback.xml index 336d9d42..13f7971d 100644 --- a/apps/bekreftelse-api/src/main/resources/logback.xml +++ b/apps/bekreftelse-api/src/main/resources/logback.xml @@ -6,7 +6,7 @@ - %d{yyyy-MM-dd HH:mm:ss.SSS} %5p %c{1}:%L - %m%n + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %5level %c{1}:%logger{36} - %msg%n @@ -20,6 +20,7 @@ + @@ -28,7 +29,6 @@ - diff --git a/apps/bekreftelse-api/src/main/resources/nais/application_config.toml b/apps/bekreftelse-api/src/main/resources/nais/application_config.toml index 0c0832de..fcb5fa25 100644 --- a/apps/bekreftelse-api/src/main/resources/nais/application_config.toml +++ b/apps/bekreftelse-api/src/main/resources/nais/application_config.toml @@ -1,34 +1,22 @@ [autorisasjon] corsAllowOrigins = "${CORS_ALLOW_ORIGINS}" -[kafkaTopology] -applicationIdSuffix = "${KAFKA_STREAMS_ID_SUFFIX}" -producerId = "${NAIS_APP_NAME}" -bekreftelseTopic = "${KAFKA_PAW_ARBEIDSSOKER_BEKREFTELSE_TOPIC}" -bekreftelseHendelsesloggTopic = "${KAFKA_PAW_ARBEIDSSOKER_BEKREFTELSE_HENDELSESLOGG_TOPIC}" -internStateStoreName = "intern-state-store" - -[[authProviders]] -name = "idporten" -discoveryUrl = "${IDPORTEN_WELL_KNOWN_URL} -clientId = "${IDPORTEN_CLIENT_ID}" -[authProviders.claims] -map = ["acr=idporten-loa-high"] - [[authProviders]] name = "tokenx" discoveryUrl = "${TOKEN_X_WELL_KNOWN_URL}" clientId = "${TOKEN_X_CLIENT_ID}" -[authProviders.claims] -map = ["acr=Level4", "acr=idporten-loa-high"] -combineWithOr = true + + [authProviders.claims] + map = ["acr=Level4", "acr=idporten-loa-high"] + combineWithOr = true [[authProviders]] name = "azure" discoveryUrl = "${AZURE_APP_WELL_KNOWN_URL}" clientId = "${AZURE_APP_CLIENT_ID}" -[authProviders.claims] -map = ["NAVident"] + + [authProviders.claims] + map = ["NAVident"] [azureM2M] tokenEndpointUrl = "${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT}" @@ -45,11 +33,26 @@ scope = "api://${NAIS_CLUSTER_NAME}.paw.paw-kafka-key-generator/.default" [kafkaClients] brokers = "${KAFKA_BROKERS}" applicationIdPrefix = "${KAFKA_STREAMS_APPLICATION_ID}" -[kafkaClients.authentication] -keystorePath = "${KAFKA_KEYSTORE_PATH}" -truststorePath = "${KAFKA_TRUSTSTORE_PATH}" -credstorePassword = "${KAFKA_CREDSTORE_PASSWORD}" -[kafkaClients.schemaRegistry] -url = "${KAFKA_SCHEMA_REGISTRY}" -username = "${KAFKA_SCHEMA_REGISTRY_USER}" -password = "${KAFKA_SCHEMA_REGISTRY_PASSWORD}" + + [kafkaClients.authentication] + keystorePath = "${KAFKA_KEYSTORE_PATH}" + truststorePath = "${KAFKA_TRUSTSTORE_PATH}" + credstorePassword = "${KAFKA_CREDSTORE_PASSWORD}" + + [kafkaClients.schemaRegistry] + url = "${KAFKA_SCHEMA_REGISTRY}" + username = "${KAFKA_SCHEMA_REGISTRY_USER}" + password = "${KAFKA_SCHEMA_REGISTRY_PASSWORD}" + +[kafkaTopology] +version = 1 +antallPartitioner = 6 +producerId = "${NAIS_APP_NAME}-v1-producer" +consumerId = "${NAIS_APP_NAME}-v1-consumer" +consumerGroupId = "${NAIS_APP_NAME}-v1" +bekreftelseTopic = "${KAFKA_PAW_ARBEIDSSOKER_BEKREFTELSE_TOPIC}" +bekreftelseHendelsesloggTopic = "${KAFKA_PAW_ARBEIDSSOKER_BEKREFTELSE_HENDELSESLOGG_TOPIC}" + +[database] +jdbcUrl = "${NAIS_DATABASE_PAW_ARBEIDSSOEKERREGISTERET_API_BEKREFTELSE_BEKREFTELSER_JDBC_URL}" +driverClassName = "org.postgresql.Driver" diff --git a/apps/bekreftelse-api/src/main/resources/test/mock-data.toml b/apps/bekreftelse-api/src/main/resources/test/mock-data.toml deleted file mode 100644 index 6fba5d9f..00000000 --- a/apps/bekreftelse-api/src/main/resources/test/mock-data.toml +++ /dev/null @@ -1,38 +0,0 @@ -[[personer]] -ident = "17830348441" -[[personer.perioder]] -id = "84201f96-363b-4aab-a589-89fa4b9b1feb" -[[personer.perioder.bekreftelser]] -id = "f45ffbf3-e4d5-49fd-b5b7-17aaee478dfc" - -[[personer]] -ident = "19519238708" -[[personer.perioder]] -id = "ec6b5a10-b67c-42c1-b6e7-a642c36bd78e" -[[personer.perioder.bekreftelser]] -id = "0cae8890-5500-4f5f-8fc1-9a0aae3b35a0" - -[[personer]] -ident = "02837797848" -[[personer.perioder]] -id = "44a4375c-b7ab-40ea-83f5-0eb9869925eb" -[[personer.perioder.bekreftelser]] -id = "4f5e7f5c-1fe3-4b27-a07b-34ff9f4ea23f" - -[[personer]] -ident = "16868598968" -[[personer.perioder]] -id = "bbf3e9eb-6d7b-465b-bf79-ae6c82cf1ddd" -[[personer.perioder.bekreftelser]] -id = "47e5c02d-abab-4e75-951c-db6c985901e4" -[[personer.perioder.bekreftelser]] -id = "77322685-80db-41db-b79f-86915a9a5d9a" - -[[personer]] -ident = "28878098821" -[[personer.perioder]] -id = "6ea57aec-353c-4df5-935f-9bead8afb221" -[[personer.perioder.bekreftelser]] -id = "992d5363-bab4-4b1d-987e-3e8eb4db3f64" -[[personer.perioder.bekreftelser]] -id = "9777408c-938d-41e6-b9fd-5177120695d6" diff --git a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/ApplicationTestContext.kt b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/ApplicationTestContext.kt index 92c4ec3d..7358cabd 100644 --- a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/ApplicationTestContext.kt +++ b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/ApplicationTestContext.kt @@ -19,83 +19,121 @@ import no.nav.paw.bekreftelse.api.config.APPLICATION_CONFIG_FILE_NAME import no.nav.paw.bekreftelse.api.config.ApplicationConfig import no.nav.paw.bekreftelse.api.config.AuthProvider import no.nav.paw.bekreftelse.api.config.AuthProviderClaims -import no.nav.paw.bekreftelse.api.config.AuthProviders import no.nav.paw.bekreftelse.api.config.SERVER_CONFIG_FILE_NAME import no.nav.paw.bekreftelse.api.config.ServerConfig -import no.nav.paw.bekreftelse.api.consumer.BekreftelseHttpConsumer import no.nav.paw.bekreftelse.api.context.ApplicationContext import no.nav.paw.bekreftelse.api.context.resolveRequest +import no.nav.paw.bekreftelse.api.handler.KafkaConsumerExceptionHandler import no.nav.paw.bekreftelse.api.model.Azure -import no.nav.paw.bekreftelse.api.model.IdPorten -import no.nav.paw.bekreftelse.api.model.InternState import no.nav.paw.bekreftelse.api.model.TilgjengeligeBekreftelserRequest import no.nav.paw.bekreftelse.api.model.TokenX +import no.nav.paw.bekreftelse.api.plugin.TestData +import no.nav.paw.bekreftelse.api.plugin.configTestDataPlugin import no.nav.paw.bekreftelse.api.plugins.configureAuthentication +import no.nav.paw.bekreftelse.api.plugins.configureDatabase import no.nav.paw.bekreftelse.api.plugins.configureHTTP -import no.nav.paw.bekreftelse.api.plugins.configureLogging import no.nav.paw.bekreftelse.api.plugins.configureSerialization import no.nav.paw.bekreftelse.api.producer.BekreftelseKafkaProducer +import no.nav.paw.bekreftelse.api.repository.BekreftelseRepository import no.nav.paw.bekreftelse.api.routes.bekreftelseRoutes import no.nav.paw.bekreftelse.api.services.AuthorizationService import no.nav.paw.bekreftelse.api.services.BekreftelseService import no.nav.paw.bekreftelse.api.utils.configureJackson +import no.nav.paw.bekreftelse.api.utils.createDataSource +import no.nav.paw.bekreftelse.internehendelser.BekreftelseHendelse +import no.nav.paw.bekreftelse.melding.v1.Bekreftelse import no.nav.paw.config.hoplite.loadNaisOrLocalConfiguration +import no.nav.paw.health.model.LivenessHealthIndicator +import no.nav.paw.health.model.ReadinessHealthIndicator import no.nav.paw.health.repository.HealthIndicatorRepository import no.nav.paw.kafkakeygenerator.client.KafkaKeysClient import no.nav.poao_tilgang.client.PoaoTilgangClient import no.nav.poao_tilgang.client.TilgangType import no.nav.security.mock.oauth2.MockOAuth2Server -import org.apache.kafka.streams.KafkaStreams -import org.apache.kafka.streams.state.ReadOnlyKeyValueStore +import org.apache.kafka.clients.consumer.KafkaConsumer +import org.apache.kafka.clients.producer.Producer +import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.containers.wait.strategy.Wait +import javax.sql.DataSource class ApplicationTestContext { val testData = TestDataGenerator() val serverConfig = loadNaisOrLocalConfiguration(SERVER_CONFIG_FILE_NAME) val applicationConfig = loadNaisOrLocalConfiguration(APPLICATION_CONFIG_FILE_NAME) + val dataSource = createTestDataSource() val prometheusMeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT) - val kafkaStreamsMock = mockk() - val stateStoreMock = mockk>() val kafkaKeysClientMock = mockk() val poaoTilgangClientMock = mockk() + val kafkaProducerMock = mockk>() val bekreftelseKafkaProducerMock = mockk() - val bekreftelseHttpConsumerMock = mockk() - val authorizationService = - AuthorizationService(serverConfig, applicationConfig, kafkaKeysClientMock, poaoTilgangClientMock) + val kafkaConsumerMock = mockk>() + val kafkaConsumerExceptionHandler = KafkaConsumerExceptionHandler( + LivenessHealthIndicator(), + ReadinessHealthIndicator() + ) + val authorizationService = AuthorizationService( + serverConfig, + applicationConfig, + kafkaKeysClientMock, + poaoTilgangClientMock + ) + val bekreftelseRepository = BekreftelseRepository() val bekreftelseServiceMock = mockk() - val bekreftelseServiceReal = BekreftelseService( + val bekreftelseService = BekreftelseService( serverConfig, applicationConfig, - bekreftelseHttpConsumerMock, - kafkaStreamsMock, - bekreftelseKafkaProducerMock + prometheusMeterRegistry, + bekreftelseKafkaProducerMock, + bekreftelseRepository ) val mockOAuth2Server = MockOAuth2Server() - fun ApplicationTestBuilder.configureTestApplication(bekreftelseService: BekreftelseService) { - val applicationContext = ApplicationContext( - serverConfig, - applicationConfig.copy(authProviders = mockOAuth2Server.createAuthProviders()), - kafkaKeysClientMock, - prometheusMeterRegistry, - HealthIndicatorRepository(), - kafkaStreamsMock, - authorizationService, - bekreftelseService - ) + fun createApplicationContext(bekreftelseService: BekreftelseService) = ApplicationContext( + serverConfig, + applicationConfig.copy(authProviders = mockOAuth2Server.createAuthProviders()), + dataSource, + kafkaKeysClientMock, + prometheusMeterRegistry, + HealthIndicatorRepository(), + kafkaProducerMock, + kafkaConsumerMock, + kafkaConsumerExceptionHandler, + authorizationService, + bekreftelseService + ) + + fun ApplicationTestBuilder.configureSimpleTestApplication(bekreftelseService: BekreftelseService) { + val applicationContext = createApplicationContext(bekreftelseService) application { configureHTTP(applicationContext) configureAuthentication(applicationContext) - configureLogging() configureSerialization() routing { - bekreftelseRoutes(applicationContext) testRoutes() } } } + fun ApplicationTestBuilder.configureCompleteTestApplication( + bekreftelseService: BekreftelseService, + testData: TestData = TestData() + ) { + val applicationContext = createApplicationContext(bekreftelseService) + + application { + configureHTTP(applicationContext) + configureAuthentication(applicationContext) + configureSerialization() + configureDatabase(applicationContext) + configTestDataPlugin(testData) + routing { + bekreftelseRoutes(applicationContext) + } + } + } + fun ApplicationTestBuilder.configureTestClient(): HttpClient { return createClient { install(ContentNegotiation) { @@ -108,7 +146,7 @@ class ApplicationTestContext { private fun Route.testRoutes() { route("/api/secured") { - authenticate("idporten", "tokenx", "azure") { + authenticate(TokenX.name, Azure.name) { get("/") { authorizationService.authorize(resolveRequest(), TilgangType.LESE) call.respond("WHATEVER") @@ -125,16 +163,9 @@ class ApplicationTestContext { } } - private fun MockOAuth2Server.createAuthProviders(): AuthProviders { + private fun MockOAuth2Server.createAuthProviders(): List { val wellKnownUrl = wellKnownUrl("default").toString() return listOf( - AuthProvider( - IdPorten.name, wellKnownUrl, "default", AuthProviderClaims( - listOf( - "acr=idporten-loa-high" - ) - ) - ), AuthProvider( TokenX.name, wellKnownUrl, "default", AuthProviderClaims( listOf( @@ -151,4 +182,26 @@ class ApplicationTestContext { ) ) } + + private fun createTestDataSource(): DataSource { + val postgres = postgresContainer() + val databaseConfig = postgres.let { + applicationConfig.database.copy( + jdbcUrl = "jdbc:postgresql://${it.host}:${it.firstMappedPort}/${it.databaseName}?user=${it.username}&password=${it.password}" + ) + } + return createDataSource(databaseConfig) + } + + private fun postgresContainer(): PostgreSQLContainer> { + val postgres = PostgreSQLContainer("postgres:16").apply { + addEnv("POSTGRES_PASSWORD", "bekreftelse_api") + addEnv("POSTGRES_USER", "5up3r_53cr3t_p455w0rd") + addEnv("POSTGRES_DB", "bekreftelser") + addExposedPorts(5432) + } + postgres.start() + postgres.waitingFor(Wait.forHealthcheck()) + return postgres + } } diff --git a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/KafkaTestDataProducer.kt b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/KafkaTestDataProducer.kt new file mode 100644 index 00000000..3668273e --- /dev/null +++ b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/KafkaTestDataProducer.kt @@ -0,0 +1,59 @@ +package no.nav.paw.bekreftelse.api + +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import no.nav.paw.bekreftelse.api.config.APPLICATION_CONFIG_FILE_NAME +import no.nav.paw.bekreftelse.api.config.ApplicationConfig +import no.nav.paw.bekreftelse.internehendelser.BekreftelseHendelse +import no.nav.paw.bekreftelse.internehendelser.BekreftelseHendelseSerializer +import no.nav.paw.config.hoplite.loadNaisOrLocalConfiguration +import no.nav.paw.config.kafka.KafkaFactory +import no.nav.paw.config.kafka.sendDeferred +import org.apache.kafka.clients.producer.Producer +import org.apache.kafka.clients.producer.ProducerRecord +import org.apache.kafka.common.serialization.LongSerializer +import java.time.Duration +import java.time.Instant +import java.util.* + +fun main() { + + val applicationConfig = loadNaisOrLocalConfiguration(APPLICATION_CONFIG_FILE_NAME) + val kafkaFactory = KafkaFactory(applicationConfig.kafkaClients) + val kafkaProducer = kafkaFactory.createProducer( + clientId = "bekreftelse-api-test-kafka-producer", + keySerializer = LongSerializer::class, + valueSerializer = BekreftelseHendelseSerializer::class + ) + + val arbeidssoekerId = 1L + val periodeId = UUID.fromString("3e415602-b7b6-47d4-bbd7-efdda468ca20") + val bekreftelseId = UUID.randomUUID() + + val testData = TestDataGenerator() + + val topic = applicationConfig.kafkaTopology.bekreftelseHendelsesloggTopic + val key = 1L + val value = testData.nyBekreftelseTilgjengelig( + hendelseId = UUID.randomUUID(), + periodeId = periodeId, + arbeidssoekerId = arbeidssoekerId, + bekreftelseId = bekreftelseId, + gjelderFra = Instant.now(), + gjelderTil = Instant.now().plus(Duration.ofDays(14)), + ) + + sendHendelse(kafkaProducer, topic, key, value) +} + +fun sendHendelse( + producer: Producer, + topic: String, + key: Long, + value: BekreftelseHendelse +) = + runBlocking { + launch { + producer.sendDeferred(ProducerRecord(topic, key, value)).await() + } + } \ No newline at end of file diff --git a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/TestDataGenerator.kt b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/TestDataGenerator.kt index c6148a27..8611911b 100644 --- a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/TestDataGenerator.kt +++ b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/TestDataGenerator.kt @@ -1,41 +1,100 @@ package no.nav.paw.bekreftelse.api -import no.nav.paw.bekreftelse.api.model.BekreftelseRequest +import io.mockk.mockk +import kotlinx.coroutines.Deferred +import no.nav.paw.bekreftelse.api.model.BekreftelseRow +import no.nav.paw.bekreftelse.api.model.MottaBekreftelseRequest import no.nav.paw.bekreftelse.api.model.TilgjengeligBekreftelse +import no.nav.paw.bekreftelse.internehendelser.BekreftelseMeldingMottatt import no.nav.paw.bekreftelse.internehendelser.BekreftelseTilgjengelig -import org.apache.kafka.streams.KeyQueryMetadata -import org.apache.kafka.streams.state.HostInfo +import org.apache.kafka.clients.producer.RecordMetadata +import java.time.Duration import java.time.Instant import java.util.* +import java.util.concurrent.Future class TestDataGenerator { val fnr1 = "01017012345" val fnr2 = "02017012345" - val arbeidsoekerId1 = 10001L - val arbeidsoekerId2 = 10002L + val fnr3 = "03017012345" + val arbeidssoekerId1 = 10001L + val arbeidssoekerId2 = 10002L + val arbeidssoekerId3 = 10003L val kafkaKey1 = -10001L val kafkaKey2 = -10002L + val kafkaKey3 = -10003L + val periodeId1 = UUID.fromString("4c0cb50a-3b4a-45df-b5b6-2cb45f04d19b") + val periodeId2 = UUID.fromString("0fc3de47-a6cd-4ad5-8433-53235738200d") + val bekreftelseId1 = UUID.fromString("0cd73e66-e5a2-4dae-88de-2dd89a910a19") + val bekreftelseId2 = UUID.fromString("7b769364-4d48-40f8-ac64-4489bb8080dd") + val bekreftelseId3 = UUID.fromString("b6e3b543-da44-4524-860f-9474bd6d505e") + val hendelseId1 = UUID.fromString("d69695e0-4249-4756-b0ef-02979ac66fea") + val hendelseId2 = UUID.fromString("9830a768-553c-4e11-b1f8-165b4e499be7") + + fun nyBekreftelseRow( + version: Int = 1, + partition: Int = 1, + offset: Long = 1, + recordKey: Long = kafkaKey1, + arbeidssoekerId: Long = arbeidssoekerId1, + periodeId: UUID = periodeId1, + bekreftelseId: UUID = bekreftelseId1, + data: BekreftelseTilgjengelig = nyBekreftelseTilgjengelig( + arbeidssoekerId = arbeidssoekerId, + periodeId = periodeId, + bekreftelseId = bekreftelseId, + ) + ) = BekreftelseRow( + version = version, + partition = partition, + offset = offset, + recordKey = recordKey, + arbeidssoekerId = arbeidssoekerId, + periodeId = periodeId, + bekreftelseId = bekreftelseId, + data = data + ) + + fun nyBekreftelseRows( + arbeidssoekerId: Long = arbeidssoekerId1, + periodeId: UUID = periodeId1, + bekreftelseRow: List> + ): List { + return bekreftelseRow.mapIndexed { index, value -> + nyBekreftelseRow( + offset = value.first, + arbeidssoekerId = arbeidssoekerId, + periodeId = periodeId, + bekreftelseId = value.second + ) + } + } fun nyTilgjengeligBekreftelse( - periodeId: UUID = UUID.randomUUID(), - bekreftelseId: UUID = UUID.randomUUID(), + periodeId: UUID = periodeId1, + bekreftelseId: UUID = bekreftelseId1, gjelderFra: Instant = Instant.now(), - gjelderTil: Instant = Instant.now() - ) = TilgjengeligBekreftelse(periodeId, bekreftelseId, gjelderFra, gjelderTil) + gjelderTil: Instant = Instant.now().plus(Duration.ofDays(14)), + ) = TilgjengeligBekreftelse( + periodeId = periodeId, + bekreftelseId = bekreftelseId, + gjelderFra = gjelderFra, + gjelderTil = gjelderTil + ) fun nyBekreftelseTilgjengelig( - hendelseId: UUID = UUID.randomUUID(), - periodeId: UUID = UUID.randomUUID(), - arbeidsoekerId: Long = Random().nextLong(), - bekreftelseId: UUID = UUID.randomUUID(), + hendelseId: UUID = hendelseId1, + periodeId: UUID = periodeId1, + arbeidssoekerId: Long = arbeidssoekerId1, + bekreftelseId: UUID = bekreftelseId1, gjelderFra: Instant = Instant.now(), - gjelderTil: Instant = Instant.now(), + gjelderTil: Instant = Instant.now().plus(Duration.ofDays(14)), hendelseTidspunkt: Instant = Instant.now() ) = BekreftelseTilgjengelig( hendelseId = hendelseId, periodeId = periodeId, - arbeidssoekerId = arbeidsoekerId, + arbeidssoekerId = arbeidssoekerId, hendelseTidspunkt = hendelseTidspunkt, bekreftelseId = bekreftelseId, gjelderFra = gjelderFra, @@ -44,12 +103,30 @@ class TestDataGenerator { fun nyBekreftelseRequest( identitetsnummer: String? = null, - bekreftelseId: UUID = UUID.randomUUID(), + bekreftelseId: UUID = bekreftelseId1, harJobbetIDennePerioden: Boolean = false, vilFortsetteSomArbeidssoeker: Boolean = true - ) = BekreftelseRequest(identitetsnummer, bekreftelseId, harJobbetIDennePerioden, vilFortsetteSomArbeidssoeker) + ) = MottaBekreftelseRequest( + identitetsnummer = identitetsnummer, + bekreftelseId = bekreftelseId, + harJobbetIDennePerioden = harJobbetIDennePerioden, + vilFortsetteSomArbeidssoeker = vilFortsetteSomArbeidssoeker + ) - fun nyKeyQueryMetadata( - host: String = "10.0.0.100" - ) = KeyQueryMetadata(HostInfo(host, 8080), emptySet(), 1) + fun nyProducerFuture() = mockk>() + fun nyProducerDeferred() = mockk>() + + fun nyBekreftelseMeldingMottatt( + hendelseId: UUID = hendelseId1, + periodeId: UUID = periodeId1, + arbeidssoekerId: Long = arbeidssoekerId1, + hendelseTidspunkt: Instant = Instant.now(), + bekreftelseId: UUID = bekreftelseId1 + ) = BekreftelseMeldingMottatt( + hendelseId = hendelseId, + periodeId = periodeId, + arbeidssoekerId = arbeidssoekerId, + hendelseTidspunkt = hendelseTidspunkt, + bekreftelseId = bekreftelseId, + ) } \ No newline at end of file diff --git a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/plugin/TestDataPlugin.kt b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/plugin/TestDataPlugin.kt new file mode 100644 index 00000000..c6c5cb4c --- /dev/null +++ b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/plugin/TestDataPlugin.kt @@ -0,0 +1,50 @@ +package no.nav.paw.bekreftelse.api.plugin + +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationPlugin +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.application.hooks.MonitoringEvent +import io.ktor.server.application.install +import io.ktor.server.application.log +import io.ktor.util.KtorDsl +import no.nav.paw.bekreftelse.api.model.BekreftelseRow +import no.nav.paw.bekreftelse.api.plugins.custom.FlywayMigrationCompleted +import no.nav.paw.bekreftelse.api.repository.BekreftelseRepository +import org.jetbrains.exposed.sql.transactions.transaction + +data class TestData( + val bereftelseRows: List = listOf() +) + +@KtorDsl +class TestDataPluginConfig { + var testData: TestData? = null + + companion object { + const val PLUGIN_NAME = "TestDataPlugin" + } +} + +val TestDataPlugin: ApplicationPlugin = + createApplicationPlugin(TestDataPluginConfig.PLUGIN_NAME, ::TestDataPluginConfig) { + application.log.info("Oppretter {}", TestDataPluginConfig.PLUGIN_NAME) + val testData = requireNotNull(pluginConfig.testData) { "TestData er null" } + val bekreftelseRepository = BekreftelseRepository() + + on(MonitoringEvent(FlywayMigrationCompleted)) { application -> + application.log.info("Oppretter testdata") + bekreftelseRepository.insertBekreftelseRows(testData.bereftelseRows) + } + } + +private fun BekreftelseRepository.insertBekreftelseRows(bereftelseRows: List) { + transaction { + bereftelseRows.forEach { insert(it) } + } +} + +fun Application.configTestDataPlugin(testData: TestData) { + install(TestDataPlugin) { + this.testData = testData + } +} \ No newline at end of file diff --git a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/routes/AuthRoutesTest.kt b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/routes/AuthRoutesTest.kt index a595ec45..7f4e9961 100644 --- a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/routes/AuthRoutesTest.kt +++ b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/routes/AuthRoutesTest.kt @@ -51,7 +51,7 @@ class AuthRoutesTest : FreeSpec({ "Skal få 401 ved manglende Bearer Token" { testApplication { - configureTestApplication(bekreftelseServiceMock) + configureSimpleTestApplication(bekreftelseServiceMock) val client = configureTestClient() val response = client.get("/api/secured") @@ -62,7 +62,7 @@ class AuthRoutesTest : FreeSpec({ "Skal få 401 ved token utstedt av ukjent issuer" { testApplication { - configureTestApplication(bekreftelseServiceMock) + configureSimpleTestApplication(bekreftelseServiceMock) val client = configureTestClient() val token = mockOAuth2Server.issueToken( @@ -83,7 +83,7 @@ class AuthRoutesTest : FreeSpec({ "Skal få 403 ved token uten noen claims" { testApplication { - configureTestApplication(bekreftelseServiceMock) + configureSimpleTestApplication(bekreftelseServiceMock) val client = configureTestClient() val token = mockOAuth2Server.issueToken() @@ -107,7 +107,7 @@ class AuthRoutesTest : FreeSpec({ "Skal få 403 ved TokenX-token uten pid claim" { testApplication { - configureTestApplication(bekreftelseServiceMock) + configureSimpleTestApplication(bekreftelseServiceMock) val client = configureTestClient() val token = mockOAuth2Server.issueToken( @@ -129,7 +129,7 @@ class AuthRoutesTest : FreeSpec({ "Skal få 403 ved TokenX-token når innsendt ident ikke er lik pid claim" { testApplication { - configureTestApplication(bekreftelseServiceMock) + configureSimpleTestApplication(bekreftelseServiceMock) val client = configureTestClient() val token = mockOAuth2Server.issueToken( @@ -158,7 +158,7 @@ class AuthRoutesTest : FreeSpec({ coEvery { kafkaKeysClientMock.getIdAndKey(any()) } returns KafkaKeysResponse(1, 1) testApplication { - configureTestApplication(bekreftelseServiceMock) + configureSimpleTestApplication(bekreftelseServiceMock) val client = configureTestClient() val token = mockOAuth2Server.issueToken( @@ -193,7 +193,7 @@ class AuthRoutesTest : FreeSpec({ "Skal få 403 ved Azure-token men GET-request" { testApplication { - configureTestApplication(bekreftelseServiceMock) + configureSimpleTestApplication(bekreftelseServiceMock) val client = configureTestClient() val token = mockOAuth2Server.issueToken( @@ -216,7 +216,7 @@ class AuthRoutesTest : FreeSpec({ "Skal få 403 ved Azure-token med POST-request uten ident" { testApplication { - configureTestApplication(bekreftelseServiceMock) + configureSimpleTestApplication(bekreftelseServiceMock) val client = configureTestClient() val token = mockOAuth2Server.issueToken( @@ -249,7 +249,7 @@ class AuthRoutesTest : FreeSpec({ ) testApplication { - configureTestApplication(bekreftelseServiceMock) + configureSimpleTestApplication(bekreftelseServiceMock) val client = configureTestClient() val token = mockOAuth2Server.issueToken( @@ -285,7 +285,7 @@ class AuthRoutesTest : FreeSpec({ ) testApplication { - configureTestApplication(bekreftelseServiceMock) + configureSimpleTestApplication(bekreftelseServiceMock) val client = configureTestClient() val token = mockOAuth2Server.issueToken( diff --git a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/routes/BekreftelseRoutesTest.kt b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/routes/BekreftelseRoutesTest.kt index 6771f750..046dde78 100644 --- a/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/routes/BekreftelseRoutesTest.kt +++ b/apps/bekreftelse-api/src/test/kotlin/no/nav/paw/bekreftelse/api/routes/BekreftelseRoutesTest.kt @@ -22,16 +22,10 @@ import io.mockk.just import io.mockk.runs import io.mockk.verify import no.nav.paw.bekreftelse.api.ApplicationTestContext -import no.nav.paw.bekreftelse.api.model.BekreftelseRequest -import no.nav.paw.bekreftelse.api.model.InternState import no.nav.paw.bekreftelse.api.model.TilgjengeligBekreftelserResponse -import no.nav.paw.bekreftelse.api.model.TilgjengeligeBekreftelserRequest +import no.nav.paw.bekreftelse.api.plugin.TestData import no.nav.paw.bekreftelse.melding.v1.Bekreftelse -import no.nav.paw.error.model.ProblemDetails import no.nav.paw.kafkakeygenerator.client.KafkaKeysResponse -import org.apache.kafka.common.serialization.Serializer -import org.apache.kafka.streams.KafkaStreams -import org.apache.kafka.streams.StoreQueryParameters class BekreftelseRoutesTest : FreeSpec({ with(ApplicationTestContext()) { @@ -43,12 +37,11 @@ class BekreftelseRoutesTest : FreeSpec({ afterSpec { coVerify { kafkaKeysClientMock.getIdAndKey(any()) } confirmVerified( - kafkaStreamsMock, - stateStoreMock, kafkaKeysClientMock, poaoTilgangClientMock, bekreftelseKafkaProducerMock, - bekreftelseHttpConsumerMock, + kafkaProducerMock, + kafkaConsumerMock, bekreftelseServiceMock ) mockOAuth2Server.shutdown() @@ -58,197 +51,30 @@ class BekreftelseRoutesTest : FreeSpec({ * SLUTTBRUKER TESTER */ "Test suite for sluttbruker" - { - "Skal få 500 om Kafka Streams ikke kjører" { + "Skal hente tilgjengelige bekreftelser" { coEvery { kafkaKeysClientMock.getIdAndKey(any()) } returns KafkaKeysResponse( - testData.arbeidsoekerId1, + testData.arbeidssoekerId1, testData.kafkaKey1 ) - every { kafkaStreamsMock.state() } returns KafkaStreams.State.NOT_RUNNING testApplication { - configureTestApplication(bekreftelseServiceReal) - val client = configureTestClient() - - val token = mockOAuth2Server.issueToken( - claims = mapOf( - "acr" to "idporten-loa-high", - "pid" to "01017012345" - ) - ) - - val response = client.get("/api/v1/tilgjengelige-bekreftelser") { - bearerAuth(token.serialize()) - } - - response.status shouldBe HttpStatusCode.InternalServerError - val body = response.body() - body.status shouldBe HttpStatusCode.InternalServerError - body.code shouldBe "PAW_SYSTEMFEIL" - verify { kafkaStreamsMock.state() } - } - } - - "Skal få 500 om ingen state store funnet" { - coEvery { kafkaKeysClientMock.getIdAndKey(any()) } returns KafkaKeysResponse( - testData.arbeidsoekerId1, - testData.kafkaKey1 - ) - every { kafkaStreamsMock.state() } returns KafkaStreams.State.RUNNING - every { kafkaStreamsMock.store(any>()) } returns null - - testApplication { - configureTestApplication(bekreftelseServiceReal) - val client = configureTestClient() - - val token = mockOAuth2Server.issueToken( - claims = mapOf( - "acr" to "idporten-loa-high", - "pid" to "01017012345" - ) - ) - - val response = client.get("/api/v1/tilgjengelige-bekreftelser") { - bearerAuth(token.serialize()) - } - - response.status shouldBe HttpStatusCode.InternalServerError - val body = response.body() - body.status shouldBe HttpStatusCode.InternalServerError - body.code shouldBe "PAW_SYSTEMFEIL" - verify { kafkaStreamsMock.state() } - verify { kafkaStreamsMock.store(any>()) } - } - } - - "Skal hente tilgjengelige bekreftelser fra intern state store" { - coEvery { kafkaKeysClientMock.getIdAndKey(any()) } returns KafkaKeysResponse( - testData.arbeidsoekerId1, - testData.kafkaKey1 - ) - every { kafkaStreamsMock.state() } returns KafkaStreams.State.RUNNING - every { kafkaStreamsMock.store(any>()) } returns stateStoreMock - every { stateStoreMock.get(any()) } returns InternState( - listOf( - testData.nyBekreftelseTilgjengelig() - ) - ) - - testApplication { - configureTestApplication(bekreftelseServiceReal) - val client = configureTestClient() - - val token = mockOAuth2Server.issueToken( - claims = mapOf( - "acr" to "idporten-loa-high", - "pid" to "01017012345" - ) - ) - - val response = client.get("/api/v1/tilgjengelige-bekreftelser") { - bearerAuth(token.serialize()) - } - - response.status shouldBe HttpStatusCode.OK - val body = response.body() - body.size shouldBe 1 - verify { kafkaStreamsMock.state() } - verify { kafkaStreamsMock.store(any>()) } - verify { stateStoreMock.get(any()) } - } - } - - "Skal hente tilgjengelige bekreftelser fra annen node" { - coEvery { kafkaKeysClientMock.getIdAndKey(any()) } returns KafkaKeysResponse( - testData.arbeidsoekerId1, - testData.kafkaKey1 - ) - every { kafkaStreamsMock.state() } returns KafkaStreams.State.RUNNING - every { kafkaStreamsMock.store(any>()) } returns stateStoreMock - every { stateStoreMock.get(any()) } returns null - every { - kafkaStreamsMock.queryMetadataForKey( - any(), - any(), - any>() - ) - } returns testData.nyKeyQueryMetadata() - coEvery { - bekreftelseHttpConsumerMock.finnTilgjengeligBekreftelser( - any(), - any(), - any() - ) - } returns listOf(testData.nyTilgjengeligBekreftelse()) - - testApplication { - configureTestApplication(bekreftelseServiceReal) - val client = configureTestClient() - - val token = mockOAuth2Server.issueToken( - claims = mapOf( - "acr" to "idporten-loa-high", - "pid" to "01017012345" + configureCompleteTestApplication( + bekreftelseService, TestData( + bereftelseRows = testData.nyBekreftelseRows( + arbeidssoekerId = testData.arbeidssoekerId1, + periodeId = testData.periodeId1, + bekreftelseRow = listOf( + 1L to testData.bekreftelseId1, 2L to testData.bekreftelseId2 + ) + ) ) ) - - val response = client.get("/api/v1/tilgjengelige-bekreftelser") { - bearerAuth(token.serialize()) - } - - response.status shouldBe HttpStatusCode.OK - val body = response.body() - body.size shouldBe 1 - verify { kafkaStreamsMock.state() } - verify { kafkaStreamsMock.store(any>()) } - verify { - kafkaStreamsMock.queryMetadataForKey( - any(), - any(), - any>() - ) - } - verify { stateStoreMock.get(any()) } - coVerify { - bekreftelseHttpConsumerMock.finnTilgjengeligBekreftelser( - any(), - any(), - any() - ) - } - } - } - - "Skal hente tilgjengelige bekreftelser men finner ingen" { - coEvery { kafkaKeysClientMock.getIdAndKey(any()) } returns KafkaKeysResponse( - testData.arbeidsoekerId1, - testData.kafkaKey1 - ) - every { kafkaStreamsMock.state() } returns KafkaStreams.State.RUNNING - every { kafkaStreamsMock.store(any>()) } returns stateStoreMock - every { stateStoreMock.get(any()) } returns null - every { - kafkaStreamsMock.queryMetadataForKey( - any(), - any(), - any>() - ) - } returns testData.nyKeyQueryMetadata(host = "127.0.0.1") - coEvery { - bekreftelseHttpConsumerMock.finnTilgjengeligBekreftelser( - any(), - any(), - any() - ) - } returns listOf(testData.nyTilgjengeligBekreftelse()) - - testApplication { - configureTestApplication(bekreftelseServiceReal) val client = configureTestClient() val token = mockOAuth2Server.issueToken( claims = mapOf( "acr" to "idporten-loa-high", - "pid" to "01017012345" + "pid" to testData.fnr1 ) ) @@ -258,108 +84,33 @@ class BekreftelseRoutesTest : FreeSpec({ response.status shouldBe HttpStatusCode.OK val body = response.body() - body.size shouldBe 0 - verify { kafkaStreamsMock.state() } - verify { kafkaStreamsMock.store(any>()) } - verify { - kafkaStreamsMock.queryMetadataForKey( - any(), - any(), - any>() - ) - } - verify { stateStoreMock.get(any()) } - coVerify { - bekreftelseHttpConsumerMock.finnTilgjengeligBekreftelser( - any(), - any(), - any() - ) - } + body.size shouldBe 2 } } - "Skal motta bekreftelse via intern state store" { + "Skal motta bekreftelse" { coEvery { kafkaKeysClientMock.getIdAndKey(any()) } returns KafkaKeysResponse( - testData.arbeidsoekerId1, - testData.kafkaKey1 + testData.arbeidssoekerId2, + testData.kafkaKey2 ) - val request = testData.nyBekreftelseRequest(identitetsnummer = testData.fnr1) - every { kafkaStreamsMock.state() } returns KafkaStreams.State.RUNNING - every { kafkaStreamsMock.store(any>()) } returns stateStoreMock - every { stateStoreMock.get(any()) } returns InternState( - listOf( - testData.nyBekreftelseTilgjengelig( - bekreftelseId = request.bekreftelseId, - arbeidsoekerId = testData.arbeidsoekerId1 - ) - ) + every { bekreftelseKafkaProducerMock.produceMessage(any(), any()) } just runs + val request = testData.nyBekreftelseRequest( + identitetsnummer = testData.fnr2, + bekreftelseId = testData.bekreftelseId3 ) - coEvery { - bekreftelseKafkaProducerMock.produceMessage( - any(), - any() - ) - } just runs testApplication { - configureTestApplication(bekreftelseServiceReal) - val client = configureTestClient() - - val token = mockOAuth2Server.issueToken( - claims = mapOf( - "acr" to "idporten-loa-high", - "pid" to testData.fnr1 + configureCompleteTestApplication( + bekreftelseService, TestData( + bereftelseRows = testData.nyBekreftelseRows( + arbeidssoekerId = testData.arbeidssoekerId2, + periodeId = testData.periodeId2, + bekreftelseRow = listOf( + 3L to testData.bekreftelseId3 + ) + ) ) ) - - val response = client.post("/api/v1/bekreftelse") { - bearerAuth(token.serialize()) - headers { - append(HttpHeaders.ContentType, ContentType.Application.Json) - } - setBody(request) - } - - response.status shouldBe HttpStatusCode.OK - verify { kafkaStreamsMock.state() } - verify { kafkaStreamsMock.store(any>()) } - verify { stateStoreMock.get(any()) } - coVerify { - bekreftelseKafkaProducerMock.produceMessage( - any(), - any() - ) - } - } - } - - "Skal motta bekreftelse og overføre den til annen node" { - coEvery { kafkaKeysClientMock.getIdAndKey(any()) } returns KafkaKeysResponse( - testData.arbeidsoekerId2, - testData.kafkaKey2 - ) - val request = testData.nyBekreftelseRequest(identitetsnummer = testData.fnr2) - every { kafkaStreamsMock.state() } returns KafkaStreams.State.RUNNING - every { kafkaStreamsMock.store(any>()) } returns stateStoreMock - every { stateStoreMock.get(any()) } returns null - every { - kafkaStreamsMock.queryMetadataForKey( - any(), - any(), - any>() - ) - } returns testData.nyKeyQueryMetadata() - coEvery { - bekreftelseHttpConsumerMock.sendBekreftelse( - any(), - any(), - any() - ) - } just runs - - testApplication { - configureTestApplication(bekreftelseServiceReal) val client = configureTestClient() val token = mockOAuth2Server.issueToken( @@ -378,58 +129,25 @@ class BekreftelseRoutesTest : FreeSpec({ } response.status shouldBe HttpStatusCode.OK - verify { kafkaStreamsMock.state() } - verify { kafkaStreamsMock.store(any>()) } - verify { - kafkaStreamsMock.queryMetadataForKey( - any(), - any(), - any>() - ) - } - verify { stateStoreMock.get(any()) } - coVerify { - bekreftelseHttpConsumerMock.sendBekreftelse( - any(), - any(), - any() - ) - } + verify { bekreftelseKafkaProducerMock.produceMessage(any(), any()) } } } "Skal motta bekreftelse men finner ikke relatert tilgjengelig bekreftelse" { coEvery { kafkaKeysClientMock.getIdAndKey(any()) } returns KafkaKeysResponse( - testData.arbeidsoekerId2, - testData.kafkaKey2 + testData.arbeidssoekerId3, + testData.kafkaKey3 ) - val request = testData.nyBekreftelseRequest(identitetsnummer = testData.fnr2) - every { kafkaStreamsMock.state() } returns KafkaStreams.State.RUNNING - every { kafkaStreamsMock.store(any>()) } returns stateStoreMock - every { stateStoreMock.get(any()) } returns null - every { - kafkaStreamsMock.queryMetadataForKey( - any(), - any(), - any>() - ) - } returns testData.nyKeyQueryMetadata(host = "127.0.0.1") - coEvery { - bekreftelseHttpConsumerMock.sendBekreftelse( - any(), - any(), - any() - ) - } just runs + val request = testData.nyBekreftelseRequest(identitetsnummer = testData.fnr3) testApplication { - configureTestApplication(bekreftelseServiceReal) + configureCompleteTestApplication(bekreftelseService) val client = configureTestClient() val token = mockOAuth2Server.issueToken( claims = mapOf( "acr" to "idporten-loa-high", - "pid" to testData.fnr2 + "pid" to testData.fnr3 ) ) @@ -442,25 +160,6 @@ class BekreftelseRoutesTest : FreeSpec({ } response.status shouldBe HttpStatusCode.BadRequest - val body = response.body() - body.code shouldBe "PAW_DATA_IKKE_FUNNET_FOR_ID" - verify { kafkaStreamsMock.state() } - verify { kafkaStreamsMock.store(any>()) } - verify { - kafkaStreamsMock.queryMetadataForKey( - any(), - any(), - any>() - ) - } - verify { stateStoreMock.get(any()) } - coVerify { - bekreftelseHttpConsumerMock.sendBekreftelse( - any(), - any(), - any() - ) - } } } } diff --git a/apps/bekreftelse-api/src/test/resources/rapporteringTilgjengelig.json b/apps/bekreftelse-api/src/test/resources/rapporteringTilgjengelig.json deleted file mode 100644 index 5d2917de..00000000 --- a/apps/bekreftelse-api/src/test/resources/rapporteringTilgjengelig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "hendelseId": "cfc94678-b0ea-462f-8c40-1a6e22fc76fb", - "periodeId": "fe3080b3-063d-4d8c-8cad-bba10d943538", - "identitetsnummer": 12345678910, - "arbeidssoekerId": 2072234860133, - "rapporteringsId": "0415bed8-92e8-4474-9ded-56b9a477bdba", - "gjelderFra": "2024-05-27T10:40:05.056592103Z", - "gjelderTil": "2024-06-07T10:40:05.056592103Z", - "hendelseType": "rapportering.tilgjengelig" -} diff --git a/apps/bekreftelse-min-side-oppgaver/build.gradle.kts b/apps/bekreftelse-min-side-oppgaver/build.gradle.kts index 74f6ccb2..cc1f5ad4 100644 --- a/apps/bekreftelse-min-side-oppgaver/build.gradle.kts +++ b/apps/bekreftelse-min-side-oppgaver/build.gradle.kts @@ -23,7 +23,7 @@ dependencies { implementation(libs.nav.common.log) implementation(libs.logbackClassic) implementation(libs.logstashLogbackEncoder) - implementation(libs.paw.kafkaClients) + implementation(libs.kafka.clients) implementation(libs.kafka.streams.core) implementation(libs.avro.core) implementation(libs.avro.kafkaStreamsSerde) diff --git a/apps/bekreftelse-utgang/src/main/kotlin/no/nav/paw/bekreftelseutgang/tilstand/InternTilstandSerde.kt b/apps/bekreftelse-utgang/src/main/kotlin/no/nav/paw/bekreftelseutgang/tilstand/InternTilstandSerde.kt index 74b190b6..aebdf5f2 100644 --- a/apps/bekreftelse-utgang/src/main/kotlin/no/nav/paw/bekreftelseutgang/tilstand/InternTilstandSerde.kt +++ b/apps/bekreftelse-utgang/src/main/kotlin/no/nav/paw/bekreftelseutgang/tilstand/InternTilstandSerde.kt @@ -37,13 +37,17 @@ object InternTilstandDeserializer : Deserializer { private val internTilstandObjectMapper = ObjectMapper() .registerKotlinModule() - .registerModules(SimpleModule().addDeserializer(BekreftelseHendelse::class.java, - BekreftelseHendelseJsonDeserializer - )) + .registerModules( + SimpleModule().addDeserializer( + BekreftelseHendelse::class.java, + BekreftelseHendelseJsonDeserializer + ) + ) .registerModules(JavaTimeModule()) object BekreftelseHendelseJsonDeserializer : JsonDeserializer() { + private val deserializer = BekreftelseHendelseDeserializer() override fun deserialize(parser: JsonParser, context: DeserializationContext): BekreftelseHendelse = - BekreftelseHendelseDeserializer.deserializeNode(context.readTree(parser)) + deserializer.deserializeNode(context.readTree(parser)) } diff --git a/apps/hendelselogg-backup/build.gradle.kts b/apps/hendelselogg-backup/build.gradle.kts index ff94cd48..5c07e06a 100644 --- a/apps/hendelselogg-backup/build.gradle.kts +++ b/apps/hendelselogg-backup/build.gradle.kts @@ -46,7 +46,7 @@ dependencies { implementation(libs.nav.common.log) implementation(libs.logbackClassic) implementation(libs.logstashLogbackEncoder) - implementation(libs.paw.kafkaClients) + implementation(libs.kafka.clients) implementation(libs.exposed.core) implementation(libs.exposed.jdbc) implementation(libs.exposed.javaTime) diff --git a/apps/hendelseprosessor/build.gradle.kts b/apps/hendelseprosessor/build.gradle.kts index 0b8c47dd..8cafeb22 100644 --- a/apps/hendelseprosessor/build.gradle.kts +++ b/apps/hendelseprosessor/build.gradle.kts @@ -20,7 +20,7 @@ dependencies { implementation(libs.jackson.datatypeJsr310) implementation(libs.jackson.kotlin) implementation(libs.nav.common.log) - implementation(libs.paw.kafkaClients) + implementation(libs.kafka.clients) implementation(libs.kafka.streams.core) implementation(libs.avro.core) implementation(libs.avro.kafkaStreamsSerde) diff --git a/docker/postgres/config/initdb/postgres-init.sql b/docker/postgres/config/initdb/postgres-init.sql new file mode 100644 index 00000000..96561f0d --- /dev/null +++ b/docker/postgres/config/initdb/postgres-init.sql @@ -0,0 +1,3 @@ +create user bekreftelse_api with password '5up3r_53cr3t_p455w0rd'; + +create database bekreftelser with owner bekreftelse_api; \ No newline at end of file diff --git a/docker/postgres/docker-compose.yaml b/docker/postgres/docker-compose.yaml new file mode 100644 index 00000000..74f5da0a --- /dev/null +++ b/docker/postgres/docker-compose.yaml @@ -0,0 +1,25 @@ +### SERVICES ### +services: + postgres: + image: postgres:16 + container_name: postgres + ports: + - "5432:5432" + environment: + - POSTGRES_USER=admin + - POSTGRES_PASSWORD=admin + volumes: + - ./config/initdb/:/docker-entrypoint-initdb.d/ + - postgres:/var/lib/postgresql + networks: + - postgres + +### VOLUMES ### +volumes: + postgres: + name: postgres + +### NETWORKS ### +networks: + postgres: + name: postgres diff --git a/domain/bekreftelse-interne-hendelser/build.gradle.kts b/domain/bekreftelse-interne-hendelser/build.gradle.kts index edbd9f0d..2241ed55 100644 --- a/domain/bekreftelse-interne-hendelser/build.gradle.kts +++ b/domain/bekreftelse-interne-hendelser/build.gradle.kts @@ -7,11 +7,11 @@ dependencies { implementation(libs.jackson.datatypeJsr310) implementation(libs.jackson.kotlin) implementation(libs.jackson.core) - compileOnly(libs.paw.kafkaClients) + compileOnly(libs.kafka.clients) testImplementation(libs.test.junit5.runner) testImplementation(libs.test.kotest.assertionsCore) - testImplementation(libs.paw.kafkaClients) + testImplementation(libs.kafka.clients) } val jvmMajorVersion: String by project diff --git a/domain/bekreftelse-interne-hendelser/src/main/kotlin/no/nav/paw/bekreftelse/internehendelser/BekreftelseHendelseSerde.kt b/domain/bekreftelse-interne-hendelser/src/main/kotlin/no/nav/paw/bekreftelse/internehendelser/BekreftelseHendelseSerde.kt index 665d0ceb..8163b41d 100644 --- a/domain/bekreftelse-interne-hendelser/src/main/kotlin/no/nav/paw/bekreftelse/internehendelser/BekreftelseHendelseSerde.kt +++ b/domain/bekreftelse-interne-hendelser/src/main/kotlin/no/nav/paw/bekreftelse/internehendelser/BekreftelseHendelseSerde.kt @@ -15,21 +15,21 @@ private val objectMapper: ObjectMapper = ObjectMapper() class BekreftelseHendelseSerde: Serde { override fun serializer(): Serializer { - return BekreftelseHendelseSerializer + return BekreftelseHendelseSerializer() } override fun deserializer(): Deserializer { - return BekreftelseHendelseDeserializer + return BekreftelseHendelseDeserializer() } } -object BekreftelseHendelseSerializer: Serializer { +class BekreftelseHendelseSerializer: Serializer { override fun serialize(topic: String?, data: BekreftelseHendelse?): ByteArray? { return data?.let { objectMapper.writeValueAsBytes(it) } } } -object BekreftelseHendelseDeserializer: Deserializer { +class BekreftelseHendelseDeserializer: Deserializer { override fun deserialize(topic: String?, data: ByteArray?): BekreftelseHendelse { val node = objectMapper.readTree(data) return deserializeNode(node) diff --git a/domain/bekreftelse-interne-hendelser/src/test/kotlin/no/nav/paw/bekreftelse/internehendelser/SerdeTest.kt b/domain/bekreftelse-interne-hendelser/src/test/kotlin/no/nav/paw/bekreftelse/internehendelser/SerdeTest.kt index 074b80cc..42a3b73c 100644 --- a/domain/bekreftelse-interne-hendelser/src/test/kotlin/no/nav/paw/bekreftelse/internehendelser/SerdeTest.kt +++ b/domain/bekreftelse-interne-hendelser/src/test/kotlin/no/nav/paw/bekreftelse/internehendelser/SerdeTest.kt @@ -16,10 +16,10 @@ class SerdeTest : FreeSpec({ hendelseTidspunkt = Instant.now(), leveringsfrist = Instant.now() ) - val resultat = BekreftelseHendelseSerializer.serialize("", hendelse) + val resultat = BekreftelseHendelseSerializer().serialize("", hendelse) .let { serialized -> println("serialized: ${String(serialized!!)}") - BekreftelseHendelseDeserializer.deserialize("", serialized) + BekreftelseHendelseDeserializer().deserialize("", serialized) } resultat shouldBe hendelse } diff --git a/domain/interne-hendelser/build.gradle.kts b/domain/interne-hendelser/build.gradle.kts index 06b74c6b..96ccaff6 100644 --- a/domain/interne-hendelser/build.gradle.kts +++ b/domain/interne-hendelser/build.gradle.kts @@ -7,11 +7,11 @@ dependencies { compileOnly(libs.jackson.datatypeJsr310) compileOnly(libs.jackson.kotlin) compileOnly(libs.jackson.core) - compileOnly(libs.paw.kafkaClients) + compileOnly(libs.kafka.clients) testImplementation(libs.test.junit5.runner) testImplementation(libs.test.kotest.assertionsCore) - testImplementation(libs.paw.kafkaClients) + testImplementation(libs.kafka.clients) testImplementation(libs.jackson.datatypeJsr310) testImplementation(libs.jackson.kotlin) testImplementation(libs.jackson.core) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b56275ab..1f56b6c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,7 @@ micrometer-registryPrometheus = { group = "io.micrometer", name = "micrometer-re paw-pdl-client = { group = "no.nav.paw", name = "pdl-client", version.ref = "pawPdlClientVersion" } paw-aareg-client = { group = "no.nav.paw", name = "aareg-client", version.ref = "pawAaregClientVersion" } paw-schema-main = { group = "no.nav.paw.arbeidssokerregisteret.api", name = "main-avro-schema", version.ref = "arbeidssokerregisteretVersion" } -paw-kafkaClients = { group = "org.apache.kafka", name = "kafka-clients", version.ref = "orgApacheKafkaVersion" } +kafka-clients = { group = "org.apache.kafka", name = "kafka-clients", version.ref = "orgApacheKafkaVersion" } kafka-streams-core = { group = "org.apache.kafka", name = "kafka-streams", version.ref = "orgApacheKafkaVersion" } kafka-streams-test = { group = "org.apache.kafka", name = "kafka-streams-test-utils", version.ref = "orgApacheKafkaVersion" } avro-core = { group = "org.apache.avro", name = "avro", version.ref = "orgApacheAvroVersion" } @@ -84,6 +84,7 @@ hoplite-core = { group = "com.sksamuel.hoplite", name = "hoplite-core", version. hoplite-toml = { group = "com.sksamuel.hoplite", name = "hoplite-toml", version.ref = "comSksamuelHopliteVersion" } hoplite-yaml = { group = "com.sksamuel.hoplite", name = "hoplite-yaml", version.ref = "comSksamuelHopliteVersion" } exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "kotlinExposedVersion" } +exposed-json = { group = "org.jetbrains.exposed", name = "exposed-json", version.ref = "kotlinExposedVersion" } exposed-crypt = { group = "org.jetbrains.exposed", name = "exposed-crypt", version.ref = "kotlinExposedVersion" } exposed-dao = { group = "org.jetbrains.exposed", name = "exposed-dao", version.ref = "kotlinExposedVersion" } exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "kotlinExposedVersion" } diff --git a/lib/error-handling/build.gradle.kts b/lib/error-handling/build.gradle.kts index bc31be2d..35e43040 100644 --- a/lib/error-handling/build.gradle.kts +++ b/lib/error-handling/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { testImplementation(libs.ktor.client.contentNegotiation) testImplementation(libs.ktor.server.core) testImplementation(libs.kafka.streams.core) + testImplementation(libs.jackson.datatypeJsr310) testImplementation(libs.logbackClassic) } diff --git a/lib/error-handling/src/main/kotlin/no/nav/paw/error/handler/HttpExceptionHandler.kt b/lib/error-handling/src/main/kotlin/no/nav/paw/error/handler/HttpExceptionHandler.kt index 7e761eb0..37b59f94 100644 --- a/lib/error-handling/src/main/kotlin/no/nav/paw/error/handler/HttpExceptionHandler.kt +++ b/lib/error-handling/src/main/kotlin/no/nav/paw/error/handler/HttpExceptionHandler.kt @@ -18,16 +18,32 @@ import org.slf4j.LoggerFactory import org.slf4j.MDC private val logger: Logger = LoggerFactory.getLogger("no.nav.paw.logger.error.http") +private const val MDC_ERROR_ID_KEY = "x_error_id" +private const val MDC_ERROR_CODE_KEY = "x_error_code" -suspend fun ApplicationCall.handleException(throwable: Throwable) { - val problemDetails = resolveProblemDetails(request, throwable) - MDC.put("x_error_id", problemDetails.id.toString()) - MDC.put("x_error_code", problemDetails.code) +suspend fun ApplicationCall.handleException( + throwable: Throwable, + resolver: (throwable: Throwable) -> ProblemDetails? = { null } +) { + val problemDetails = resolveProblemDetails(request, throwable, resolver) + MDC.put(MDC_ERROR_ID_KEY, problemDetails.id.toString()) + MDC.put(MDC_ERROR_CODE_KEY, problemDetails.code) logger.error(problemDetails.detail, throwable) + MDC.remove(MDC_ERROR_ID_KEY) + MDC.remove(MDC_ERROR_CODE_KEY) respond(problemDetails.status, problemDetails) } -fun resolveProblemDetails(request: ApplicationRequest, throwable: Throwable): ProblemDetails { +fun resolveProblemDetails( + request: ApplicationRequest, + throwable: Throwable, + resolver: (throwable: Throwable) -> ProblemDetails? = { null } +): ProblemDetails { + val problemDetails = resolver(throwable) + if (problemDetails != null) { + return problemDetails + } + when (throwable) { is BadRequestException -> { return build400Error( diff --git a/lib/error-handling/src/main/kotlin/no/nav/paw/error/model/ProblemDetails.kt b/lib/error-handling/src/main/kotlin/no/nav/paw/error/model/ProblemDetails.kt index 1df6674a..41b97977 100644 --- a/lib/error-handling/src/main/kotlin/no/nav/paw/error/model/ProblemDetails.kt +++ b/lib/error-handling/src/main/kotlin/no/nav/paw/error/model/ProblemDetails.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.ktor.http.HttpStatusCode import no.nav.paw.error.serialize.HttpStatusCodeDeserializer import no.nav.paw.error.serialize.HttpStatusCodeSerializer +import java.time.Instant import java.util.* /** @@ -18,7 +19,8 @@ data class ProblemDetails( @JsonSerialize(using = HttpStatusCodeSerializer::class) @JsonDeserialize(using = HttpStatusCodeDeserializer::class) val status: HttpStatusCode, val detail: String, val instance: String, - val type: String = "about:blank" + val type: String = "about:blank", + val timestamp: Instant = Instant.now() ) fun build400Error(code: String, detail: String, instance: String, type: String = "about:blank") = diff --git a/lib/error-handling/src/test/kotlin/no/nav/paw/error/handler/HttpExceptionHandlerTest.kt b/lib/error-handling/src/test/kotlin/no/nav/paw/error/handler/HttpExceptionHandlerTest.kt index 76b69ec3..b46d97e2 100644 --- a/lib/error-handling/src/test/kotlin/no/nav/paw/error/handler/HttpExceptionHandlerTest.kt +++ b/lib/error-handling/src/test/kotlin/no/nav/paw/error/handler/HttpExceptionHandlerTest.kt @@ -1,5 +1,7 @@ package no.nav.paw.error.handler +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.kotlinModule import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.shouldBe import io.ktor.client.call.body @@ -29,7 +31,10 @@ class HttpExceptionHandlerTest : FreeSpec({ } } serverInstall(ServerContentNegotiation) { - jackson {} + jackson { + registerModule(JavaTimeModule()) + kotlinModule() + } } routing { get("/api/400") { @@ -40,7 +45,10 @@ class HttpExceptionHandlerTest : FreeSpec({ val client = createClient { install(ClientContentNegotiation) { - jackson {} + jackson { + registerModule(JavaTimeModule()) + kotlinModule() + } } } diff --git a/lib/kafka-streams/build.gradle.kts b/lib/kafka-streams/build.gradle.kts index b635d95a..d40c317a 100644 --- a/lib/kafka-streams/build.gradle.kts +++ b/lib/kafka-streams/build.gradle.kts @@ -4,7 +4,7 @@ plugins { dependencies { api(project(":lib:kafka")) - implementation(libs.paw.kafkaClients) + implementation(libs.kafka.clients) implementation(libs.kafka.streams.core) implementation(libs.avro.kafkaStreamsSerde) diff --git a/lib/kafka/build.gradle.kts b/lib/kafka/build.gradle.kts index 4bd4280f..ebb1a36b 100644 --- a/lib/kafka/build.gradle.kts +++ b/lib/kafka/build.gradle.kts @@ -6,7 +6,7 @@ plugins { dependencies { compileOnly(libs.coroutinesCore) compileOnly(libs.avro.kafkaSerializer) - implementation(libs.paw.kafkaClients) + implementation(libs.kafka.clients) // Test testImplementation(libs.bundles.testLibsWithUnitTesting)