From a01c6e5ecada21f3c6d3bfa2a3c64ca1763d304e Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Mon, 13 Jan 2025 15:29:58 +0100 Subject: [PATCH 01/27] =?UTF-8?q?KAlenderavtale=20st=C3=B8tter=20AVHOLDT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kalenderavtale kan settes som avholdt, ny slutttilstand. Når starttidspunkt passerer så vil SkedulertUtgått trigge at kalenderavtalen blir markert som AVHOLDT, dersom den er i en åpen tilstand. SkedulertUtgått tjenesten skrives ut av EphemeralDatabase til fordel for postgreSQL. --- app/nais/dev-gcp-skedulert-utgaatt.yaml | 11 +- app/nais/prod-gcp-skedulert-utgaatt.yaml | 9 + .../notifikasjon/bruker/BrukerAPI.kt | 4 +- .../notifikasjon/bruker/BrukerModel.kt | 1 + .../notifikasjon/bruker/BrukerRepository.kt | 1 + .../notifikasjon/hendelse/Hendelse.kt | 1 + .../notifikasjon/infrastruktur/Database.kt | 44 +- .../local_database/EphemeralDatabase.kt | 1 + .../notifikasjon/produsent/ProdusentModel.kt | 2 + .../produsent/api/MutationKalenderavtale.kt | 59 +- .../SkedulertUtg\303\245tt.kt" | 45 +- .../SkedulertUtg\303\245ttRepository.kt" | 557 ++++++++++++------ .../SkedulertUtg\303\245ttService.kt" | 128 +--- .../statistikk/StatistikkModel.kt | 1 - app/src/main/resources/bruker.graphql | 1 + .../skedulert_utgatt_model/V1__init.sql | 70 +++ app/src/main/resources/produsent.graphql | 4 + .../QueryKommendeKalenderavtalerTests.kt | 2 +- .../bruker/QuerySakerTidslinjeTest.kt | 6 +- .../bruker/SakerMedOppgaveTilstandTests.kt | 3 +- .../produsent/api/NyStatusSakTests.kt | 16 +- .../FristUtsattTests.kt" | 198 ++++--- .../KalenderavtaleAvholdtTests.kt" | 245 ++++++++ .../OppgaveUtg\303\245ttTests.kt" | 61 +- .../SkedulertUtg\303\245ttIdempotensTests.kt" | 13 +- .../notifikasjon/util/EksempelHendelser.kt | 3 +- local-db-init.sql | 3 + .../NotifikasjonListeElement.tsx | 20 + 28 files changed, 1042 insertions(+), 467 deletions(-) create mode 100644 app/src/main/resources/db/migration/skedulert_utgatt_model/V1__init.sql create mode 100644 "app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/KalenderavtaleAvholdtTests.kt" rename "app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttServiceTests.kt" => "app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/OppgaveUtg\303\245ttTests.kt" (71%) diff --git a/app/nais/dev-gcp-skedulert-utgaatt.yaml b/app/nais/dev-gcp-skedulert-utgaatt.yaml index 130ce9470..a1e4e6dcc 100644 --- a/app/nais/dev-gcp-skedulert-utgaatt.yaml +++ b/app/nais/dev-gcp-skedulert-utgaatt.yaml @@ -24,4 +24,13 @@ spec: enabled: true path: /internal/metrics kafka: - pool: nav-dev \ No newline at end of file + pool: nav-dev + gcp: + sqlInstances: + - type: POSTGRES_17 + tier: db-f1-micro + diskAutoresize: true + highAvailability: false + databases: + - name: skedulert-utgatt-model + envVarPrefix: DB \ No newline at end of file diff --git a/app/nais/prod-gcp-skedulert-utgaatt.yaml b/app/nais/prod-gcp-skedulert-utgaatt.yaml index 292d34b7d..bfe9737fb 100644 --- a/app/nais/prod-gcp-skedulert-utgaatt.yaml +++ b/app/nais/prod-gcp-skedulert-utgaatt.yaml @@ -26,3 +26,12 @@ spec: path: /internal/metrics kafka: pool: nav-prod + gcp: + sqlInstances: + - type: POSTGRES_17 + tier: db-custom-1-3840 + diskAutoresize: true + highAvailability: false + databases: + - name: skedulert-utgatt-model + envVarPrefix: DB \ No newline at end of file diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerAPI.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerAPI.kt index 8e03810c0..95341a4ba 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerAPI.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerAPI.kt @@ -108,7 +108,8 @@ object BrukerAPI { ARBEIDSGIVER_VIL_AVLYSE, ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED, ARBEIDSGIVER_HAR_GODTATT, - AVLYST; + AVLYST, + AVHOLDT; companion object { fun BrukerModel.Kalenderavtale.Tilstand.tilBrukerAPI(): Tilstand = when (this) { @@ -117,6 +118,7 @@ object BrukerAPI { BrukerModel.Kalenderavtale.Tilstand.ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED -> ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED BrukerModel.Kalenderavtale.Tilstand.ARBEIDSGIVER_HAR_GODTATT -> ARBEIDSGIVER_HAR_GODTATT BrukerModel.Kalenderavtale.Tilstand.AVLYST -> AVLYST + BrukerModel.Kalenderavtale.Tilstand.AVHOLDT -> AVHOLDT } } } diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerModel.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerModel.kt index 85227b945..5c0a6fbda 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerModel.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerModel.kt @@ -106,6 +106,7 @@ object BrukerModel { ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED, ARBEIDSGIVER_HAR_GODTATT, AVLYST, + AVHOLDT, } } diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerRepository.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerRepository.kt index 4d81a5924..9a5004c81 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerRepository.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/BrukerRepository.kt @@ -786,6 +786,7 @@ class BrukerRepositoryImpl( where n.type = 'KALENDERAVTALE' and n.tilstand != 'AVLYST' and + n.tilstand != 'AVHOLDT' and n.start_tidspunkt::timestamp > now() and n.virksomhetsnummer = any(?) order by diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/hendelse/Hendelse.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/hendelse/Hendelse.kt index 6f58c48f1..b2e07bdcd 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/hendelse/Hendelse.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/hendelse/Hendelse.kt @@ -557,6 +557,7 @@ object HendelseModel { ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED, ARBEIDSGIVER_HAR_GODTATT, AVLYST, + AVHOLDT, } @JsonTypeName("Lokasjon") diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt index 51d9d06fa..614040c4e 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt @@ -22,7 +22,7 @@ import java.util.* class Database private constructor( private val config: Config, private val dataSource: NonBlockingDataSource<*>, -): Closeable { +) : Closeable { data class Config( val host: String, val port: String, @@ -72,7 +72,7 @@ class Database private constructor( suspend fun openDatabase( config: Config, - flywayAction: Flyway.() -> Unit = { migrate () }, + flywayAction: Flyway.() -> Unit = { migrate() }, ): Database { val hikariConfig = config.asHikariConfig() @@ -250,24 +250,37 @@ class ParameterSetters( private var index = 1 - fun >enumAsText(value: T) = text(value.toString()) + fun > enumAsText(value: T) = text(value.toString()) fun text(value: String) = preparedStatement.setString(index++, value) fun nullableText(value: String?) = preparedStatement.setString(index++, value) fun integer(value: Int) = preparedStatement.setInt(index++, value) fun long(value: Long) = preparedStatement.setLong(index++, value) fun boolean(newState: Boolean) = preparedStatement.setBoolean(index++, newState) - fun nullableBoolean(value: Boolean?) = if (value == null) preparedStatement.setNull(index++, Types.BOOLEAN) else boolean(value) + fun nullableBoolean(value: Boolean?) = + if (value == null) preparedStatement.setNull(index++, Types.BOOLEAN) else boolean(value) + fun uuid(value: UUID) = preparedStatement.setObject(index++, value) fun nullableUuid(value: UUID?) = preparedStatement.setObject(index++, value) + /** * all timestamp values must be `truncatedTo` micros to avoid rounding/precision errors when writing and reading **/ - fun nullableTimestamptz(value: OffsetDateTime?) = preparedStatement.setObject(index++, value?.truncatedTo(ChronoUnit.MICROS)) - fun timestamp_without_timezone_utc(value: OffsetDateTime) = timestamp_without_timezone(value.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()) - fun timestamp_without_timezone_utc(value: Instant) = timestamp_without_timezone(LocalDateTime.ofInstant(value, ZoneOffset.UTC)) - fun timestamp_without_timezone(value: LocalDateTime) = preparedStatement.setObject(index++, value.truncatedTo(ChronoUnit.MICROS)) - fun timestamp_with_timezone(value: OffsetDateTime) = preparedStatement.setObject(index++, value.truncatedTo(ChronoUnit.MICROS)) + fun nullableTimestamptz(value: OffsetDateTime?) = + preparedStatement.setObject(index++, value?.truncatedTo(ChronoUnit.MICROS)) + + fun timestamp_without_timezone_utc(value: OffsetDateTime) = + timestamp_without_timezone(value.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()) + + fun timestamp_without_timezone_utc(value: Instant) = + timestamp_without_timezone(LocalDateTime.ofInstant(value, ZoneOffset.UTC)) + + fun timestamp_without_timezone(value: LocalDateTime) = + preparedStatement.setObject(index++, value.truncatedTo(ChronoUnit.MICROS)) + + fun timestamp_with_timezone(value: OffsetDateTime) = + preparedStatement.setObject(index++, value.truncatedTo(ChronoUnit.MICROS)) + fun bytea(value: ByteArray) = preparedStatement.setBytes(index++, value) fun byteaOrNull(value: ByteArray?) = preparedStatement.setBytes(index++, value) fun toInstantAsText(value: OffsetDateTime) = instantAsText(value.toInstant()) @@ -277,10 +290,13 @@ class ParameterSetters( fun nullableLocalDateTimeAsText(value: LocalDateTime?) = nullableText(value?.toString()) fun localDateTimeAsText(value: LocalDateTime) = text(value.toString()) + fun nullableLocalDateAsText(value: LocalDate?) = nullableText(value?.toString()) + fun localDateAsText(value: LocalDate) = text(value.toString()) fun periodAsText(value: ISO8601Period) = text(value.toString()) fun nullableDate(value: LocalDate?) = - preparedStatement.setDate(index++, value?.let { java.sql.Date.valueOf(it) } ) + preparedStatement.setDate(index++, value?.let { java.sql.Date.valueOf(it) }) + fun date(value: LocalDate) = preparedStatement.setDate(index++, value.let { java.sql.Date.valueOf(it) }) @@ -289,6 +305,7 @@ class ParameterSetters( nullableText( value?.let { laxObjectMapper.writeValueAsStringSupportingTypeInfoInCollections(value) } ) + inline fun jsonb(value: T) = text( laxObjectMapper.writeValueAsStringSupportingTypeInfoInCollections(value) @@ -327,12 +344,15 @@ class ParameterSetters( fun ResultSet.getUuid(column: String): UUID = getObject(column, UUID::class.java) -fun measureSql(sql: String, action: () -> T): T { +fun measureSql( + sql: String, + action: () -> T +): T { val timer = getTimer( name = "database.execution", tags = setOf("sql" to sql), description = "Execution time for sql query or update" ) @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") - return timer.record(action) + return timer.recordCallable(action) } diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/local_database/EphemeralDatabase.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/local_database/EphemeralDatabase.kt index 323bc8613..6a0a609b3 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/local_database/EphemeralDatabase.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/local_database/EphemeralDatabase.kt @@ -130,6 +130,7 @@ fun ResultSet.getUUID(columnLabel: String): UUID = UUID.fromString(getString(col fun ResultSet.getInstant(columnLabel: String): Instant = Instant.parse(getString(columnLabel)) fun ResultSet.getLocalDate(columnLabel: String): LocalDate = LocalDate.parse(getString(columnLabel)) fun ResultSet.getLocalDateOrNull(columnLabel: String) = getString(columnLabel)?.let(LocalDate::parse) +fun ResultSet.getLocalDateTime(columnLabel: String) = LocalDateTime.parse(getString(columnLabel)) fun ResultSet.getLocalDateTimeOrNull(columnLabel: String) = getString(columnLabel)?.let(LocalDateTime::parse) inline fun > ResultSet.getEnum(columnLabel: String): E = enumValueOf(getString(columnLabel)) inline fun ResultSet.getJson(columnLabel: String) = ephemeralDatabaseObjectMapper.readValue(getString(columnLabel)) diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/ProdusentModel.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/ProdusentModel.kt index d20e28254..3dd635385 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/ProdusentModel.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/ProdusentModel.kt @@ -194,6 +194,7 @@ object ProdusentModel { ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED, ARBEIDSGIVER_HAR_GODTATT, AVLYST, + AVHOLDT, } override fun erDuplikatAv(other: Notifikasjon): Boolean { @@ -318,6 +319,7 @@ fun KalenderavtaleOpprettet.tilProdusentModel() = KalenderavtaleTilstand.ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED -> ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED KalenderavtaleTilstand.ARBEIDSGIVER_HAR_GODTATT -> ARBEIDSGIVER_HAR_GODTATT KalenderavtaleTilstand.AVLYST -> AVLYST + KalenderavtaleTilstand.AVHOLDT -> AVHOLDT }, deletedAt = null, eksterneVarsler = eksterneVarsler.map(EksterntVarsel::tilProdusentModel), diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/api/MutationKalenderavtale.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/api/MutationKalenderavtale.kt index c33211365..554fb910a 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/api/MutationKalenderavtale.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/api/MutationKalenderavtale.kt @@ -61,8 +61,6 @@ internal class MutationKalenderavtale( nyTilstand = env.getTypedArgumentOrNull("nyTilstand"), nyTekst = env.getTypedArgumentOrNull("nyTekst"), nyLenke = env.getTypedArgumentOrNull("nyLenke"), - nyttStartTidspunkt = env.getTypedArgumentOrNull("nyttStartTidspunkt"), - nyttSluttTidspunkt = env.getTypedArgumentOrNull("nyttSluttTidspunkt"), nyLokasjon = env.getTypedArgumentOrNull("nyLokasjon"), nyErDigitalt = env.getTypedArgumentOrNull("nyErDigitalt"), eksterneVarsler = env.getTypedArgumentOrDefault>("eksterneVarsler") { emptyList() }.ifEmpty { null }, @@ -82,8 +80,6 @@ internal class MutationKalenderavtale( nyTilstand = env.getTypedArgumentOrNull("nyTilstand"), nyTekst = env.getTypedArgumentOrNull("nyTekst"), nyLenke = env.getTypedArgumentOrNull("nyLenke"), - nyttStartTidspunkt = env.getTypedArgumentOrNull("nyttStartTidspunkt"), - nyttSluttTidspunkt = env.getTypedArgumentOrNull("nyttSluttTidspunkt"), nyLokasjon = env.getTypedArgumentOrNull("nyLokasjon"), nyErDigitalt = env.getTypedArgumentOrNull("nyErDigitalt"), eksterneVarsler = env.getTypedArgumentOrDefault>("eksterneVarsler") { emptyList() }.ifEmpty { null }, @@ -194,7 +190,8 @@ internal class MutationKalenderavtale( ARBEIDSGIVER_VIL_AVLYSE, ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED, ARBEIDSGIVER_HAR_GODTATT, - AVLYST; + AVLYST, + AVHOLDT; fun tilHendelseModel(): HendelseModel.KalenderavtaleTilstand = when (this) { VENTER_SVAR_FRA_ARBEIDSGIVER -> HendelseModel.KalenderavtaleTilstand.VENTER_SVAR_FRA_ARBEIDSGIVER @@ -202,6 +199,7 @@ internal class MutationKalenderavtale( ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED -> HendelseModel.KalenderavtaleTilstand.ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED ARBEIDSGIVER_HAR_GODTATT -> HendelseModel.KalenderavtaleTilstand.ARBEIDSGIVER_HAR_GODTATT AVLYST -> HendelseModel.KalenderavtaleTilstand.AVLYST + AVHOLDT -> HendelseModel.KalenderavtaleTilstand.AVHOLDT } } @@ -210,13 +208,11 @@ internal class MutationKalenderavtale( nyKalenderavtale: NyKalenderavtaleInput, ): NyKalenderavtaleResultat { val produsent = hentProdusent(context) { error -> return error } - val sak: Sak = nyKalenderavtale.grupperingsid.let { grupperingsid -> - hentSak( - produsentRepository = produsentRepository, - grupperingsid = grupperingsid, - merkelapp = nyKalenderavtale.merkelapp - ) { error -> return error } - } + val sak: Sak = hentSak( + produsentRepository = produsentRepository, + grupperingsid = nyKalenderavtale.grupperingsid, + merkelapp = nyKalenderavtale.merkelapp + ) { error -> return error } val id = UUID.randomUUID() val nyKalenderavtaleHendelse = nyKalenderavtale.somKalenderavtaleOpprettetHendelse( @@ -281,29 +277,8 @@ internal class MutationKalenderavtale( onValidationError: (error: Error.UgyldigKalenderavtale) -> Nothing, ): KalenderavtaleOppdatert { - if ( - nyttStartTidspunkt != null && - nyttSluttTidspunkt != null && - nyttStartTidspunkt!!.isAfter(nyttSluttTidspunkt) - ) { - onValidationError( - Error.UgyldigKalenderavtale("startTidspunkt må være før sluttTidspunkt") - ) - } else if ( - nyttStartTidspunkt != null && - nyttStartTidspunkt!!.inOsloLocalDateTime().isAfter(eksisterende.sluttTidspunkt) - ) { - onValidationError( - Error.UgyldigKalenderavtale("startTidspunkt må være før sluttTidspunkt") - ) - } else if ( - nyttSluttTidspunkt != null - && nyttSluttTidspunkt!!.inOsloLocalDateTime().isBefore(eksisterende.startTidspunkt) - ) { - onValidationError( - Error.UgyldigKalenderavtale("startTidspunkt må være før sluttTidspunkt") - ) - } + // TODO: mulig vi skal hindre at man ved oppdater går fra lukket til åpen tilstand, + // men at man kan bevege seg mellom lukkede tilstander er ok. return KalenderavtaleOppdatert( virksomhetsnummer = eksisterende.virksomhetsnummer, @@ -315,8 +290,8 @@ internal class MutationKalenderavtale( tilstand = nyTilstand?.tilHendelseModel(), lenke = nyLenke, tekst = nyTekst, - startTidspunkt = nyttStartTidspunkt?.inOsloLocalDateTime(), - sluttTidspunkt = nyttSluttTidspunkt?.inOsloLocalDateTime(), + startTidspunkt = null, // endring av tidspunkt er ikke støttet. disse er ment å representere evt ny tid + sluttTidspunkt = null, // endring av tidspunkt er ikke støttet. disse er ment å representere evt ny tid lokasjon = nyLokasjon?.tilHendelseModel(), erDigitalt = nyErDigitalt, hardDelete = hardDelete?.tilHendelseModel(), @@ -324,7 +299,7 @@ internal class MutationKalenderavtale( påminnelse = if (nyTilstand == AVLYST) null else paaminnelse?.tilDomene( notifikasjonOpprettetTidspunkt = eksisterende.opprettetTidspunkt, frist = null, - startTidspunkt = (nyttStartTidspunkt?.inOsloLocalDateTime() ?: eksisterende.startTidspunkt), + startTidspunkt = eksisterende.startTidspunkt, virksomhetsnummer = eksisterende.virksomhetsnummer, ), idempotenceKey = idempotenceKey, @@ -337,8 +312,6 @@ internal class MutationKalenderavtale( abstract val nyTilstand: KalenderavtaleTilstand? abstract val nyTekst: String? abstract val nyLenke: String? - abstract val nyttStartTidspunkt: OffsetDateTime? - abstract val nyttSluttTidspunkt: OffsetDateTime? abstract val nyLokasjon: NyKalenderavtaleInput.LokasjonInput? abstract val nyErDigitalt: Boolean? abstract val hardDelete: HardDeleteUpdateInput? @@ -355,8 +328,6 @@ internal class MutationKalenderavtale( nyTilstand, nyTekst, nyLenke, - nyttStartTidspunkt, - nyttSluttTidspunkt, nyLokasjon, nyErDigitalt, hardDelete, @@ -370,8 +341,6 @@ internal class MutationKalenderavtale( override val nyTilstand: KalenderavtaleTilstand?, override val nyTekst: String?, override val nyLenke: String?, - override val nyttStartTidspunkt: OffsetDateTime?, - override val nyttSluttTidspunkt: OffsetDateTime?, override val nyLokasjon: NyKalenderavtaleInput.LokasjonInput?, override val nyErDigitalt: Boolean?, override val eksterneVarsler: List?, @@ -386,8 +355,6 @@ internal class MutationKalenderavtale( override val nyTilstand: KalenderavtaleTilstand?, override val nyTekst: String?, override val nyLenke: String?, - override val nyttStartTidspunkt: OffsetDateTime?, - override val nyttSluttTidspunkt: OffsetDateTime?, override val nyLokasjon: NyKalenderavtaleInput.LokasjonInput?, override val nyErDigitalt: Boolean?, override val eksterneVarsler: List?, diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" index f857a29d4..69949a702 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" @@ -1,19 +1,27 @@ package no.nav.arbeidsgiver.notifikasjon.skedulert_utgått import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database.Companion.openDatabaseAsync import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Health import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Subsystem import no.nav.arbeidsgiver.notifikasjon.infrastruktur.http.launchHttpServer -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.PartitionAwareHendelsesstrøm +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.HendelsesstrømKafkaImpl +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.NOTIFIKASJON_TOPIC import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.lagKafkaHendelseProdusent +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.launchProcessingLoop +import java.time.Duration object SkedulertUtgått { + val databaseConfig = Database.config("skedulert_utgatt_model") private val hendelsesstrøm by lazy { - PartitionAwareHendelsesstrøm( - groupId = "skedulert-utgaatt-1", - newPartitionProcessor = { SkedulertUtgåttService(hendelseProdusent = lagKafkaHendelseProdusent()) }, + HendelsesstrømKafkaImpl( + topic = NOTIFIKASJON_TOPIC, + groupId = "skedulert-harddelete-model-builder-1", + replayPeriodically = true, ) } @@ -21,9 +29,36 @@ object SkedulertUtgått { Health.subsystemReady[Subsystem.DATABASE] = true runBlocking(Dispatchers.Default) { + val database = openDatabaseAsync(databaseConfig) + val repoAsync = async { + SkedulertUtgåttRepository(database.await()) + } launch { - hendelsesstrøm.start() + val repo = repoAsync.await() + hendelsesstrøm.forEach { hendelse, metadata -> + repo.oppdaterModellEtterHendelse(hendelse) + } + } + + val service = async { + SkedulertUtgåttService( + repository = repoAsync.await(), + hendelseProdusent = lagKafkaHendelseProdusent(topic = NOTIFIKASJON_TOPIC) + ) } + launchProcessingLoop( + "utgaatt-oppgaver-service", + pauseAfterEach = Duration.ofMinutes(1) + ) { + service.await().settOppgaverUtgåttBasertPåFrist() + } + launchProcessingLoop( + "avholdt-kalenderavtaler-service", + pauseAfterEach = Duration.ofMinutes(1) + ) { + service.await().settKalenderavtalerAvholdtBasertPåTidspunkt() + } + launchHttpServer(httpPort = httpPort) } } diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttRepository.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttRepository.kt" index b9598b827..d66a59bb8 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttRepository.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttRepository.kt" @@ -1,117 +1,227 @@ package no.nav.arbeidsgiver.notifikasjon.skedulert_utgått -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.local_database.* -import java.sql.Connection +import no.nav.arbeidsgiver.notifikasjon.hendelse.HardDeletedRepository +import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel +import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.KalenderavtaleTilstand +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Transaction +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.local_database.getLocalDate +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.local_database.getLocalDateTime +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.local_database.getUUID +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.logger +import no.nav.arbeidsgiver.notifikasjon.tid.asOsloLocalDate +import java.time.Instant import java.time.LocalDate +import java.time.LocalDateTime import java.util.* -class SkedulertUtgåttRepository : AutoCloseable { - private val database = EphemeralDatabase("skedulert_utgatt", - """ - create table oppgaver ( - oppgave_id text not null primary key, - frist text not null, - virksomhetsnummer text not null, - produsent_id text not null - ); - - create index oppgaver_frist_idx on oppgaver(frist); - - create table oppgave_sak_kobling ( - oppgave_id text not null primary key, - sak_id text not null - ); - - create table slettede_oppgaver ( - oppgave_id text not null primary key - ); - - create table slettede_saker ( - sak_id text not null, - grupperingsid text not null, - merkelapp text not null, - constraint slettede_saker_pk primary key (sak_id) - ); - """.trimIndent() - ) - override fun close() = database.close() - - class SkedulertUtgått( +class SkedulertUtgåttRepository( + private val database: Database +) : HardDeletedRepository(database) { + class Oppgave( val oppgaveId: UUID, val frist: LocalDate, val virksomhetsnummer: String, val produsentId: String, ) - fun hentOgFjernAlleMedFrist(localDateNow: LocalDate): Collection { - return database.useTransaction { - executeQuery( - """ - delete from oppgaver - where frist < ? - returning * - """.trimIndent(), - setup = { - setLocalDate(localDateNow) - }, - result = { - val alleUtgåtte = mutableListOf() - while (next()) { - alleUtgåtte.add( - SkedulertUtgått( - oppgaveId = getUUID("oppgave_id"), - frist = getLocalDate("frist"), - virksomhetsnummer = getString("virksomhetsnummer"), - produsentId = getString("produsent_id"), - ) + class Kalenderavtale( + val kalenderavtaleId: UUID, + val startTidspunkt: LocalDateTime, + val tilstand: KalenderavtaleTilstand, + val virksomhetsnummer: String, + val produsentId: String, + val merkelapp: String, + val grupperingsid: String, + val opprettetTidspunkt: Instant, + ) + + enum class AggregatType { + OPPGAVE, + KALENDERAVTALE, + } + + private val log = logger() + + suspend fun oppdaterModellEtterHendelse(hendelse: HendelseModel.Hendelse) { + if (hendelse is HendelseModel.AggregatOpprettet) { + registrerKoblingForCascadeDelete(hendelse) + } + if (erHardDeleted(hendelse.aggregateId)) { + log.info("skipping harddeleted event {}", hendelse) + return + } + + when (hendelse) { + is HendelseModel.OppgaveOpprettet -> { + /* Vi må huske saks-id uavhengig av om det er frist på oppgaven, for + * det kan komme en frist senere. */ + if (hendelse.sakId != null) { + huskSakOppgaveKobling( + sakId = hendelse.sakId, + oppgaveId = hendelse.aggregateId + ) + } + + if (hendelse.frist != null) { + upsertSkedulertUtgåttOppgave( + Oppgave( + oppgaveId = hendelse.notifikasjonId, + frist = hendelse.frist, + virksomhetsnummer = hendelse.virksomhetsnummer, + produsentId = hendelse.produsentId, ) + ) + } + } + + is HendelseModel.KalenderavtaleOpprettet -> { + huskSakKalenderavtaleKobling( + sakId = hendelse.sakId, + kalenderavtaleId = hendelse.aggregateId + ) + + skedulerAvholdtKalenderavtale( + Kalenderavtale( + kalenderavtaleId = hendelse.notifikasjonId, + startTidspunkt = hendelse.startTidspunkt, + virksomhetsnummer = hendelse.virksomhetsnummer, + produsentId = hendelse.produsentId, + tilstand = hendelse.tilstand, + merkelapp = hendelse.merkelapp, + grupperingsid = hendelse.grupperingsid, + opprettetTidspunkt = hendelse.opprettetTidspunkt.toInstant(), + ) + ) + } + + is HendelseModel.KalenderavtaleOppdatert -> { + when (hendelse.tilstand) { + null -> { + // tilstand er uendret, skedulering forblir som den er + return + } + + KalenderavtaleTilstand.AVLYST, + KalenderavtaleTilstand.AVHOLDT -> { + // kalenderavtale er avlyst eller avholdt, slett skedulering + database.transaction { + slettSkedulertUtgått(hendelse.notifikasjonId) + } + } + + else -> { + // kalenderavtale er endret til en annen åpen tilstand, skeduler avholdt på nytt + oppdaterSkedulerAvholdtKalenderavtale(hendelse.notifikasjonId) } - alleUtgåtte } - ) - } - } + } + + is HendelseModel.FristUtsatt -> { + upsertSkedulertUtgåttOppgave( + Oppgave( + oppgaveId = hendelse.notifikasjonId, + frist = hendelse.frist, + virksomhetsnummer = hendelse.virksomhetsnummer, + produsentId = hendelse.produsentId, + ) + ) + } + is HendelseModel.OppgaveUtgått -> + slettSkeduleringHvisOppgaveErEldre( + oppgaveId = hendelse.aggregateId, + utgaattTidspunkt = hendelse.utgaattTidspunkt.asOsloLocalDate() + ) - private fun Connection.erOppgaveSlettet(oppgaveId: UUID): Boolean { - return executeQuery( - """ - select true as slettet - from slettede_oppgaver - where oppgave_id = ? - limit 1 - """.trimIndent(), - setup = { - setUUID(oppgaveId) - }, - result = { - if (next()) getBoolean("slettet") else false + is HendelseModel.OppgaveUtført -> + database.transaction { + slettOppgave(aggregateId = hendelse.aggregateId) + } + + is HendelseModel.SoftDelete, + is HendelseModel.HardDelete -> { + database.transaction { + slett( + aggregateId = hendelse.aggregateId, + grupperingsid = when (hendelse) { + is HendelseModel.SoftDelete -> hendelse.grupperingsid + is HendelseModel.HardDelete -> hendelse.grupperingsid + else -> throw IllegalStateException("Uventet hendelse $hendelse") + }, + merkelapp = when (hendelse) { + is HendelseModel.SoftDelete -> hendelse.merkelapp + is HendelseModel.HardDelete -> hendelse.merkelapp + else -> throw IllegalStateException("Uventet hendelse $hendelse") + }, + ) + registrerDelete(this, hendelse.aggregateId) + } } - ) + + is HendelseModel.BeskjedOpprettet, + is HendelseModel.BrukerKlikket, + is HendelseModel.PåminnelseOpprettet, + is HendelseModel.SakOpprettet, + is HendelseModel.NyStatusSak, + is HendelseModel.NesteStegSak, + is HendelseModel.TilleggsinformasjonSak, + is HendelseModel.EksterntVarselFeilet, + is HendelseModel.EksterntVarselKansellert, + is HendelseModel.OppgavePåminnelseEndret, + is HendelseModel.EksterntVarselVellykket -> Unit + } } - private fun Connection.erSakForOppgaveSlettet(oppgaveId: UUID): Boolean { - return executeQuery( - """ - select true as slettet - from oppgave_sak_kobling - join slettede_saker using (sak_id) - where oppgave_sak_kobling.oppgave_id = ? - limit 1 - """.trimIndent(), - setup = { - setUUID(oppgaveId) - }, - result = { - if (next()) getBoolean("slettet") else false - } + suspend fun hentOgFjernAlleUtgåtteOppgaver(localDateNow: LocalDate) = database.nonTransactionalExecuteQuery( + """ + delete from skedulert_utgatt as s + using oppgave as o + where o.oppgave_id = s.aggregat_id + and s.aggregat_type = ? and o.frist < ? + returning * + """, { + text(AggregatType.OPPGAVE.name) + localDateAsText(localDateNow) + }) { + Oppgave( + oppgaveId = getUUID("oppgave_id"), + frist = getLocalDate("frist"), + virksomhetsnummer = getString("virksomhetsnummer"), + produsentId = getString("produsent_id"), ) } - private fun Connection.upsertOppgaveFrist(skedulertUtgått: SkedulertUtgått) { + suspend fun hentOgFjernAlleAvholdteKalenderavtaler(localDateTimeNow: LocalDateTime) = + database.nonTransactionalExecuteQuery( + """ + delete from skedulert_utgatt as s + using kalenderavtale as k + where k.kalenderavtale_id = s.aggregat_id + and s.aggregat_type = ? and k.start_tidspunkt < ? + returning * + """, { + text(AggregatType.KALENDERAVTALE.name) + localDateTimeAsText(localDateTimeNow) + }) { + Kalenderavtale( + kalenderavtaleId = getUUID("kalenderavtale_id"), + startTidspunkt = getLocalDateTime("start_tidspunkt"), + virksomhetsnummer = getString("virksomhetsnummer"), + tilstand = getString("tilstand").let { KalenderavtaleTilstand.valueOf(it) }, + produsentId = getString("produsent_id"), + merkelapp = getString("merkelapp"), + grupperingsid = getString("grupperingsid"), + opprettetTidspunkt = Instant.parse( + getString("opprettet_tidspunkt"), + ) + ) + } + + private suspend fun upsertSkedulertUtgåttOppgave(oppgave: Oppgave) = database.transaction { executeUpdate( """ - insert into oppgaver(oppgave_id, frist, virksomhetsnummer, produsent_id) + insert into oppgave(oppgave_id, frist, virksomhetsnummer, produsent_id) values (?, ?, ?, ?) on conflict (oppgave_id) do update set frist = excluded.frist, @@ -119,134 +229,237 @@ class SkedulertUtgåttRepository : AutoCloseable { produsent_id = excluded.produsent_id """.trimIndent() ) { - setUUID(skedulertUtgått.oppgaveId) - setLocalDate(skedulertUtgått.frist) - setText(skedulertUtgått.virksomhetsnummer) - setText(skedulertUtgått.produsentId) + uuid(oppgave.oppgaveId) + localDateAsText(oppgave.frist) + text(oppgave.virksomhetsnummer) + text(oppgave.produsentId) + } + executeUpdate( + """ + insert into skedulert_utgatt(aggregat_id, aggregat_type) + values (?, ?) + on conflict (aggregat_id) do nothing + """.trimIndent() + ) { + uuid(oppgave.oppgaveId) + text(AggregatType.OPPGAVE.name) } } - fun skedulerUtgått(skedulertUtgått: SkedulertUtgått) { - database.useTransaction { - if (erOppgaveSlettet(oppgaveId = skedulertUtgått.oppgaveId)) { - return@useTransaction + private suspend fun skedulerAvholdtKalenderavtale( + kalenderavtale: Kalenderavtale + ) = database.transaction { + executeUpdate( + """ + insert into kalenderavtale( + kalenderavtale_id, start_tidspunkt, tilstand, virksomhetsnummer, produsent_id, merkelapp, grupperingsid, opprettet_tidspunkt + ) + values (?, ?, ?, ?, ?, ?, ?, ?) + on conflict (kalenderavtale_id) do update set + start_tidspunkt = excluded.start_tidspunkt, + tilstand = excluded.tilstand, + virksomhetsnummer = excluded.virksomhetsnummer, + produsent_id = excluded.produsent_id, + merkelapp = excluded.merkelapp, + grupperingsid = excluded.grupperingsid, + opprettet_tidspunkt = excluded.opprettet_tidspunkt + """.trimIndent() + ) { + uuid(kalenderavtale.kalenderavtaleId) + localDateTimeAsText(kalenderavtale.startTidspunkt) + text(kalenderavtale.tilstand.name) + text(kalenderavtale.virksomhetsnummer) + text(kalenderavtale.produsentId) + text(kalenderavtale.merkelapp) + text(kalenderavtale.grupperingsid) + text(kalenderavtale.opprettetTidspunkt.toString()) + } + when (kalenderavtale.tilstand) { + KalenderavtaleTilstand.AVHOLDT, + KalenderavtaleTilstand.AVLYST -> { + // sluttilstand ingen behov for skedulert overgang } - if (erSakForOppgaveSlettet(oppgaveId = skedulertUtgått.oppgaveId)) { - return@useTransaction + + else -> { + executeUpdate( + """ + insert into skedulert_utgatt(aggregat_id, aggregat_type) + values (?, ?) + on conflict (aggregat_id) do nothing + """.trimIndent() + ) { + uuid(kalenderavtale.kalenderavtaleId) + text(AggregatType.KALENDERAVTALE.name) + } } - upsertOppgaveFrist(skedulertUtgått) } } + private suspend fun oppdaterSkedulerAvholdtKalenderavtale( + kalenderavtaleId: UUID + ) = database.transaction { - fun slettOmEldre(oppgaveId: UUID, utgaattTidspunkt: LocalDate) { - database.useTransaction { - executeUpdate( - """ - delete from oppgaver - where oppgave_id = ? and frist <= ? - """.trimIndent() - ) { - setUUID(oppgaveId) - setLocalDate(utgaattTidspunkt) - } + executeUpdate( + """ + insert into skedulert_utgatt(aggregat_id, aggregat_type) + select ?, ? + where exists ( + select 1 from kalenderavtale + where kalenderavtale_id = ? + ) + on conflict (aggregat_id) do nothing + """.trimIndent() + ) { + uuid(kalenderavtaleId) + text(AggregatType.KALENDERAVTALE.name) + uuid(kalenderavtaleId) } } - fun slettOppgave(aggregateId: UUID) { - database.useTransaction { - this@useTransaction.slettOppgave(aggregateId = aggregateId) + suspend fun slettSkeduleringHvisOppgaveErEldre(oppgaveId: UUID, utgaattTidspunkt: LocalDate) = + database.nonTransactionalExecuteUpdate( + """ + delete from skedulert_utgatt + using skedulert_utgatt as s + left join oppgave as o on s.aggregat_id = o.oppgave_id + where s.aggregat_type = ? + and o.oppgave_id = ? + and o.frist <= ? + """.trimIndent() + ) { + text(AggregatType.OPPGAVE.name) + uuid(oppgaveId) + localDateAsText(utgaattTidspunkt) + } + + fun Transaction.slett( + aggregateId: UUID, + grupperingsid: String?, + merkelapp: String?, + ) { + if (grupperingsid != null && merkelapp != null) { + slettOppgaverKnyttetTilSak(sakId = aggregateId) + slettKalenderavtalerKnyttetTilSak(sakId = aggregateId) + } else { + slettOppgave(aggregateId = aggregateId) + slettKalenderavtale(aggregateId = aggregateId) } } - private fun Connection.slettOppgave(aggregateId: UUID) { + private fun Transaction.slettOppgaverKnyttetTilSak(sakId: UUID) { executeUpdate( """ - delete from oppgaver - where oppgave_id = ? + delete from oppgave + where oppgave_id in ( + select oppgave_sak_kobling.oppgave_id + from oppgave_sak_kobling + where sak_id = ? + ) + """ + ) { + uuid(sakId) + } + executeUpdate( + """ + delete from skedulert_utgatt + where aggregat_id in ( + select oppgave_sak_kobling.oppgave_id + from oppgave_sak_kobling + where sak_id = ? + ) + """ + ) { + uuid(sakId) + } + } + + private fun Transaction.slettKalenderavtalerKnyttetTilSak(sakId: UUID) { + executeUpdate( + """ + delete from kalenderavtale + where kalenderavtale_id in ( + select kalenderavtale_sak_kobling.kalenderavtale_id + from kalenderavtale_sak_kobling + where sak_id = ? + ) """.trimIndent() ) { - setUUID(aggregateId) + uuid(sakId) + } + + executeUpdate( + """ + delete from skedulert_utgatt + where aggregat_id in ( + select kalenderavtale_sak_kobling.kalenderavtale_id + from kalenderavtale_sak_kobling + where sak_id = ? + ) + """ + ) { + uuid(sakId) } } - private fun Connection.huskSlettetOppgave(aggregateId: UUID) { + fun Transaction.slettOppgave(aggregateId: UUID) { executeUpdate( """ - insert into slettede_oppgaver(oppgave_id) - values (?) - on conflict (oppgave_id) do nothing + delete from oppgave + where oppgave_id = ? """.trimIndent() ) { - setUUID(aggregateId) + uuid(aggregateId) } + slettSkedulertUtgått(aggregateId = aggregateId) } - private fun Connection.huskSlettetSak( - grupperingsid: String, - merkelapp: String, - sakId: UUID, - ) { + fun Transaction.slettKalenderavtale(aggregateId: UUID) { executeUpdate( """ - insert into slettede_saker(grupperingsid, merkelapp, sak_id) - values (?, ?, ?) - on conflict (sak_id) do nothing + delete from kalenderavtale + where kalenderavtale_id = ? """.trimIndent() ) { - setText(grupperingsid) - setText(merkelapp) - setUUID(sakId) + uuid(aggregateId) } + slettSkedulertUtgått(aggregateId = aggregateId) } - private fun Connection.slettOppgaverKnyttetTilSak(sakId: UUID) { + fun Transaction.slettSkedulertUtgått(aggregateId: UUID) { executeUpdate( """ - delete from oppgaver - where oppgave_id in ( - select oppgave_sak_kobling.oppgave_id - from oppgave_sak_kobling - where sak_id = ? - ) + delete from skedulert_utgatt + where aggregat_id = ? """.trimIndent() ) { - setUUID(sakId) + uuid(aggregateId) } } - fun huskSakOppgaveKobling(sakId: UUID, oppgaveId: UUID) { - database.useTransaction { - executeUpdate( - """ - insert into oppgave_sak_kobling (oppgave_id, sak_id) values (?, ?) - on conflict (oppgave_id) do update - set sak_id = excluded.sak_id - """.trimIndent() - ) { - setUUID(oppgaveId) - setUUID(sakId) - } + suspend fun huskSakOppgaveKobling(sakId: UUID, oppgaveId: UUID) { + database.nonTransactionalExecuteUpdate( + """ + insert into oppgave_sak_kobling (oppgave_id, sak_id) values (?, ?) + on conflict (oppgave_id) do update + set sak_id = excluded.sak_id + """.trimIndent() + ) { + uuid(oppgaveId) + uuid(sakId) } } - fun slettOgHuskSlett( - aggregateId: UUID, - grupperingsid: String?, - merkelapp: String?, - ) { - database.useTransaction { - if (grupperingsid != null && merkelapp != null) { - huskSlettetSak( - grupperingsid = grupperingsid, - merkelapp = merkelapp, - sakId = aggregateId, - ) - slettOppgaverKnyttetTilSak(sakId = aggregateId) - } else { - huskSlettetOppgave(aggregateId = aggregateId) - slettOppgave(aggregateId = aggregateId) - } + + suspend fun huskSakKalenderavtaleKobling(sakId: UUID, kalenderavtaleId: UUID) { + database.nonTransactionalExecuteUpdate( + """ + insert into kalenderavtale_sak_kobling (kalenderavtale_id, sak_id) values (?, ?) + on conflict (kalenderavtale_id) do update + set sak_id = excluded.sak_id + """.trimIndent() + ) { + uuid(kalenderavtaleId) + uuid(sakId) } } } diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttService.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttService.kt" index 0291b2864..5d55bf199 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttService.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttService.kt" @@ -1,16 +1,12 @@ package no.nav.arbeidsgiver.notifikasjon.skedulert_utgått -import kotlinx.coroutines.time.delay import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseProdusent import no.nav.arbeidsgiver.notifikasjon.infrastruktur.NaisEnvironment -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.PartitionHendelseMetadata -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.PartitionProcessor import no.nav.arbeidsgiver.notifikasjon.tid.OsloTid import no.nav.arbeidsgiver.notifikasjon.tid.OsloTidImpl -import no.nav.arbeidsgiver.notifikasjon.tid.asOsloLocalDate import no.nav.arbeidsgiver.notifikasjon.tid.atOslo -import java.time.Duration +import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -18,99 +14,12 @@ import java.util.* class SkedulertUtgåttService( + private val repository: SkedulertUtgåttRepository, private val hendelseProdusent: HendelseProdusent, private val osloTid: OsloTid = OsloTidImpl -): PartitionProcessor { - private val repository = SkedulertUtgåttRepository() - - override fun close() = repository.close() - - override suspend fun processHendelse(hendelse: HendelseModel.Hendelse, metadata: PartitionHendelseMetadata) { - @Suppress("UNUSED_VARIABLE") - val ignored = when (hendelse) { - is HendelseModel.OppgaveOpprettet -> { - /* Vi må huske saks-id uavhengig av om det er frist på oppgaven, for - * det kan komme en frist senere. */ - if (hendelse.sakId != null) { - repository.huskSakOppgaveKobling( - sakId = hendelse.sakId, - oppgaveId = hendelse.aggregateId - ) - } - - if (hendelse.frist != null) { - repository.skedulerUtgått( - SkedulertUtgåttRepository.SkedulertUtgått( - oppgaveId = hendelse.notifikasjonId, - frist = hendelse.frist, - virksomhetsnummer = hendelse.virksomhetsnummer, - produsentId = hendelse.produsentId, - ) - ) - } - - Unit - } - - is HendelseModel.FristUtsatt -> { - repository.skedulerUtgått( - SkedulertUtgåttRepository.SkedulertUtgått( - oppgaveId = hendelse.notifikasjonId, - frist = hendelse.frist, - virksomhetsnummer = hendelse.virksomhetsnummer, - produsentId = hendelse.produsentId, - ) - ) - } - - is HendelseModel.OppgaveUtgått -> - repository.slettOmEldre( - oppgaveId = hendelse.aggregateId, - utgaattTidspunkt = hendelse.utgaattTidspunkt.asOsloLocalDate() - ) - - is HendelseModel.OppgaveUtført -> - repository.slettOppgave(aggregateId = hendelse.aggregateId) - - is HendelseModel.HardDelete -> { - repository.slettOgHuskSlett( - aggregateId = hendelse.aggregateId, - merkelapp = hendelse.merkelapp, - grupperingsid = hendelse.grupperingsid, - ) - } - - is HendelseModel.SoftDelete -> { - repository.slettOgHuskSlett( - aggregateId = hendelse.aggregateId, - merkelapp = hendelse.merkelapp, - grupperingsid = hendelse.grupperingsid, - ) - } - - is HendelseModel.BeskjedOpprettet, - is HendelseModel.KalenderavtaleOpprettet, - is HendelseModel.KalenderavtaleOppdatert, - is HendelseModel.BrukerKlikket, - is HendelseModel.PåminnelseOpprettet, - is HendelseModel.SakOpprettet, - is HendelseModel.NyStatusSak, - is HendelseModel.NesteStegSak, - is HendelseModel.TilleggsinformasjonSak, - is HendelseModel.EksterntVarselFeilet, - is HendelseModel.EksterntVarselKansellert, - is HendelseModel.OppgavePåminnelseEndret, - is HendelseModel.EksterntVarselVellykket -> Unit - } - } - - override suspend fun processingLoopStep() { - sendVedUtgåttFrist() - delay(Duration.ofSeconds(1)) - } - - suspend fun sendVedUtgåttFrist(now: LocalDate = osloTid.localDateNow()) { - val utgåttFrist = repository.hentOgFjernAlleMedFrist(now) +) { + suspend fun settOppgaverUtgåttBasertPåFrist(now: LocalDate = osloTid.localDateNow()) { + val utgåttFrist = repository.hentOgFjernAlleUtgåtteOppgaver(now) utgåttFrist.forEach { utgått -> val fristLocalDateTime = LocalDateTime.of(utgått.frist, LocalTime.MAX) hendelseProdusent.send(HendelseModel.OppgaveUtgått( @@ -125,4 +34,31 @@ class SkedulertUtgåttService( )) } } + + suspend fun settKalenderavtalerAvholdtBasertPåTidspunkt(now: LocalDateTime = osloTid.localDateTimeNow()) { + repository.hentOgFjernAlleAvholdteKalenderavtaler(now).forEach { avholdt -> + hendelseProdusent.send(HendelseModel.KalenderavtaleOppdatert( + virksomhetsnummer = avholdt.virksomhetsnummer, + notifikasjonId = avholdt.kalenderavtaleId, + hendelseId = UUID.randomUUID(), + produsentId = avholdt.produsentId, + kildeAppNavn = NaisEnvironment.clientId, + merkelapp = avholdt.merkelapp, + grupperingsid = avholdt.grupperingsid, + opprettetTidspunkt = avholdt.opprettetTidspunkt, + oppdatertTidspunkt = Instant.now(), + tilstand = HendelseModel.KalenderavtaleTilstand.AVHOLDT, + lenke = null, + tekst = null, + startTidspunkt = null, + sluttTidspunkt = null, + lokasjon = null, + erDigitalt = null, + påminnelse = null, + idempotenceKey = null, + hardDelete = null, + eksterneVarsler = emptyList(), + )) + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/statistikk/StatistikkModel.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/statistikk/StatistikkModel.kt index 5662bc75a..487e6bf93 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/statistikk/StatistikkModel.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/statistikk/StatistikkModel.kt @@ -29,7 +29,6 @@ import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.SmsVarselKontakti import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.SoftDelete import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.TilleggsinformasjonSak import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.basedOnEnv import no.nav.arbeidsgiver.notifikasjon.infrastruktur.logger import java.security.MessageDigest import java.time.ZoneOffset.UTC diff --git a/app/src/main/resources/bruker.graphql b/app/src/main/resources/bruker.graphql index fdc83b42e..8b0d43706 100644 --- a/app/src/main/resources/bruker.graphql +++ b/app/src/main/resources/bruker.graphql @@ -218,6 +218,7 @@ enum KalenderavtaleTilstand { ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED ARBEIDSGIVER_HAR_GODTATT AVLYST + AVHOLDT } type Virksomhet { diff --git a/app/src/main/resources/db/migration/skedulert_utgatt_model/V1__init.sql b/app/src/main/resources/db/migration/skedulert_utgatt_model/V1__init.sql new file mode 100644 index 000000000..470ad443c --- /dev/null +++ b/app/src/main/resources/db/migration/skedulert_utgatt_model/V1__init.sql @@ -0,0 +1,70 @@ +create table oppgave +( + oppgave_id uuid not null primary key, + frist text not null, + virksomhetsnummer text not null, + produsent_id text not null +); + +create index oppgave_frist_idx on oppgave (frist); + +create table oppgave_sak_kobling +( + oppgave_id uuid not null primary key, + sak_id uuid not null +); + +create table kalenderavtale +( + kalenderavtale_id uuid not null primary key, + start_tidspunkt text not null, + tilstand text not null, + virksomhetsnummer text not null, + produsent_id text not null, + merkelapp text not null, + grupperingsid text not null, + opprettet_tidspunkt text not null +); + +create index kalenderavtale_starttidspunkt_idx on kalenderavtale (start_tidspunkt); + +create table kalenderavtale_sak_kobling +( + kalenderavtale_id uuid not null primary key, + sak_id uuid not null +); + +create table skedulert_utgatt +( + aggregat_id uuid not null primary key, + aggregat_type text not null +); + + +create table hard_deleted_aggregates +( + aggregate_id uuid not null primary key +); + +create table hard_deleted_aggregates_metadata +( + aggregate_id uuid not null references hard_deleted_aggregates (aggregate_id) on delete cascade, + aggregate_type text not null, + grupperingsid text, + merkelapp text +); + +create index on hard_deleted_aggregates_metadata (aggregate_type, grupperingsid, merkelapp); + + +-- denne tabellen er en koblingstabell mellom sak og notifikasjon +-- det at noe ligger i den betyr ikke at det er slettet, men brukes for å sjekke cascade delete +create table hard_delete_sak_til_notifikasjon_kobling +( + sak_id uuid not null, + notifikasjon_id uuid not null +); + + +create unique index hard_delete_sak_til_notifikasjon_kobling_uniq + on hard_delete_sak_til_notifikasjon_kobling (sak_id, notifikasjon_id) \ No newline at end of file diff --git a/app/src/main/resources/produsent.graphql b/app/src/main/resources/produsent.graphql index 4c047c792..c8dfb9d89 100644 --- a/app/src/main/resources/produsent.graphql +++ b/app/src/main/resources/produsent.graphql @@ -1883,6 +1883,10 @@ enum KalenderavtaleTilstand { Avtalen er avlyst """ AVLYST + """ + Avtalen er avholdt, dette skjer automatisk når starttidspunkt har passert, men kan også settes manuelt + """ + AVHOLDT } union NyOppgaveResultat = diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/QueryKommendeKalenderavtalerTests.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/QueryKommendeKalenderavtalerTests.kt index e56c77ba0..9a363f658 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/QueryKommendeKalenderavtalerTests.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/QueryKommendeKalenderavtalerTests.kt @@ -33,7 +33,7 @@ class QueryKommendeKalenderavtalerTests: DescribeSpec({ grupperingsid = grupperingsid, ) val sak2 = brukerRepository.sakOpprettet( - sakId = uuid("1"), + sakId = uuid("2"), mottakere = listOf(TEST_MOTTAKER_2), virksomhetsnummer = TEST_VIRKSOMHET_2, merkelapp = merkelapp, diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/QuerySakerTidslinjeTest.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/QuerySakerTidslinjeTest.kt index 9d0c6ad77..48b215f95 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/QuerySakerTidslinjeTest.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/QuerySakerTidslinjeTest.kt @@ -160,9 +160,9 @@ class QuerySakerTidslinjeTest: DescribeSpec({ it.startTidspunkt.toInstant() shouldBe kalenderavtale.startTidspunkt.atOslo().toInstant() it.sluttTidspunkt?.toInstant() shouldBe kalenderavtale.sluttTidspunkt?.atOslo()?.toInstant() it.lokasjon.shouldNotBeNull() - it.lokasjon.adresse shouldBe kalenderavtale.lokasjon!!.adresse - it.lokasjon.poststed shouldBe kalenderavtale.lokasjon.poststed - it.lokasjon.postnummer shouldBe kalenderavtale.lokasjon.postnummer + it.lokasjon!!.adresse shouldBe kalenderavtale.lokasjon!!.adresse + it.lokasjon!!.poststed shouldBe kalenderavtale.lokasjon!!.poststed + it.lokasjon!!.postnummer shouldBe kalenderavtale.lokasjon!!.postnummer it.digitalt shouldBe kalenderavtale.erDigitalt } instanceOf(tidslinje1[1]) { diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/SakerMedOppgaveTilstandTests.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/SakerMedOppgaveTilstandTests.kt index 732d9eadb..eaebeca87 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/SakerMedOppgaveTilstandTests.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/bruker/SakerMedOppgaveTilstandTests.kt @@ -96,7 +96,7 @@ class SakerMedOppgaveTilstandTests : DescribeSpec({ repo.opprettOppgave(sak1, LocalDate.parse("2023-05-15")) repo.opprettOppgave(sak1, LocalDate.parse("2023-01-15")).also { repo.oppgaveTilstandUtgått(it) } - val sak2 = repo.opprettSak("2").also { sak -> + repo.opprettSak("2").also { sak -> repo.opprettOppgave(sak, LocalDate.parse("2023-01-15")).also { repo.oppgaveTilstandUtført(it) } repo.opprettOppgave(sak, LocalDate.parse("2023-05-15")).also { repo.oppgaveTilstandUtført(it) } repo.opprettOppgave(sak, LocalDate.parse("2023-05-15")).also { repo.oppgaveTilstandUtgått(it) } @@ -105,6 +105,7 @@ class SakerMedOppgaveTilstandTests : DescribeSpec({ HendelseModel.KalenderavtaleTilstand.ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED, HendelseModel.KalenderavtaleTilstand.ARBEIDSGIVER_HAR_GODTATT, HendelseModel.KalenderavtaleTilstand.AVLYST, + HendelseModel.KalenderavtaleTilstand.AVHOLDT, )) { repo.kalenderavtaleOpprettet( sakId = sak.sakId, diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/api/NyStatusSakTests.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/api/NyStatusSakTests.kt index 9e5b93408..e6321a09c 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/api/NyStatusSakTests.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/produsent/api/NyStatusSakTests.kt @@ -76,13 +76,27 @@ class NyStatusSakTests : DescribeSpec({ val r6 = engine.nyStatusSak( id = sakId, SaksStatus.FERDIG, - hardDelete = LocalDateTime.MAX + hardDelete = LocalDateTime.MAX.minusDays(1) ) it("vellyket oppdatering med harddelete satt i kafka") { r6.getTypedContent("$.nyStatusSak.__typename") shouldBe "NyStatusSakVellykket" val hendelse = stubbedKafkaProducer.hendelser.last() hendelse should beInstanceOf() (hendelse as HendelseModel.NyStatusSak).hardDelete shouldNotBe null + hendelse.hardDelete!!.nyTid.denOrNull() shouldBe LocalDateTime.MAX.minusDays(1) + } + + val r7 = engine.nyStatusSak( + id = sakId, + SaksStatus.FERDIG, + hardDelete = LocalDateTime.MAX + ) + it("vellyket oppdatering med harddelete satt i kafka") { + r7.getTypedContent("$.nyStatusSak.__typename") shouldBe "NyStatusSakVellykket" + val hendelse = stubbedKafkaProducer.hendelser.last() + hendelse should beInstanceOf() + (hendelse as HendelseModel.NyStatusSak).hardDelete shouldNotBe null + hendelse.hardDelete!!.nyTid.denOrNull() shouldBe LocalDateTime.MAX } } }) diff --git "a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/FristUtsattTests.kt" "b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/FristUtsattTests.kt" index 389a00b7e..44d351f9e 100644 --- "a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/FristUtsattTests.kt" +++ "b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/FristUtsattTests.kt" @@ -6,9 +6,9 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.OppgaveUtgått -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.PartitionHendelseMetadata import no.nav.arbeidsgiver.notifikasjon.tid.asOsloLocalDate import no.nav.arbeidsgiver.notifikasjon.util.FakeHendelseProdusent +import no.nav.arbeidsgiver.notifikasjon.util.testDatabase import no.nav.arbeidsgiver.notifikasjon.util.uuid import java.time.LocalDate import java.time.LocalTime.MIDNIGHT @@ -127,183 +127,203 @@ private val softDeleteSak = HendelseModel.SoftDelete( class FristUtsattTests: DescribeSpec({ - val metadata = PartitionHendelseMetadata(0, 0) describe("Frist utsatt på oppgave uten frist") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetUtenFrist, metadata) - service.processHendelse(fristUtsatt, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetUtenFrist) + repo.oppdaterModellEtterHendelse(fristUtsatt) it("Sender ett oppgaveUtgått-event basert på utsatt frist") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 1 - hendelseProdusent.hendelser[0].shouldBeInstanceOf().let { + hendelseProdusent.hendelser[0].shouldBeInstanceOf().also { it.utgaattTidspunkt.asOsloLocalDate() shouldBe fristUtsatt.frist } } } describe("Frist utsatt på oppgave med eksisterende frist") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(fristUtsatt, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(fristUtsatt) it("Sender ett oppgaveUtgått-event basert på utsatt frist") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 1 - hendelseProdusent.hendelser[0].shouldBeInstanceOf().let { + hendelseProdusent.hendelser[0].shouldBeInstanceOf().also { it.utgaattTidspunkt.asOsloLocalDate() shouldBe fristUtsatt.frist } } } describe("Frist utsatt parallelt med oppgaveUtgått, hvor utsettelse kommer først") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(fristUtsatt, metadata) - service.processHendelse(oppgaveUtgått, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(fristUtsatt) + repo.oppdaterModellEtterHendelse(oppgaveUtgått) + it("Sender ett oppgaveUtgått-event for utsatt frist") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 1 - hendelseProdusent.hendelser[0].shouldBeInstanceOf().let { + hendelseProdusent.hendelser[0].shouldBeInstanceOf().also { it.utgaattTidspunkt.asOsloLocalDate() shouldBe fristUtsatt.frist } } } describe("Frist utsatt parallelt med oppgaveUtgått, hvor utgått kommer først") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(oppgaveUtgått, metadata) - service.processHendelse(fristUtsatt, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(oppgaveUtgått) + repo.oppdaterModellEtterHendelse(fristUtsatt) + it("Sender ett oppgaveUtgått-event for utsatt frist") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 1 - hendelseProdusent.hendelser[0].shouldBeInstanceOf().let { + hendelseProdusent.hendelser[0].shouldBeInstanceOf().also { it.utgaattTidspunkt.asOsloLocalDate() shouldBe fristUtsatt.frist } } } describe("Frist utsatt parallelt med oppgave utført, hvor utsettelse kommer først") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(fristUtsatt, metadata) - service.processHendelse(oppgaveUtført, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(fristUtsatt) + repo.oppdaterModellEtterHendelse(oppgaveUtført) + it("Skal ikke sende noen event, siden oppgaven er utført") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 0 } } describe("Frist utsatt parallelt med oppgave utført, hvor utført kommer først") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(fristUtsatt, metadata) - service.processHendelse(oppgaveUtført, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(fristUtsatt) + repo.oppdaterModellEtterHendelse(oppgaveUtført) + it("Skal ikke sende noen event, siden oppgaven er utført") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 0 } } describe("Softdelete på en oppgave med utsatt frist, soft-delete først") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(softDelete, metadata) - service.processHendelse(fristUtsatt, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(softDelete) + repo.oppdaterModellEtterHendelse(fristUtsatt) + it("Skal ikke sende noen event, siden oppgaven er soft-deleted") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 0 } } + describe("Softdelete på en oppgave med utsatt frist, soft-delete sist") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(fristUtsatt, metadata) - service.processHendelse(softDelete, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(fristUtsatt) + repo.oppdaterModellEtterHendelse(softDelete) + it("Skal ikke sende noen event, siden oppgaven er soft-deleted") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 0 } } describe("Harddelete på en oppgave med utsatt frist, hard-delete først") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(hardDelete, metadata) - service.processHendelse(fristUtsatt, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(hardDelete) + repo.oppdaterModellEtterHendelse(fristUtsatt) + it("Skal ikke sende noen event, siden oppgaven er hard-deleted") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 0 } } describe("Harddelete på en oppgave med utsatt frist, hard-delete sist") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(fristUtsatt, metadata) - service.processHendelse(hardDelete, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(fristUtsatt) + repo.oppdaterModellEtterHendelse(hardDelete) + it("Skal ikke sende noen event, siden oppgaven er hard-deleted") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 0 } } describe("Harddelete på grupperingsid") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(fristUtsatt, metadata) - service.processHendelse(hardDeleteSak, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(fristUtsatt) + repo.oppdaterModellEtterHendelse(hardDeleteSak) + it("Skal ikke sende noen event, siden saken er hard-deleted") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 0 } } describe("Harddelete på grupperingsid, mottatt før saken er opprettet") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(hardDeleteSak, metadata) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(fristUtsatt, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(hardDeleteSak) + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(fristUtsatt) + it("Skal ikke sende noen event, siden saken er hard-deleted") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 0 } } describe("Softdelete på grupperingsid") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(fristUtsatt, metadata) - service.processHendelse(softDeleteSak, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(fristUtsatt) + repo.oppdaterModellEtterHendelse(softDeleteSak) + it("Skal ikke sende noen event, siden saken er soft-deleted") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 0 } } describe("Softdelete på grupperingsid, mottatt før saken er opprettet") { - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) - service.processHendelse(softDeleteSak, metadata) - service.processHendelse(oppgaveOpprettetMedFrist, metadata) - service.processHendelse(fristUtsatt, metadata) + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(softDeleteSak) + repo.oppdaterModellEtterHendelse(oppgaveOpprettetMedFrist) + repo.oppdaterModellEtterHendelse(fristUtsatt) + it("Skal ikke sende noen event, siden saken er soft-deleted") { - service.sendVedUtgåttFrist(now = utsattFrist.plusDays(1)) + service.settOppgaverUtgåttBasertPåFrist(now = utsattFrist.plusDays(1)) hendelseProdusent.hendelser shouldHaveSize 0 } } -}) \ No newline at end of file +}) + +private fun DescribeSpec.setupTestApp(): Triple { + val database = testDatabase(SkedulertUtgått.databaseConfig) + val repo = SkedulertUtgåttRepository(database) + val hendelseProdusent = FakeHendelseProdusent() + val service = SkedulertUtgåttService(repo, hendelseProdusent) + return Triple(repo, hendelseProdusent, service) +} \ No newline at end of file diff --git "a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/KalenderavtaleAvholdtTests.kt" "b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/KalenderavtaleAvholdtTests.kt" new file mode 100644 index 000000000..6aac7d598 --- /dev/null +++ "b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/KalenderavtaleAvholdtTests.kt" @@ -0,0 +1,245 @@ +package no.nav.arbeidsgiver.notifikasjon.skedulert_utgått + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.datatest.withData +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel +import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.KalenderavtaleTilstand.* +import no.nav.arbeidsgiver.notifikasjon.util.FakeHendelseProdusent +import no.nav.arbeidsgiver.notifikasjon.util.testDatabase +import no.nav.arbeidsgiver.notifikasjon.util.uuid +import java.time.Instant +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.util.* + +class KalenderavtaleAvholdtTests : DescribeSpec({ + val localDateTimeNow = LocalDateTime.now() + val tidspunktSomHarPassert = localDateTimeNow.minusHours(1) + val tidspunktSomIkkeHarPassert = localDateTimeNow.plusHours(1) + + val sakOpprettet = HendelseModel.SakOpprettet( + hendelseId = uuid("1"), + virksomhetsnummer = "1", + produsentId = "", + kildeAppNavn = "", + sakId = uuid("1"), + grupperingsid = "42", + merkelapp = "test", + mottakere = listOf( + HendelseModel.AltinnMottaker( + virksomhetsnummer = "1", + serviceCode = "1", + serviceEdition = "1" + ) + ), + tittel = "test", + tilleggsinformasjon = null, + lenke = "#test", + oppgittTidspunkt = OffsetDateTime.now(), + mottattTidspunkt = OffsetDateTime.now(), + nesteSteg = null, + hardDelete = null, + ) + val opprettet = HendelseModel.KalenderavtaleOpprettet( + virksomhetsnummer = sakOpprettet.virksomhetsnummer, + merkelapp = sakOpprettet.merkelapp, + eksternId = "42", + mottakere = sakOpprettet.mottakere, + hendelseId = uuid("2"), + notifikasjonId = uuid("2"), + tekst = "test", + lenke = "#test", + opprettetTidspunkt = OffsetDateTime.now(), + kildeAppNavn = "", + produsentId = "", + grupperingsid = "42", + eksterneVarsler = listOf(), + hardDelete = null, + sakId = sakOpprettet.sakId, + påminnelse = null, + tilstand = HendelseModel.KalenderavtaleTilstand.VENTER_SVAR_FRA_ARBEIDSGIVER, + startTidspunkt = localDateTimeNow, + sluttTidspunkt = null, + lokasjon = null, + erDigitalt = false, + ) + val oppdatert = HendelseModel.KalenderavtaleOppdatert( + virksomhetsnummer = sakOpprettet.virksomhetsnummer, + merkelapp = sakOpprettet.merkelapp, + hendelseId = uuid("3"), + notifikasjonId = uuid("2"), + tekst = "test", + lenke = "#test", + opprettetTidspunkt = Instant.now(), + oppdatertTidspunkt = opprettet.opprettetTidspunkt.toInstant(), + kildeAppNavn = "", + produsentId = "", + grupperingsid = "42", + eksterneVarsler = listOf(), + hardDelete = null, + påminnelse = null, + tilstand = HendelseModel.KalenderavtaleTilstand.VENTER_SVAR_FRA_ARBEIDSGIVER, + startTidspunkt = localDateTimeNow, + sluttTidspunkt = null, + lokasjon = null, + erDigitalt = false, + idempotenceKey = null, + ) + + describe("noop når starttidspunkt ikke har passert enda") { + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(opprettet.copy(startTidspunkt = tidspunktSomIkkeHarPassert)) + service.settKalenderavtalerAvholdtBasertPåTidspunkt(now = localDateTimeNow) + + hendelseProdusent.hendelser shouldBe emptyList() + } + + describe("noop når aggregat er fjernet") { + val (repo, hendelseProdusent, service) = setupTestApp() + withData( + listOf( + HendelseModel.HardDelete( + aggregateId = opprettet.aggregateId, + virksomhetsnummer = opprettet.virksomhetsnummer, + hendelseId = UUID.randomUUID(), + produsentId = opprettet.virksomhetsnummer, + kildeAppNavn = opprettet.virksomhetsnummer, + deletedAt = OffsetDateTime.now(), + grupperingsid = null, + merkelapp = opprettet.merkelapp, + ), + opprettet.copy(startTidspunkt = tidspunktSomHarPassert), + oppdatert.copy(startTidspunkt = tidspunktSomHarPassert), + ) + ) { + + repo.oppdaterModellEtterHendelse(it) + service.settKalenderavtalerAvholdtBasertPåTidspunkt(now = localDateTimeNow) + hendelseProdusent.hendelser shouldBe emptyList() + } + } + + describe("noop når status er avholdt elelr avlyst") { + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(opprettet.copy( + notifikasjonId = uuid("11"), + startTidspunkt = tidspunktSomHarPassert, + tilstand = AVLYST + )) + service.settKalenderavtalerAvholdtBasertPåTidspunkt(now = localDateTimeNow) + hendelseProdusent.hendelser shouldBe emptyList() + + repo.oppdaterModellEtterHendelse(opprettet.copy( + notifikasjonId = uuid("22"), + startTidspunkt = tidspunktSomHarPassert, + tilstand = AVHOLDT + )) + service.settKalenderavtalerAvholdtBasertPåTidspunkt(now = localDateTimeNow) + hendelseProdusent.hendelser shouldBe emptyList() + } + + describe("Skedulerer avholdt når starttidspunkt har passert") { + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(opprettet.copy(startTidspunkt = tidspunktSomHarPassert)) + service.settKalenderavtalerAvholdtBasertPåTidspunkt(now = localDateTimeNow) + + hendelseProdusent.hendelser.first().also { + it as HendelseModel.KalenderavtaleOppdatert + it.tilstand shouldBe AVHOLDT + } + } + + describe("Skedulerer avholdt når starttidspunkt har passert og det finnes et starttidspunkt på kø som ikke har passert") { + /** + * denne testen passer på at vi klarer å skille på forskjellige aggregater + */ + + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse( + opprettet.copy( + notifikasjonId = uuid("11"), + startTidspunkt = tidspunktSomIkkeHarPassert + ) + ) + repo.oppdaterModellEtterHendelse( + opprettet.copy( + notifikasjonId = uuid("22"), + startTidspunkt = tidspunktSomHarPassert + ) + ) + service.settKalenderavtalerAvholdtBasertPåTidspunkt(now = localDateTimeNow) + + hendelseProdusent.hendelser shouldHaveSize 1 + hendelseProdusent.hendelser.first().also { + it as HendelseModel.KalenderavtaleOppdatert + it.tilstand shouldBe AVHOLDT + it.aggregateId shouldBe uuid("22") + } + } + + describe("skedulering fjernes når kalenderavtale setter til slutt tilstand") { + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(opprettet.copy(startTidspunkt = tidspunktSomHarPassert)) + repo.oppdaterModellEtterHendelse(oppdatert.copy(tilstand = AVLYST)) + service.settKalenderavtalerAvholdtBasertPåTidspunkt(now = localDateTimeNow) + + hendelseProdusent.hendelser shouldBe emptyList() + + repo.oppdaterModellEtterHendelse( + opprettet.copy( + notifikasjonId = uuid("42"), + startTidspunkt = tidspunktSomHarPassert + ) + ) + repo.oppdaterModellEtterHendelse( + oppdatert.copy( + notifikasjonId = uuid("42"), + tilstand = AVHOLDT + ) + ) + service.settKalenderavtalerAvholdtBasertPåTidspunkt(now = localDateTimeNow) + + hendelseProdusent.hendelser shouldBe emptyList() + } + + + describe("oppdater etter avholdt fører til ny skedulert avholdt") { + /** + * siden tilstand kan settes via API og vi automatisk setter til avholdt + * så bør vi passe på at oppgaver som gjenåpnes fortsatt settes til avholdt + * tenkt scenario er at lagg i en produsent fører til at vi får en oppdatering på tilstand etter at vi har satt + * avholdt. da bør kalenderavtalen forbli avholdt og ikke ende i en åpen state + */ + + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(opprettet.copy(startTidspunkt = tidspunktSomHarPassert)) + repo.oppdaterModellEtterHendelse(oppdatert.copy(tilstand = AVLYST)) + service.settKalenderavtalerAvholdtBasertPåTidspunkt(now = localDateTimeNow) + hendelseProdusent.hendelser shouldBe emptyList() + + repo.oppdaterModellEtterHendelse(oppdatert.copy(tilstand = ARBEIDSGIVER_HAR_GODTATT)) + service.settKalenderavtalerAvholdtBasertPåTidspunkt(now = localDateTimeNow) + + hendelseProdusent.hendelser.first().also { + it as HendelseModel.KalenderavtaleOppdatert + it.tilstand shouldBe AVHOLDT + } + } + +}) + +private fun DescribeSpec.setupTestApp(): Triple { + val database = testDatabase(SkedulertUtgått.databaseConfig) + val repo = SkedulertUtgåttRepository(database) + val hendelseProdusent = FakeHendelseProdusent() + val service = SkedulertUtgåttService(repo, hendelseProdusent) + return Triple(repo, hendelseProdusent, service) +} \ No newline at end of file diff --git "a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttServiceTests.kt" "b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/OppgaveUtg\303\245ttTests.kt" similarity index 71% rename from "app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttServiceTests.kt" rename to "app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/OppgaveUtg\303\245ttTests.kt" index dfd9824e5..111a20329 100644 --- "a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttServiceTests.kt" +++ "b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/OppgaveUtg\303\245ttTests.kt" @@ -7,15 +7,13 @@ import io.kotest.matchers.should import io.kotest.matchers.shouldBe import io.kotest.matchers.types.beInstanceOf import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.PartitionHendelseMetadata import no.nav.arbeidsgiver.notifikasjon.util.FakeHendelseProdusent +import no.nav.arbeidsgiver.notifikasjon.util.testDatabase import no.nav.arbeidsgiver.notifikasjon.util.uuid import java.time.LocalDate import java.time.OffsetDateTime -class SkedulertUtgåttServiceTests : DescribeSpec({ - val hendelseProdusent = FakeHendelseProdusent() - val service = SkedulertUtgåttService(hendelseProdusent) +class OppgaveUtgåttTests : DescribeSpec({ val oppgaveOpprettet = HendelseModel.OppgaveOpprettet( virksomhetsnummer = "1", merkelapp = "123", @@ -43,26 +41,22 @@ class SkedulertUtgåttServiceTests : DescribeSpec({ ) val fristSomHarPassert = LocalDate.now().minusDays(1) val fristSomIkkeHarPassert = LocalDate.now().plusDays(2) - val metadata = PartitionHendelseMetadata(0, 0) describe("Skedulerer utgått når frist har passert") { - hendelseProdusent.clear() - service.processHendelse(oppgaveOpprettet.copy(frist = fristSomHarPassert), metadata) - service.sendVedUtgåttFrist() + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettet.copy(frist = fristSomHarPassert)) + service.settOppgaverUtgåttBasertPåFrist() hendelseProdusent.hendelser.first() should beInstanceOf() } + describe("Skedulerer utgått når frist har passert og det finnes en frist på kø som ikke har passert") { - hendelseProdusent.clear() - service.processHendelse( - oppgaveOpprettet.copy(notifikasjonId = uuid("11"), frist = fristSomIkkeHarPassert), - metadata - ) - service.processHendelse( - oppgaveOpprettet.copy(notifikasjonId = uuid("22"), frist = fristSomHarPassert), - metadata - ) - service.sendVedUtgåttFrist() + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettet.copy(notifikasjonId = uuid("11"), frist = fristSomIkkeHarPassert)) + repo.oppdaterModellEtterHendelse(oppgaveOpprettet.copy(notifikasjonId = uuid("22"), frist = fristSomHarPassert)) + service.settOppgaverUtgåttBasertPåFrist() hendelseProdusent.hendelser shouldHaveSize 1 hendelseProdusent.hendelser.first() should beInstanceOf() @@ -70,12 +64,10 @@ class SkedulertUtgåttServiceTests : DescribeSpec({ } describe("noop når frist ikke har passert enda") { - hendelseProdusent.clear() - service.processHendelse( - oppgaveOpprettet.copy(frist = fristSomIkkeHarPassert), - metadata - ) - service.sendVedUtgåttFrist() + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettet.copy(frist = fristSomIkkeHarPassert)) + service.settOppgaverUtgåttBasertPåFrist() hendelseProdusent.hendelser shouldBe emptyList() } @@ -113,16 +105,23 @@ class SkedulertUtgåttServiceTests : DescribeSpec({ nyLenke = null, ) )) { hendelse -> - hendelseProdusent.clear() - service.processHendelse( - oppgaveOpprettet.copy(frist = fristSomHarPassert), - metadata - ) - service.processHendelse(hendelse, metadata) - service.sendVedUtgåttFrist() + val (repo, hendelseProdusent, service) = setupTestApp() + + repo.oppdaterModellEtterHendelse(oppgaveOpprettet.copy(frist = fristSomHarPassert)) + repo.oppdaterModellEtterHendelse(hendelse) + + service.settOppgaverUtgåttBasertPåFrist() hendelseProdusent.hendelser shouldBe emptyList() } } }) + +private fun DescribeSpec.setupTestApp(): Triple { + val database = testDatabase(SkedulertUtgått.databaseConfig) + val repo = SkedulertUtgåttRepository(database) + val hendelseProdusent = FakeHendelseProdusent() + val service = SkedulertUtgåttService(repo, hendelseProdusent) + return Triple(repo, hendelseProdusent, service) +} diff --git "a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttIdempotensTests.kt" "b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttIdempotensTests.kt" index 4b21b2cfc..58f7ac2c1 100644 --- "a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttIdempotensTests.kt" +++ "b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttIdempotensTests.kt" @@ -2,17 +2,18 @@ package no.nav.arbeidsgiver.notifikasjon.skedulert_utgått import io.kotest.core.spec.style.DescribeSpec import io.kotest.datatest.withData -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.PartitionHendelseMetadata import no.nav.arbeidsgiver.notifikasjon.util.EksempelHendelse -import no.nav.arbeidsgiver.notifikasjon.util.NoopHendelseProdusent +import no.nav.arbeidsgiver.notifikasjon.util.testDatabase class SkedulertUtgåttIdempotensTests : DescribeSpec({ - val service = SkedulertUtgåttService(NoopHendelseProdusent) - describe("SkedulertUtgått Idempotent oppførsel") { + val repo = SkedulertUtgåttRepository( + testDatabase(SkedulertUtgått.databaseConfig) + ) + withData(EksempelHendelse.Alle) { hendelse -> - service.processHendelse(hendelse, PartitionHendelseMetadata(0, 0)) - service.processHendelse(hendelse, PartitionHendelseMetadata(0, 0)) + repo.oppdaterModellEtterHendelse(hendelse) + repo.oppdaterModellEtterHendelse(hendelse) } } }) diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/EksempelHendelser.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/EksempelHendelser.kt index e27243891..ab097eb41 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/EksempelHendelser.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/EksempelHendelser.kt @@ -8,6 +8,7 @@ import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.BeskjedOpprettet import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.EksterntVarselSendingsvindu import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.EpostVarselKontaktinfo import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.Hendelse +import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.KalenderavtaleTilstand.AVHOLDT import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.KalenderavtaleTilstand.VENTER_SVAR_FRA_ARBEIDSGIVER import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.NærmesteLederMottaker import no.nav.arbeidsgiver.notifikasjon.hendelse.HendelseModel.SmsVarselKontaktinfo @@ -790,7 +791,7 @@ object EksempelHendelse { ), lenke = "https://foo.no", tekst = "foo", - tilstand = VENTER_SVAR_FRA_ARBEIDSGIVER, + tilstand = AVHOLDT, startTidspunkt = LocalDateTime.now().plusHours(1), sluttTidspunkt = LocalDateTime.now().plusHours(2), lokasjon = HendelseModel.Lokasjon("foo", "bar", "baz"), diff --git a/local-db-init.sql b/local-db-init.sql index 1f1b672d5..dc1a84b6a 100644 --- a/local-db-init.sql +++ b/local-db-init.sql @@ -21,6 +21,9 @@ GRANT ALL PRIVILEGES ON DATABASE "ekstern-varsling-model" TO postgres; CREATE DATABASE "skedulert-harddelete-model"; GRANT ALL PRIVILEGES ON DATABASE "skedulert-harddelete-model" TO postgres; +CREATE DATABASE "skedulert-utgatt-model"; +GRANT ALL PRIVILEGES ON DATABASE "skedulert-utgatt-model" TO postgres; + CREATE DATABASE "kafka-backup-model"; GRANT ALL PRIVILEGES ON DATABASE "kafka-backup-model" TO postgres; diff --git a/widget/component/src/NotifikasjonWidget/NotifikasjonPanel/NotifikasjonListeElement/NotifikasjonListeElement.tsx b/widget/component/src/NotifikasjonWidget/NotifikasjonPanel/NotifikasjonListeElement/NotifikasjonListeElement.tsx index 676b9965e..742f9778b 100644 --- a/widget/component/src/NotifikasjonWidget/NotifikasjonPanel/NotifikasjonListeElement/NotifikasjonListeElement.tsx +++ b/widget/component/src/NotifikasjonWidget/NotifikasjonPanel/NotifikasjonListeElement/NotifikasjonListeElement.tsx @@ -222,6 +222,26 @@ export const NotifikasjonListeElement = (props: Props) => { } /> ); + case KalenderavtaleTilstand.Avholdt: + return ( + + } + tittel={kalenderavtaleTekst(notifikasjon)} + statuslinje={ + + Avholdt + + } + /> + ); default: console.error(`ukjent avtaletilstand ${avtaletilstand}: ignorerer`); return null; From 019fc69600ac7d4376e3cb5ee4c8dd46e6c59df2 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Mon, 13 Jan 2025 15:32:02 +0100 Subject: [PATCH 02/27] =?UTF-8?q?st=C3=B8tt=20avholdt=20i=20testprodusent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test-produsent/package-lock.json | 7 ++++--- test-produsent/src/api/graphql-types.ts | 15 ++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/test-produsent/package-lock.json b/test-produsent/package-lock.json index 6903b9689..5a1c1be2a 100644 --- a/test-produsent/package-lock.json +++ b/test-produsent/package-lock.json @@ -6712,9 +6712,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -6722,6 +6722,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, diff --git a/test-produsent/src/api/graphql-types.ts b/test-produsent/src/api/graphql-types.ts index 43ae14d18..1d3ff2482 100644 --- a/test-produsent/src/api/graphql-types.ts +++ b/test-produsent/src/api/graphql-types.ts @@ -15,9 +15,8 @@ export type Scalars = { /** Dato etter ISO8601-standaren. F.eks. `2020-01-02`, altså 2. mars 2020. */ ISO8601Date: { input: any; output: any; } /** - * DateTime med offset etter ISO8601-standaren. F.eks. '2011-12-03T10:15:30+01:00'. - * - * Er representert som String. + * DateTime etter ISO8601-standaren. F.eks. '2011-12-03T10:15:30+01:00'. + * Dersom tidssone/offset ikke er oppgitt, så antar vi at tidspunktet er Oslo-tid ('Europe/Oslo'). */ ISO8601DateTime: { input: any; output: any; } /** @@ -293,9 +292,9 @@ export type KalenderavtaleData = { /** Merkelapp for kalenderavtalen. Er typisk navnet på ytelse eller lignende. */ merkelapp: Scalars['String']['output']; /** Når avtalen slutter. */ - sluttTidspunkt?: Maybe; + sluttTidspunkt?: Maybe; /** Når avtalen starter. */ - startTidspunkt: Scalars['ISO8601LocalDateTime']['output']; + startTidspunkt: Scalars['ISO8601DateTime']['output']; /** Teksten som vises til brukeren. */ tekst: Scalars['String']['output']; /** @@ -316,6 +315,8 @@ export enum KalenderavtaleTilstand { ArbeidsgiverVilAvlyse = 'ARBEIDSGIVER_VIL_AVLYSE', /** Arbeidsgiver har svart at de ønsker å endre tid eller sted */ ArbeidsgiverVilEndreTidEllerSted = 'ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED', + /** Avtalen er avholdt, dette skjer automatisk når starttidspunkt har passert, men kan også settes manuelt */ + Avholdt = 'AVHOLDT', /** Avtalen er avlyst */ Avlyst = 'AVLYST', /** Avtalen venter på at brukeren skal svare. Dette er standardtilstanden. */ @@ -631,8 +632,8 @@ export type MutationNyKalenderavtaleArgs = { merkelapp: Scalars['String']['input']; mottakere: Array; paaminnelse?: InputMaybe; - sluttTidspunkt?: InputMaybe; - startTidspunkt: Scalars['ISO8601LocalDateTime']['input']; + sluttTidspunkt?: InputMaybe; + startTidspunkt: Scalars['ISO8601DateTime']['input']; tekst: Scalars['String']['input']; tilstand?: InputMaybe; virksomhetsnummer: Scalars['String']['input']; From 4b8f78e75ce387b7a9b2597f5f317084f784489b Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Mon, 13 Jan 2025 15:39:30 +0100 Subject: [PATCH 03/27] Fjern manuelt inkopiert skjema Denne filen skal ikke ligge duplisert her. Filen kopieres med et run script: gql:cp_schema som del av bygget --- fake-produsent-api/server/produsent.graphql | 1992 ------------------- 1 file changed, 1992 deletions(-) delete mode 100644 fake-produsent-api/server/produsent.graphql diff --git a/fake-produsent-api/server/produsent.graphql b/fake-produsent-api/server/produsent.graphql deleted file mode 100644 index b6ebca3fb..000000000 --- a/fake-produsent-api/server/produsent.graphql +++ /dev/null @@ -1,1992 +0,0 @@ -# Denne dokumentasjonen er skrev i "du/dere"-form, hvor du/dere er produsenter, og "vi" er -# notifikasjons-plattformen. - - -directive @Validate on ARGUMENT_DEFINITION -directive @MaxLength(max: Int) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION -directive @NonIdentifying on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION -directive @ExactlyOneFieldGiven on INPUT_OBJECT -directive @MaxValue(upToIncluding: Int) on ARGUMENT_DEFINITION - -""" -DateTime med offset etter ISO8601-standaren. F.eks. '2011-12-03T10:15:30+01:00'. - -Er representert som String. -""" -scalar ISO8601DateTime - -""" -Dato og lokaltid etter ISO8601-standaren. F.eks. '2001-12-24T10:44:01'. -Vi tolker tidspunktet som Oslo-tid ('Europe/Oslo'). -""" -scalar ISO8601LocalDateTime - -""" -Duration ISO8601-standaren. F.eks. 'P2DT3H4M'. - -Er representert som String. -""" -scalar ISO8601Duration - -""" -Dato etter ISO8601-standaren. F.eks. `2020-01-02`, altså 2. mars 2020. -""" -scalar ISO8601Date - -""" -Tilstanden til en oppgave. -""" -enum OppgaveTilstand { - """ - En oppgave som kan utføres. - """ - NY - - """ - En oppgave som allerede er utført. - """ - UTFOERT - - """ - En oppgave hvor frist har utgått. - """ - UTGAATT -} - -""" -Dette er roten som alle forespørsler starter fra. -""" -type Query { - """ - Egnet for feilsøking. Forteller hvem du er autentisert som. - """ - whoami: String - - """ - Vi bruker det Connections-patternet for paginering. Se - [Connection-standaren](https://relay.dev/graphql/connections.htm) for mer - informasjon. - - Dere må gjenta paremetere når dere blar gjennom alle notifikasjonen. - - Hvis verken `merkelapp` eller `merkelapper` er gitt, vil notifikasjoner - med alle dine merkelapper være med. - """ - mineNotifikasjoner( - "antall notifikasjoner du ønsker å hente" - first: Int @Validate @MaxValue(upToIncluding: 10000) - - """Cursor til notifikasjonen du henter fra. Cursor får du fra [NotifikasjonEdge](#notifikasjonedge).""" - after: String - - """Filtrer på merkelapp. Kan ikke brukes sammen med `merkelapper`.""" - merkelapp: String - - """Filtrer på merkelapper. Kan ikke brukes sammen med `merkelapp`.""" - merkelapper: [String!] - - grupperingsid: String - ): MineNotifikasjonerResultat! - - hentNotifikasjon(id: ID!): HentNotifikasjonResultat! - hentSak(id: ID!): HentSakResultat! - hentSakMedGrupperingsid(grupperingsid: String!, merkelapp: String!): HentSakResultat! -} - -union MineNotifikasjonerResultat = - | NotifikasjonConnection - | UgyldigMerkelapp - | UkjentProdusent - -union HentNotifikasjonResultat = - | HentetNotifikasjon - | UkjentProdusent - | UgyldigMerkelapp - | NotifikasjonFinnesIkke - -type HentetNotifikasjon { - notifikasjon: Notifikasjon! -} - -union HentSakResultat = - | HentetSak - | SakFinnesIkke - | UgyldigMerkelapp - | UkjentProdusent - - -type HentetSak { - sak: Sak! -} - -type Sak { - id: ID! - grupperingsid: String! - virksomhetsnummer: String! - tittel: String! - lenke: String - nesteSteg: String - tilleggsinformasjon: String - merkelapp: String! - sisteStatus: SaksStatus -} - -type Virksomhet { - virksomhetsnummer: String! - navn: String! -} - -type NotifikasjonConnection { - edges: [NotifikasjonEdge!]! - pageInfo: PageInfo! -} - -type NotifikasjonEdge { - node: Notifikasjon! - cursor: String! -} - -type PageInfo { - hasNextPage: Boolean! - endCursor: String! -} - -union Notifikasjon = Beskjed | Oppgave | Kalenderavtale - -type Oppgave { - mottaker: Mottaker! - mottakere: [Mottaker!]! - metadata: Metadata! - oppgave: OppgaveData! - eksterneVarsler: [EksterntVarsel!]! -} - -type Beskjed { - mottaker: Mottaker! - mottakere: [Mottaker!]! - metadata: Metadata! - beskjed: BeskjedData! - eksterneVarsler: [EksterntVarsel!]! -} - -type Kalenderavtale { - mottakere: [Mottaker!]! - metadata: Metadata! - kalenderavtale: KalenderavtaleData! - eksterneVarsler: [EksterntVarsel!]! -} - -union Mottaker = - | AltinnMottaker - | NaermesteLederMottaker - -type AltinnMottaker { - serviceCode: String! - serviceEdition: String! - virksomhetsnummer: String! -} - -type NaermesteLederMottaker { - naermesteLederFnr: String! - ansattFnr: String! - virksomhetsnummer: String! -} - -type OppgaveData { - tilstand: OppgaveTilstand - - """ - Merkelapp for beskjeden. Er typisk navnet på ytelse eller lignende. Den vises til brukeren. - """ - merkelapp: String! - - """ - Teksten som vises til brukeren. - """ - tekst: String! - - """ - Lenken som brukeren føres til hvis de klikker på beskjeden. - """ - lenke: String! -} - -type BeskjedData { - """ - Merkelapp for beskjeden. Er typisk navnet på ytelse eller lignende. Den vises til brukeren. - """ - merkelapp: String! - - """ - Teksten som vises til brukeren. - """ - tekst: String! - - """ - Lenken som brukeren føres til hvis de klikker på beskjeden. - """ - lenke: String! -} - -type KalenderavtaleData { - """ - Merkelapp for kalenderavtalen. Er typisk navnet på ytelse eller lignende. - """ - merkelapp: String! - - """ - Teksten som vises til brukeren. - """ - tekst: String! - - """ - Lenken som brukeren føres til hvis de klikker på kalenderavtalen. - """ - lenke: String! - - """ - Når avtalen starter. - """ - startTidspunkt: ISO8601LocalDateTime! - - """ - Når avtalen slutter. - """ - sluttTidspunkt: ISO8601LocalDateTime - - """ - Her kan dere oppgi en fysisk adresse som brukeren kan møte opp på dersom dere har det. - Denne vil vises til brukeren hvis den er angitt. - """ - lokasjon: Lokasjon - - """ - Ved å sette dette flagget kan dere vise brukeren at det er mulig å møte digitalt. - Det vil vises til brukeren hvis det er satt til true. - """ - digitalt: Boolean! - - """ - Tilstanden til avtalen. Default er `VENTER_SVAR_FRA_ARBEIDSGIVER`. - Denne vises til brukeren. - """ - tilstand: KalenderavtaleTilstand -} - -type Metadata { - id: ID! - - """ - """ - eksternId: String! - - """ - """ - opprettetTidspunkt: ISO8601DateTime - - """ - """ - grupperingsid: String - - softDeleted: Boolean! - - softDeletedAt: ISO8601DateTime -} - -type EksterntVarsel { - id: ID! - status: EksterntVarselStatus! -} - -enum EksterntVarselStatus { - NY - SENDT - FEILET - KANSELLERT -} - -union NySakResultat = - | NySakVellykket - | UgyldigMerkelapp - | UgyldigMottaker - | DuplikatGrupperingsid - | DuplikatGrupperingsidEtterDelete - | UkjentProdusent - | UkjentRolle - -type NySakVellykket { - id: ID! -} - -union NyStatusSakResultat = - | NyStatusSakVellykket - | SakFinnesIkke - | Konflikt - | UgyldigMerkelapp - | UkjentProdusent - -type NyStatusSakVellykket { - id: ID! - - """Nyeste statusoppdatering er først i listen.""" - statuser: [StatusOppdatering!]! -} - -union TilleggsinformasjonSakResultat = - | TilleggsinformasjonSakVellykket - | SakFinnesIkke - | Konflikt - | UgyldigMerkelapp - | UkjentProdusent - -union NesteStegSakResultat = - | NesteStegSakVellykket - | SakFinnesIkke - | Konflikt - | UgyldigMerkelapp - | UkjentProdusent - -type TilleggsinformasjonSakVellykket { - id: ID! -} - -type NesteStegSakVellykket { - id: ID! -} - -""" -Statusen påvirker bl.a. hvilket ikon som vises og brukes bl.a. -for å kunne filtrere saksoversikten på min side arbeidsgiver. -""" -enum SaksStatus { - """ - Naturlig start-tilstand for en sak. - - Default tekst som vises til bruker: "Mottatt" - """ - MOTTATT - - """ - Default tekst som vises til bruker: "Under behandling" - """ - UNDER_BEHANDLING - - """ - Slutt-tilstand for en sak. Når en sak er `FERDIG`, så vil vi - nedprioritere visningen av den på min side arbeidsgivere. - - Default tekst som vises til bruker: "Ferdig". - """ - FERDIG -} - -type StatusOppdatering { - status: SaksStatus! - tidspunkt: ISO8601DateTime! - overstyrStatusTekstMed: String -} - -""" -Dette er roten som alle endringer ("mutations") starter fra. Endringer inkluderer også -å opprette nye ting. -""" -type Mutation { - nyKalenderavtale( - """ - Hvilken virksomhet som skal motta kalenderavtalen. - """ - virksomhetsnummer: String! - - """ - Grupperings-id-en knytter denne kalenderavtalen til en sak med samme grupperings-id og merkelapp. - Det vises ikke til brukere. - Saksnummer er en naturlig grupperings-id. - - Når dere bruker grupperings-id, så er det mulig for oss å presentere en tidslinje - med alle notifikasjonene og status-oppdateringer knyttet til en sak. - """ - grupperingsid: String! - - """ - Merkelapp for kalenderavtalen. Er typisk navnet på ytelse eller lignende. Den vises ikke til brukeren, - men brukes i kombinasjon med grupperingsid for å koble kalenderavtalen til sak. - - Hva du kan oppgi som merkelapp er bestemt av produsent-registeret. - """ - merkelapp: String! - - """ - Den eksterne id-en brukes for å unikt identifisere en notifikasjon. Den må være unik for merkelappen. - - Hvis dere har en enkel, statisk bruk av notifikasjoner, så kan dere utlede eksternId - fra f.eks. et saksnummer, og på den måten kunne referere til notifikasjoner dere har opprettet, - uten at dere må lagre ID-ene vi genererer og returnerer til dere. - """ - eksternId: String! - - """ - Teksten som vises til brukeren. - """ - tekst: String! - - """ - Lenken som brukeren føres til hvis de klikker på kalenderavtalen. Typisk en side i deres system som viser detaljer om avtalen. - """ - lenke: String! - - """ - Her bestemmer dere hvem som skal få se kalenderavtalen. - """ - mottakere: [MottakerInput!]! @Validate - - """ - Når avtalen starter. - """ - startTidspunkt: ISO8601LocalDateTime! - - """ - Når avtalen slutter. - """ - sluttTidspunkt: ISO8601LocalDateTime - - """ - Her kan dere oppgi en fysisk adresse som brukeren kan møte opp på dersom dere har det. - Denne vil vises til brukeren hvis den er angitt. - """ - lokasjon: LokasjonInput - - """ - Ved å sette dette flagget kan dere vise brukeren at det er mulig å møte digitalt. - Det vil vises til brukeren hvis det er satt til true. - """ - erDigitalt: Boolean - - """ - Tilstanden til avtalen. Default er `VENTER_SVAR_FRA_ARBEIDSGIVER`. - Denne vises til brukeren. - """ - tilstand: KalenderavtaleTilstand - - eksterneVarsler: [EksterntVarselInput!]! = [] - - """ - Her kan du spesifisere en påminnelse for kalenderavtalen. - Brukeren vil bli gjort oppmerksom via bjellen og evt ekstern varsling dersom du oppgir det. - """ - paaminnelse: PaaminnelseInput - - """ - Oppgi dersom dere ønsker at hard delete skal skeduleres. Vi - tolker relative datoer basert på når vi mottok kallet. - """ - hardDelete: FutureTemporalInput @Validate - ): NyKalenderavtaleResultat! - - nySak( - """ - Grupperings-id-en knytter en sak og notifikasjoner sammen. - Den skal være unik for saker innenfor merkelappen. - Et naturlig valg av grupperingsid er f.eks. et saksnummer. - """ - grupperingsid: String! - - """Merkelapp som saken skal assossieres med.""" - merkelapp: String! - - """Virksomhetsnummeret til virksomheten som saken omhandler.""" - virksomhetsnummer: String! - - """ - Hvem som skal få se saken. - - NB. At en bruker har tilgang til en sak påvirker ikke om de har tilgang - til en notifikasjon. De tilgangsstyres hver for seg. - """ - mottakere: [MottakerInput!]! @Validate - - """En tittel på saken, som vises til brukeren.""" - tittel: String! - - """ - Dette feltet er frivillig. - Tilleggssinformasjon som vises under tittelen på en sak. - Feltet er begrenset til 140 tegn og kan ikke inneholde fødselsnummer. - """ - tilleggsinformasjon: String @Validate @MaxLength(max: 140) @NonIdentifying - - """ - Her oppgir dere en lenke som brukeren kan klikke på for å komme rett til saken. - Dersom lenken ikke oppgis vil saken lenke til en enkel visning av saken i Min side - Arbeidsgiver. - """ - lenke: String - - """ - """ - initiellStatus: SaksStatus! - - """ - Dette feltet er frivillig. - Her har dere mulighet til å vise virksomheten hva som er neste steg i saken. - F.eks. "Saksbehandlignstiden er lang. Du kan forvente refusjon utbetalt i januar 2025." - Dette vises i tidslinjen som en fremtidig handling i saken, over siste oppgave, beskjed eller kalenderavtale. - """ - nesteSteg: String - - """ - Når endringen skjedde. Det kan godt være i fortiden. - Dette feltet er frivillig. Hvis feltet ikke er oppgitt, bruker vi tidspunktet dere gjør - kallet på. - """ - tidspunkt: ISO8601DateTime - - """ - Dette feltet er frivillig. Det lar deg overstyre hvilken tekst vi viser - til brukeren. Se `SaksStatus` for default tekster. - """ - overstyrStatustekstMed: String - - """ - Oppgi dersom dere ønsker at hard delete skal skeduleres. Vi - tolker relative datoer basert på `opprettetTidspunkt` (eller - når vi mottok kallet hvis dere ikke har oppgitt `opprettetTidspunkt`). - """ - hardDelete: FutureTemporalInput @Validate - ): NySakResultat! - - nyStatusSak( - idempotencyKey: String - id: ID! - nyStatus: SaksStatus! - - """ - Når endringen skjedde. Det kan godt være i fortiden. - Dette feltet er frivillig. Hvis feltet ikke er oppgitt, bruker vi tidspunktet dere gjør - kallet på. - """ - tidspunkt: ISO8601DateTime - - """ - Dette feltet er frivillig. Det lar deg overstyre hvilken tekst vi viser - til brukeren. Se `SaksStatus` for default tekster. - """ - overstyrStatustekstMed: String - - """ - Se: [HardDeleteUpdateInput typen](#harddeleteupdateinput) - """ - hardDelete: HardDeleteUpdateInput - - """ - Her kan dere endre lenken til saken. F.eks hvis saken har gått fra å være en - søknad til en kvittering, så kan det være dere har behov for å endre url som brukeren sendes til. - Dette feltet er frivillig; er det ikke oppgitt (eller er null), så forblir lenken knyttet til saken uendret. - """ - nyLenkeTilSak: String - - ): NyStatusSakResultat! - - nyStatusSakByGrupperingsid( - idempotencyKey: String - grupperingsid: String! - merkelapp: String! - - nyStatus: SaksStatus! - - """ - Når endringen skjedde. Det kan godt være i fortiden. - Dette feltet er frivillig. Hvis feltet ikke er oppgitt, bruker vi tidspunktet dere gjør - kallet på. - """ - tidspunkt: ISO8601DateTime - - """ - Dette feltet er frivillig. Det lar deg overstyre hvilken tekst vi viser - til brukeren. Se `SaksStatus` for default tekster. - """ - overstyrStatustekstMed: String - - """ - Se: [HardDeleteUpdateInput typen](#harddeleteupdateinput) - """ - hardDelete: HardDeleteUpdateInput - - """ - Her kan dere endre lenken til saken. F.eks hvis saken har gått fra å være en - søknad til en kvittering, så kan det være dere har behov for å endre url som brukeren sendes til. - Dette feltet er frivillig; er det ikke oppgitt (eller er null), så forblir lenken knyttet til saken uendret. - """ - nyLenkeTilSak: String - ): NyStatusSakResultat! - - tilleggsinformasjonSak( - idempotencyKey: String - id: ID! - - """ - Dette feltet er frivillig. - Her har dere mulighet til å vise virksomheten mer informasjon om saken. - Feltet er begrenset til 140 tegn og kan ikke inneholde fødselsnummer. - """ - tilleggsinformasjon: String @Validate @MaxLength(max: 140) @NonIdentifying - ): TilleggsinformasjonSakResultat! - - tilleggsinformasjonSakByGrupperingsid( - idempotencyKey: String - grupperingsid: String! - merkelapp: String! - - """ - Dette feltet er frivillig. - Her har dere mulighet til å vise virksomheten mer informasjon om saken. - Feltet er begrenset til 140 tegn og kan ikke inneholde fødselsnummer. - """ - tilleggsinformasjon: String @Validate @MaxLength(max: 140) @NonIdentifying - ): TilleggsinformasjonSakResultat! - - - nesteStegSak( - idempotencyKey: String - id: ID! - - """ - Dette feltet er frivillig. - Her har dere mulighet til å vise virksomheten hva som er neste steg i saken. - F.eks. "Saksbehandlignstiden er lang. Du kan forvente refusjon utbetalt i januar 2025." - Dette vises i tidslinjen som en fremtidig handling i saken, over siste oppgave, beskjed eller kalenderavtale. - Her kan dere oppgi verdien null for å fjerne neste steg. - """ - nesteSteg: String - ): NesteStegSakResultat! - - nesteStegSakByGrupperingsid( - idempotencyKey: String - grupperingsid: String! - merkelapp: String! - - """ - Dette feltet er frivillig. - Her har dere mulighet til å vise virksomheten hva som er neste steg i saken. - F.eks. "Saksbehandlignstiden er lang. Du kan forvente refusjon utbetalt i januar 2025." - Dette vises i tidslinjen som en fremtidig handling i saken, over siste oppgave, beskjed eller kalenderavtale. - Her kan dere oppgi verdien null for å fjerne neste steg. - """ - nesteSteg: String - ): NesteStegSakResultat! - - """ - Opprett en ny beskjed. - """ - nyBeskjed(nyBeskjed: NyBeskjedInput! @Validate): NyBeskjedResultat! - - """ - Opprett en ny oppgave. - """ - nyOppgave(nyOppgave: NyOppgaveInput! @Validate): NyOppgaveResultat! - - """ - Marker en oppgave (identifisert ved id) som utført. - """ - oppgaveUtfoert( - """ - ID-en som oppgaven har. Den du fikk da du opprettet oppgaven med `nyOppgave`. - """ - id: ID! - - """ - Se: [HardDeleteUpdateInput typen](#harddeleteupdateinput) - """ - hardDelete: HardDeleteUpdateInput - - """ - Ny lenke som oppgaven peker på: overskriver lenken som ble gitt ved - opprettelse av oppgave. F.eks. for å peke på en kvitterings-side. - - Optional: hvis ikke oppgitt/null, så beholdes forrige lenke. - """ - nyLenke: String = null - - """ - Tidspunkt for når oppgaven ble utført. - - Optional: hvis ikke oppgitt/null, så blir den satt til now(). - """ - utfoertTidspunkt: ISO8601DateTime = null - ): OppgaveUtfoertResultat! - - """ - Marker en oppgave (identifisert ved ekstern id) som utført. - """ - oppgaveUtfoertByEksternId( - """ - Merkelapp som oppgaven er registrert med. - """ - merkelapp: String!, - - """ - ID-en som *dere ga oss* da dere opprettet oppgaven med `nyOppgave`. - """ - eksternId: ID! - - """ - Se: [HardDeleteUpdateInput typen](#harddeleteupdateinput) - """ - hardDelete: HardDeleteUpdateInput - ): OppgaveUtfoertResultat! - @deprecated(reason: - "Using the type ID for `eksternId` can lead to unexpected behaviour. Use oppgaveUtfoertByEksternId_V2 instead." - ) - - """ - Marker en oppgave (identifisert ved ekstern id) som utført. - """ - oppgaveUtfoertByEksternId_V2( - """ - Merkelapp som oppgaven er registrert med. - """ - merkelapp: String!, - - """ - ID-en som *dere ga oss* da dere opprettet oppgaven med `nyOppgave`. - """ - eksternId: String! - - """ - Se: [HardDeleteUpdateInput typen](#harddeleteupdateinput) - """ - hardDelete: HardDeleteUpdateInput - - """ - Ny lenke som oppgaven peker på: overskriver lenken som ble gitt ved - opprettelse av oppgave. F.eks. for å peke på en kvitterings-side. - - Optional: hvis ikke oppgitt/null, så beholdes forrige lenke. - """ - nyLenke: String = null - - """ - Tidspunkt for når oppgaven ble utført. - - Optional: hvis ikke oppgitt/null, så blir den satt til now(). - """ - utfoertTidspunkt: ISO8601DateTime = null - ): OppgaveUtfoertResultat! - - """ - Marker en oppgave (identifisert ved id) som utgått. - """ - oppgaveUtgaatt( - """ - ID-en som oppgaven har. Den du fikk da du opprettet oppgaven med `nyOppgave`. - """ - id: ID! - - """ - Se: [HardDeleteUpdateInput typen](#harddeleteupdateinput) - """ - hardDelete: HardDeleteUpdateInput - - """ - Ny lenke som oppgaven peker på: overskriver lenken som ble gitt ved - opprettelse av oppgave. F.eks. for å peke på en kvitterings-side. - - Optional: hvis ikke oppgitt/null, så beholdes forrige lenke. - """ - nyLenke: String = null - - """ - Tidspunkt for når oppgaven utgikk. - - Optional: hvis ikke oppgitt/null, så blir den satt til now(). - """ - utgaattTidspunkt: ISO8601DateTime = null - ): OppgaveUtgaattResultat! - - - """ - Marker en oppgave (identifisert ved ekstern id) som utgått. - """ - oppgaveUtgaattByEksternId( - """ - Merkelapp som oppgaven er registrert med. - """ - merkelapp: String!, - - """ - ID-en som *dere ga oss* da dere opprettet oppgaven med `nyOppgave`. - """ - eksternId: String! - - """ - Se: [HardDeleteUpdateInput typen](#harddeleteupdateinput) - """ - hardDelete: HardDeleteUpdateInput - - """ - Ny lenke som oppgaven peker på: overskriver lenken som ble gitt ved - opprettelse av oppgave. F.eks. for å peke på en kvitterings-side. - - Optional: hvis ikke oppgitt/null, så beholdes forrige lenke. - """ - nyLenke: String = null - - """ - Tidspunkt for når oppgaven utgikk. - - Optional: hvis ikke oppgitt/null, så blir den satt til now(). - """ - utgaattTidspunkt: ISO8601DateTime = null - ): OppgaveUtgaattResultat! - - """ - Utsett frist på en oppgave. - Dersom oppgaven allerede er utgått så gjenåpnes den og fristen utsettes. - Dersom fristen ikke var utgått, og det tidligere var angitt en påminnelse så vil den påminnelsen bli slettet. - Dersom dere ønsker at brukeren skal få en påminnelse når fristen nærmer seg må det angis i denne mutasjonen. - """ - oppgaveUtsettFrist( - """ - ID-en som oppgaven har. Den du fikk da du opprettet oppgaven med `nyOppgave`. - """ - id: ID! - - """ - Her angir du den nye fristen for når oppgaven skal utføres av bruker. - - Fristen vises til bruker i grensesnittet. - Oppgaven blir automatisk markert som `UTGAAT` når fristen er forbi. - Dere kan kun oppgi frist med dato, og ikke klokkelsett. - Fristen regnes som utløpt når dagen er omme (midnatt, norsk tidssone). - """ - nyFrist: ISO8601Date! - - """ - Her kan du spesifisere en påminnelse for oppgaven. - Brukeren vil bli gjort oppmerksom via bjellen og evt ekstern varsling dersom du oppgir det. - """ - paaminnelse: PaaminnelseInput - ): OppgaveUtsettFristResultat! - - - """ - Utsett frist på en oppgave (identifisert ved ekstern id). - Dersom oppgaven allerede er utgått så gjenåpnes den og fristen utsettes. - Dersom fristen ikke var utgått, og det tidligere var angitt en påminnelse så vil den påminnelsen bli slettet. - Dersom dere ønsker at brukeren skal få en påminnelse når fristen nærmer seg må det angis i denne mutasjonen. - """ - oppgaveUtsettFristByEksternId( - """ - Merkelapp som oppgaven er registrert med. - """ - merkelapp: String!, - - """ - ID-en som *dere ga oss* da dere opprettet oppgaven med `nyOppgave`. - """ - eksternId: String! - - """ - Her angir du den nye fristen for når oppgaven skal utføres av bruker. - - Fristen vises til bruker i grensesnittet. - Oppgaven blir automatisk markert som `UTGAAT` når fristen er forbi. - Dere kan kun oppgi frist med dato, og ikke klokkelsett. - Fristen regnes som utløpt når dagen er omme (midnatt, norsk tidssone). - """ - nyFrist: ISO8601Date! - - """ - Her kan du spesifisere en påminnelse for oppgaven. - Brukeren vil bli gjort oppmerksom via bjellen og evt ekstern varsling dersom du oppgir det. - """ - paaminnelse: PaaminnelseInput - ): OppgaveUtsettFristResultat! - - """ - TODO: skriv beskrivelse - """ - oppgaveEndrePaaminnelse( - """ - ID-en som oppgaven har. Den du fikk da du opprettet oppgaven med `nyOppgave`. - """ - id: ID! - - """ - Her kan du spesifisere en påminnelse for oppgaven. - Brukeren vil bli gjort oppmerksom via bjellen og evt ekstern varsling dersom du oppgir det. - """ - paaminnelse: PaaminnelseInput - ) : OppgaveEndrePaaminnelseResultat - - """ - TODO: skriv beskrivelse - """ - oppgaveEndrePaaminnelseByExternId( - """ - Merkelapp som oppgaven er registrert med. - """ - merkelapp: String!, - - """ - ID-en som *dere ga oss* da dere opprettet oppgaven med `nyOppgave`. - """ - eksternId: String! - - """ - Her kan du spesifisere en påminnelse for oppgaven. - Brukeren vil bli gjort oppmerksom via bjellen og evt ekstern varsling dersom du oppgir det. - """ - paaminnelse: PaaminnelseInput - ) : OppgaveEndrePaaminnelseResultat - - - """ - Oppdater tilstand på en kalenderavtale. - Det er ingen regler tilknyttet endring av tilstand. Dere bestemmer her hvilken tilstand avtalen skal ha. - Den nye tilstanden vises til brukeren. Dette kallet er ment for å oppdatere tilstand samt gi brukeren mer utfyllende informasjon når det blir kjent. - Dette kallet vil anses som vellyket uavhengig av om dataen faktisk er endret. Dvs hvis dere sender inn samme tilstand som allerede er satt, så vil kallet anses som vellykket. - Dette gjelder også hvis dere gjør kallet uten å oppgi noen nye verdier. - """ - oppdaterKalenderavtale( - """ - ID-en som kalenderavtalen har. Den du fikk da du opprettet kalenderavtalen med `nyKalenderavtale`. - """ - id: ID! - - """ - Ny tilstand for kalenderavtalen. - Dersom ny tilstand settes til `AVLYST` vil vi stoppe eksterne varslinger som ikke allerede er sendt, - og kansellere registrerte påminnelser. - """ - nyTilstand: KalenderavtaleTilstand - - """ - Teksten som vises til brukeren. - """ - nyTekst: String - - """ - Lenken som brukeren føres til hvis de klikker på kalenderavtalen. Typisk en side i deres system som viser detaljer om avtalen. - """ - nyLenke: String - - """ - Her kan dere oppgi en fysisk adresse som brukeren kan møte opp på dersom dere har det. - Denne vil vises til brukeren hvis den er angitt. - """ - nyLokasjon: LokasjonInput - - """ - Ved å sette dette flagget kan dere vise brukeren at det er mulig å møte digitalt. - Det vil vises til brukeren hvis det er satt til true. - """ - nyErDigitalt: Boolean - - """ - Se: [HardDeleteUpdateInput typen](#harddeleteupdateinput) - """ - hardDelete: HardDeleteUpdateInput - - """ - Dersom dere angir idempotenceKey så vil konsekvente kall med samme idempotenceKey gi samme resultat. - Dette kan f.eks være nyttig hvis dere har retry mekanismer i deres system. - """ - idempotencyKey: String - - """ - Her kan dere oppgi eksterne varsler som skal sendes i forbindelse med oppdateringen. - Disse varslene vil erstatte tidligere bestilte varsler som ikke er sendt enda. - Vi anbefaler dere å bruke denne i kombinasjon med idempotencyKey for å unngå å sende samme varsel flere ganger. - """ - eksterneVarsler: [EksterntVarselInput!]! = [] - - """ - Her kan du spesifisere en påminnelse for kalenderavtalen. - Brukeren vil bli gjort oppmerksom via bjellen og evt ekstern varsling dersom du oppgir det. - Dersom dere senere oppdaterer kalenderavtalen og setter den til AVLYST, vil vi kansellere registrerte påminnelser. - Vi anbefaler dere å bruke denne i kombinasjon med idempotencyKey. - """ - paaminnelse: PaaminnelseInput - ): OppdaterKalenderavtaleResultat! - - - """ - Oppdater tilstand på en kalenderavtale (identifisert ved ekstern id). - Det er ingen regler tilknyttet endring av tilstand. Dere bestemmer her hvilken tilstand avtalen skal ha. - Den nye tilstanden vises til brukeren. Dette kallet er ment for å oppdatere tilstand samt gi brukeren mer utfyllende informasjon når det blir kjent. - Dette kallet vil anses som vellyket uavhengig av om dataen faktisk er endret. Dvs hvis dere sender inn samme tilstand som allerede er satt, så vil kallet anses som vellykket. - Dette gjelder også hvis dere gjør kallet uten å oppgi noen nye verdier. - """ - oppdaterKalenderavtaleByEksternId( - """ - Merkelapp som kalenderavtalen er registrert med. - """ - merkelapp: String!, - - """ - ID-en som *dere ga oss* da dere opprettet kalenderavtalen med `nyKalenderavtale`. - """ - eksternId: String! - - """ - Ny tilstand for kalenderavtalen. - Dersom ny tilstand settes til `AVLYST` vil vi stoppe eksterne varslinger som ikke allerede er sendt, - og kansellere registrerte påminnelser. - """ - nyTilstand: KalenderavtaleTilstand - - """ - Teksten som vises til brukeren. - """ - nyTekst: String - - """ - Lenken som brukeren føres til hvis de klikker på kalenderavtalen. Typisk en side i deres system som viser detaljer om avtalen. - """ - nyLenke: String - - """ - Her kan dere oppgi en fysisk adresse som brukeren kan møte opp på dersom dere har det. - Denne vil vises til brukeren hvis den er angitt. - """ - nyLokasjon: LokasjonInput - - """ - Ved å sette dette flagget kan dere vise brukeren at det er mulig å møte digitalt. - Det vil vises til brukeren hvis det er satt til true. - """ - nyErDigitalt: Boolean - - """ - Se: [HardDeleteUpdateInput typen](#harddeleteupdateinput) - """ - hardDelete: HardDeleteUpdateInput - - """ - Dersom dere angir idempotenceKey så vil konsekvente kall med samme idempotenceKey gi samme resultat. - Dette kan f.eks være nyttig hvis dere har retry mekanismer i deres system. - """ - idempotencyKey: String - - """ - Her kan dere oppgi eksterne varsler som skal sendes i forbindelse med oppdateringen. - Disse varslene vil erstatte tidligere bestilte varsler som ikke er sendt enda. - Vi anbefaler dere å bruke denn i kombinasjon med idempoencyKey for å unngå å sende samme varsel flere ganger. - """ - eksterneVarsler: [EksterntVarselInput!]! = [] - - """ - Her kan du spesifisere en påminnelse for kalenderavtalen. - Brukeren vil bli gjort oppmerksom via bjellen og evt ekstern varsling dersom du oppgir det. - Dersom dere senere oppdaterer kalenderavtalen og setter den til AVLYST, vil vi kansellere registrerte påminnelser. - Vi anbefaler dere å bruke denne i kombinasjon med idempotencyKey. - """ - paaminnelse: PaaminnelseInput - ): OppdaterKalenderavtaleResultat! - - """ - Markerer en notifikasjon som slettet (soft delete). - - Notifikasjonen vil forsvinne helt for mottakeren: de vil ikke kunne se den på - noen som helst måte — som om notifikasjonen aldri eksisterte. - - For dere (produsenter), så kan dere fortsatt se notifikasjonen i listen over deres notifikasjoner. - - Eventuelle eksterne varsler (SMS, e-post) knyttet til notifikasjonen vil bli fortsatt bli sendt. - - Advarsel: det er ikke mulig å angre på denne operasjonen. - """ - softDeleteNotifikasjon(id: ID!): SoftDeleteNotifikasjonResultat! - - """ - Se dokumentasjon for `softDeleteNotifikasjon(id)`. - """ - softDeleteNotifikasjonByEksternId( - """ - Merkelappen som dere ga oss da dere opprettet notifikasjonen. - """ - merkelapp: String!, - - """ - ID-en som dere ga oss da dere opprettet notifikasjonen. - """ - eksternId: ID! - ): SoftDeleteNotifikasjonResultat! - @deprecated(reason: - "Using the type ID for `eksternId` can lead to unexpected behaviour. Use softDeleteNotifikasjonByEksternId_V2 instead." - ) - - """ - Se dokumentasjon for `softDeleteNotifikasjon(id)`. - """ - softDeleteNotifikasjonByEksternId_V2( - """ - Merkelappen som dere ga oss da dere opprettet notifikasjonen. - """ - merkelapp: String!, - - """ - ID-en som dere ga oss da dere opprettet notifikasjonen. - """ - eksternId: String! - ): SoftDeleteNotifikasjonResultat! - - """ - Sletter en notifikasjon og tilhørende data helt fra databasen og kafka. - Formålet er å støtte juridiske krav om sletting i henhold til personvern. - - Eventuelle eksterne varsler (SMS, e-post) knyttet til notifikasjonen vil bli fortsatt bli sendt. - - Advarsel: det er ikke mulig å angre på denne operasjonen. All data blir borte for godt. - """ - hardDeleteNotifikasjon( - """ - ID-en som notifikasjolnen har. Den du fikk da du opprettet notifikasjonen. - """ - id: ID! - ): HardDeleteNotifikasjonResultat! - - """ - Se dokumentasjon for `hardDeleteNotifikasjon(id)`. - """ - hardDeleteNotifikasjonByEksternId( - """ - Merkelappen som dere ga oss da dere opprettet notifikasjonen. - """ - merkelapp: String! - - """ - ID-en som dere ga oss da dere opprettet notifikasjonen. - """ - eksternId: ID! - ): HardDeleteNotifikasjonResultat! - @deprecated(reason: - "Using the type ID for `eksternId` can lead to unexpected behaviour. Use hardDeleteNotifikasjonByEksternId_V2 instead." - ) - - """ - Se dokumentasjon for `hardDeleteNotifikasjon(id)`. - """ - hardDeleteNotifikasjonByEksternId_V2( - """ - Merkelappen som dere ga oss da dere opprettet notifikasjonen. - """ - merkelapp: String! - - """ - ID-en som dere ga oss da dere opprettet notifikasjonen. - """ - eksternId: String! - ): HardDeleteNotifikasjonResultat! - - """ - Markerer en sak som slettet (soft delete). - - Sak vil forsvinne helt for mottakeren: de vil ikke kunne se den på - noen som helst måte — som om saken aldri eksisterte. - - Advarsel: det er ikke mulig å angre på denne operasjonen. - Advarsel: ingen notifikasjoner blir slettet, selv om de har samme grupperingsid. - """ - softDeleteSak( - """ - ID-en som notifikasjolnen har. Den du fikk da du opprettet notifikasjonen. - """ - id: ID! - ): SoftDeleteSakResultat! - - """ - Se dokumentasjon for `softDeleteSak(id)`. - """ - softDeleteSakByGrupperingsid( - """ - Merkelappen som dere ga oss da dere opprettet saken. - """ - merkelapp: String! - - """ - ID-en som dere ga oss da dere opprettet saken. - """ - grupperingsid: String! - ): SoftDeleteSakResultat! - - """ - Sletter en sak og tilhørende data helt fra databasen og kafka. - Formålet er å støtte juridiske krav om sletting i henhold til personvern. - - Advarsel: det er ikke mulig å angre på denne operasjonen. All data blir borte for godt. - Advarsel: notifikasjoner med samme merkelapp og grupperingsid blir slettet. - Advarsel: Det vil ikke være mulig å lage en ny sak med samme merkelapp og grupperingsid. - """ - hardDeleteSak(id: ID!): HardDeleteSakResultat! - - """ - Se dokumentasjon for `hardDeleteSak(id)`. - """ - hardDeleteSakByGrupperingsid( - """ - Merkelappen som dere ga oss da dere opprettet saken. - """ - merkelapp: String! - - """ - ID-en som dere ga oss da dere opprettet saken. - """ - grupperingsid: String! - ): HardDeleteSakResultat! - - -} - -union SoftDeleteNotifikasjonResultat = - | SoftDeleteNotifikasjonVellykket - | UgyldigMerkelapp - | NotifikasjonFinnesIkke - | UkjentProdusent - -type SoftDeleteNotifikasjonVellykket { - """ - ID-en til oppgaven du "soft-delete"-et. - """ - id: ID! -} - -union HardDeleteNotifikasjonResultat = - | HardDeleteNotifikasjonVellykket - | UgyldigMerkelapp - | NotifikasjonFinnesIkke - | UkjentProdusent - -type HardDeleteNotifikasjonVellykket { - """ - ID-en til oppgaven du "hard-delete"-et. - """ - id: ID! -} - -union SoftDeleteSakResultat = - | SoftDeleteSakVellykket - | UgyldigMerkelapp - | SakFinnesIkke - | UkjentProdusent - -type SoftDeleteSakVellykket { - """ - ID-en til saken du "soft-delete"-et. - """ - id: ID! -} - -union HardDeleteSakResultat = - | HardDeleteSakVellykket - | UgyldigMerkelapp - | SakFinnesIkke - | UkjentProdusent - -type HardDeleteSakVellykket { - """ - ID-en til saken du "hard-delete"-et. - """ - id: ID! -} -union OppgaveUtgaattResultat = - | OppgaveUtgaattVellykket - | UgyldigMerkelapp - | OppgavenErAlleredeUtfoert - | NotifikasjonFinnesIkke - | UkjentProdusent - -type OppgaveUtgaattVellykket { - """ - ID-en til oppgaven du oppdaterte. - """ - id: ID! -} - -union OppgaveUtfoertResultat = - | OppgaveUtfoertVellykket - | UgyldigMerkelapp - | NotifikasjonFinnesIkke - | UkjentProdusent - -type OppgaveUtfoertVellykket { - """ - ID-en til oppgaven du oppdaterte. - """ - id: ID! -} - -union OppgaveUtsettFristResultat = - | OppgaveUtsettFristVellykket - | UgyldigMerkelapp - | Konflikt - | UgyldigPaaminnelseTidspunkt - | NotifikasjonFinnesIkke - | UkjentProdusent - -type OppgaveUtsettFristVellykket { - """ - ID-en til oppgaven du oppdaterte. - """ - id: ID! -} - -type OppgaveEndrePaaminnelseVellykket { - """ - ID-en til oppgaven du oppdaterte. - """ - id: ID! -} - -union OppgaveEndrePaaminnelseResultat = - | OppgaveEndrePaaminnelseVellykket - | UgyldigMerkelapp - | UgyldigPaaminnelseTidspunkt - | OppgavenErAlleredeUtfoert - | NotifikasjonFinnesIkke - | UkjentProdusent - -union OppdaterKalenderavtaleResultat = - | OppdaterKalenderavtaleVellykket - | UgyldigKalenderavtale - | UgyldigMerkelapp - | Konflikt - | NotifikasjonFinnesIkke - | UkjentProdusent - -type OppdaterKalenderavtaleVellykket { - """ - ID-en til kalenderavtalen du oppdaterte. - """ - id: ID! -} - -input NyBeskjedInput { - """ - Se dokumentasjonen til `mottakere`-feltet. - """ - mottaker: MottakerInput - - """ - Her bestemmer dere hvem som skal få se notifikasjonen. - - Hvis dere oppgir en mottaker i `mottaker`-feltet, så tolker vi det som om det var et element - i denne listen over mottakere. - - Dere må gi oss minst 1 mottaker. - """ - mottakere: [MottakerInput!]! = [] - - notifikasjon: NotifikasjonInput! - metadata: MetadataInput! - eksterneVarsler: [EksterntVarselInput!]! = [] -} - -input NyOppgaveInput { - """ - Se dokumentasjonen til `mottakere`-feltet. - """ - mottaker: MottakerInput - - """ - Her bestemmer dere hvem som skal få se notifikasjonen. - - Hvis dere oppgir en mottaker i `mottaker`-feltet, så tolker vi det som om det var et element - i denne listen over mottakere. - - Dere må gi oss minst 1 mottaker. - """ - mottakere: [MottakerInput!]! = [] - notifikasjon: NotifikasjonInput! - - """ - Her kan du spesifisere frist for når oppgaven skal utføres av bruker. - Ideen er at etter fristen, så har ikke bruker lov, eller dere sperret for, - å gjøre oppgaven. - - Fristen vises til bruker i grensesnittet. - Oppgaven blir automatisk markert som `UTGAAT` når fristen er forbi. - Dere kan kun oppgi frist med dato, og ikke klokkelsett. - Fristen regnes som utløpt når dagen er omme (midnatt, norsk tidssone). - - Hvis dere ikke sender med frist, så viser vi ingen frist for bruker, - og oppgaven anses som NY frem til dere markerer oppgaven som `UTFOERT` eller - `UTGAATT`. - """ - frist: ISO8601Date - metadata: MetadataInput! - eksterneVarsler: [EksterntVarselInput!]! = [] - - """ - Her kan du spesifisere en påminnelse for en oppgave. - Brukeren vil bli gjort oppmerksom via bjellen og evt ekstern varsling dersom du oppgir det. - """ - paaminnelse: PaaminnelseInput -} - -input PaaminnelseInput { - """ - Tidspunktet for når påminnelsen skal aktiveres. - Dersom det er angitt frist må påminnelsen være før dette. - - Hvis du sender `eksterneVarsler`, så vil vi sjekke at vi har - mulighet for å sende dem før fristen, ellers får du feil ved - opprettelse av oppgaven/kalenderavtalen. - """ - tidspunkt: PaaminnelseTidspunktInput! - eksterneVarsler: [PaaminnelseEksterntVarselInput!]! = [] -} - -input PaaminnelseTidspunktInput @ExactlyOneFieldGiven { - """ - Konkret tidspunkt - """ - konkret: ISO8601LocalDateTime - """ - Relativ til når oppgaven/kalenderavtalen er angitt som opprettet. Altså X duration etter opprettelse. - Ved utsatt frist er den relativ til tidspunktet fristen ble utsatt. - """ - etterOpprettelse: ISO8601Duration - - """ - Relativ til oppgavens frist, altså X duration før frist. Anses som ugyldig dersom det ikke er en oppgave med frist. - """ - foerFrist: ISO8601Duration - - """ - Relativ til kalenderavtalens startTidspunkt, altså X duration før startTidspunkt. Anses som ugyldig dersom det ikke er en kalenderavtale. - """ - foerStartTidspunkt: ISO8601Duration -} - -input PaaminnelseEksterntVarselInput @ExactlyOneFieldGiven { - sms: PaaminnelseEksterntVarselSmsInput - epost: PaaminnelseEksterntVarselEpostInput - altinntjeneste: PaaminnelseEksterntVarselAltinntjenesteInput -} - -input PaaminnelseEksterntVarselSmsInput { - mottaker: SmsMottakerInput! - """ - Teksten som sendes i SMS-en. - OBS: Det er ikke lov med personopplysninger i teksten. SMS er ikke en sikker kanal. - """ - smsTekst: String! - - """ - Vi sender SMS-en med utgangspunkt i påminnelsestidspunktet, men tar hensyn - til sendingsvinduet. Hvis påminnelsestidspunktet er utenfor vinduet, sender vi - det ved første mulighet. - """ - sendevindu: Sendevindu! -} - -input PaaminnelseEksterntVarselEpostInput { - mottaker: EpostMottakerInput! - """ - Subject/emne til e-posten. - OBS: Det er ikke lov med personopplysninger i teksten. E-post er ikke en sikker kanal. - """ - epostTittel: String! - """ - Kroppen til e-posten. Tolkes som HTML. - OBS: Det er ikke lov med personopplysninger i teksten. E-post er ikke en sikker kanal. - """ - epostHtmlBody: String! - - """ - Vi sender eposten med utgangspunkt i påminnelsestidspunktet, men tar hensyn - til sendingsvinduet. Hvis påminnelsestidspunktet er utenfor vinduet, sender vi - det ved første mulighet. - """ - sendevindu: Sendevindu! -} - -input PaaminnelseEksterntVarselAltinntjenesteInput { - mottaker: AltinntjenesteMottakerInput! - """ - Subject/emne til e-posten, eller tekst i sms - OBS: Det er ikke lov med personopplysninger i teksten. - """ - tittel: String! - """ - Kroppen til e-posten. Dersom det sendes SMS blir dette feltet lagt til i kroppen på sms etter tittel - OBS: Det er ikke lov med personopplysninger i teksten. - """ - innhold: String! - - """ - Vi sender eposten med utgangspunkt i påminnelsestidspunktet, men tar hensyn - til sendingsvinduet. Hvis påminnelsestidspunktet er utenfor vinduet, sender vi - det ved første mulighet. - """ - sendevindu: Sendevindu! -} - -input EksterntVarselInput @ExactlyOneFieldGiven { - sms: EksterntVarselSmsInput - epost: EksterntVarselEpostInput - altinntjeneste: EksterntVarselAltinntjenesteInput -} - -""" -For SMS, så vil -[Altinns varslingsvindu](https://altinn.github.io/docs/utviklingsguider/varsling/#varslingsvindu-for-sms) -også gjelde. Dette burde kun påvirke `LOEPENDE`. -""" -enum Sendevindu { - """ - Vi sender varselet slik at mottaker skal ha mulighet for å - kontakte NAVs kontaktsenter (NKS) når de mottar varselet. Varsler - blir sendt litt før NKS åpner, og vi slutter å sende litt før - NKS stenger. - - Vi tar foreløpig ikke hensyn til røde dager eller produksjonshendelser som fører til - at NKS er utilgjengelig. - """ - NKS_AAPNINGSTID - - - """ - Vi sender varselet på dagtid, mandag til lørdag. - Altså sender vi ikke om kvelden og om natten, og ikke i det hele tatt på søndager. - - Vi tar ikke hensyn til røde dager. - """ - DAGTID_IKKE_SOENDAG - - """ - Vi sender varslet så fort vi kan. - """ - LOEPENDE -} - -""" -Med denne typen velger du når du ønsker at det eksterne varselet blir sendt. -Du skal velge en (og kun en) av feltene, ellers blir forespørselen din avvist -med en feil. -""" -input SendetidspunktInput @ExactlyOneFieldGiven { - """ - Hvis du spesifiserer et tidspunkt på formen "YYYY-MM-DDThh:mm", så sender - vi notifikasjonen på det tidspunktet. Oppgir du et tidspunkt i fortiden, - så sender vi varselet øyeblikkelig. - - Tidspunktet tolker vi som lokal, norsk tid (veggklokke-tid). - """ - tidspunkt: ISO8601LocalDateTime - - sendevindu: Sendevindu -} - - -input EksterntVarselSmsInput { - mottaker: SmsMottakerInput! - """ - Teksten som sendes i SMS-en. - OBS: Det er ikke lov med personopplysninger i teksten. SMS er ikke en sikker kanal. - """ - smsTekst: String! - sendetidspunkt: SendetidspunktInput! -} - -input EksterntVarselEpostInput { - mottaker: EpostMottakerInput! - """ - Subject/emne til e-posten. - OBS: Det er ikke lov med personopplysninger i teksten. E-post er ikke en sikker kanal. - """ - epostTittel: String! - """ - Kroppen til e-posten. Tolkes som HTML. - OBS: Det er ikke lov med personopplysninger i teksten. E-post er ikke en sikker kanal. - """ - epostHtmlBody: String! - sendetidspunkt: SendetidspunktInput! -} - -""" -Med denne typen vil varsel sendes til virksomheten vha tjenesten i Altinn. -Dette vil bli sendt med EMAIL_PREFERRED, som betyr at det mest sannsynlig blir sendt som epost, men i enkelte tilfeller -vil bli sendt sms. - -De som har registrert sin kontaktadresse på underenheten (enten uten filter eller hvor filteret stemmer med tjenestekoden som oppgis) vil bli varslet. -Den offisielle kontaktinformasjonen til overenheten vil bli varslet. - -Malen som benyttes er TokenTextOnly og den ser slik ut: -
-type   | subject  | notificationText
-SMS    |          | {tittel}{innhold}
-EMAIL  | {tittel} | {innhold}
-
-""" -input EksterntVarselAltinntjenesteInput { - mottaker: AltinntjenesteMottakerInput! - """ - Subject/emne til e-posten, eller tekst i sms - OBS: Det er ikke lov med personopplysninger i teksten. - """ - tittel: String! - """ - Kroppen til e-posten. Dersom det sendes SMS blir dette feltet lagt til i kroppen på sms etter tittel - OBS: Det er ikke lov med personopplysninger i teksten. - """ - innhold: String! - sendetidspunkt: SendetidspunktInput! -} - -input SmsMottakerInput @ExactlyOneFieldGiven { - kontaktinfo: SmsKontaktInfoInput -} - -input SmsKontaktInfoInput { - """deprecated. value is ignored. """ - fnr: String - tlf: String! -} - -input EpostMottakerInput @ExactlyOneFieldGiven { - kontaktinfo: EpostKontaktInfoInput -} - -input EpostKontaktInfoInput { - """deprecated. value is ignored. """ - fnr: String - epostadresse: String! -} - -input AltinntjenesteMottakerInput { - serviceCode: String! - serviceEdition: String! -} - -""" -Hvem som skal se notifikasjonen. - -Du kan spesifisere mottaker av notifikasjoner på forskjellige måter. Du skal bruke nøyaktig ett av feltene. - -Vi har implementert det på denne måten fordi GraphQL ikke støtter union-typer som input. -""" -input MottakerInput @ExactlyOneFieldGiven { - altinn: AltinnMottakerInput - naermesteLeder: NaermesteLederMottakerInput -} - -""" -Spesifiser mottaker ved hjelp av tilganger i Altinn. Enhver som har den gitte tilgangen vil -kunne se notifikasjone. - -Tilgangssjekken utføres hver gang en bruker ser på notifikasjoner. Det betyr at hvis en -bruker mister en Altinn-tilgang, så vil de hverken se historiske eller nye notifikasjone knyttet til den Altinn-tilgangen. -Og motsatt, hvis en bruker får en Altinn-tilgang, vil de se tidligere notifikasjoner for den Altinn-tilgangen. -""" -input AltinnMottakerInput { - serviceCode: String! - serviceEdition: String! -} - -""" -Spesifiser mottaker ved hjelp av fødselsnummer. Fødselsnummeret er det til nærmeste leder. Det er kun denne personen -som potensielt kan se notifikasjonen. Det er videre en sjekk for å se om denne personen fortsatt er nærmeste leder -for den ansatte notifikasjonen gjelder. - -Tilgangssjekken utføres hver gang en bruker ønsker se notifikasjonen. -""" -input NaermesteLederMottakerInput { - naermesteLederFnr: String! - ansattFnr: String! -} - - -""" -""" -input NotifikasjonInput { - """ - Merkelapp for beskjeden. Er typisk navnet på ytelse eller lignende. Den vises til brukeren. - - Hva du kan oppgi som merkelapp er bestemt av produsent-registeret. - """ - merkelapp: String! - - """ - Teksten som vises til brukeren. Feltet er begrenset til 300 tegn og kan ikke inneholde fødselsnummer. - """ - tekst: String! @MaxLength(max: 300) @NonIdentifying - - """ - Lenken som brukeren føres til hvis de klikker på beskjeden. - """ - lenke: String! -} - -input MetadataInput { - """ - Hvilken virksomhet som skal motta notifikasjonen. - """ - virksomhetsnummer: String! - - """ - Den eksterne id-en brukes for å unikt identifisere en notifikasjon. Den må være unik for merkelappen. - - Hvis dere har en enkel, statisk bruk av notifikasjoner, så kan dere utlede eksternId - fra f.eks. et saksnummer, og på den måten kunne referere til notifikasjoner dere har opprettet, - uten at dere må lagre ID-ene vi genererer og returnerer til dere. - """ - eksternId: String! - - """ - Hvilken dato vi viser til brukeren. Dersom dere ikke oppgir noen dato, så - bruker vi tidspuktet dere gjør kallet på. - """ - opprettetTidspunkt: ISO8601DateTime - - """ - Grupperings-id-en gjør det mulig å knytte sammen forskjellige oppgaver, beskjed og saker. - Det vises ikke til brukere. - Saksnummer er en naturlig grupperings-id. - - Når dere bruker grupperings-id, så er det mulig for oss å presentere en tidslinje - med alle notifikasjonene og status-oppdateringer knyttet til en sak. - """ - grupperingsid: String - - """ - Oppgi dersom dere ønsker at hard delete skal skeduleres. Vi - tolker relative datoer basert på `opprettetTidspunkt` (eller - når vi mottok kallet hvis dere ikke har oppgitt `opprettetTidspunkt`). - """ - hardDelete: FutureTemporalInput -} - -""" -Med denne kan dere spesifiserer et konkret tidspunkt. -""" -input FutureTemporalInput @ExactlyOneFieldGiven { - """ - En konkret dato. I Europe/Oslo-tidssone. - """ - den: ISO8601LocalDateTime - - """ - Som duration-offset relativt til implisitt dato. Dere må se - på dokumentasjonen til feltet hvor denne datatypen er brukt for - å vite hva vi bruker som implisitt dato. - """ - om: ISO8601Duration -} - -""" -Dersom dere vet at saken/notifikasjonen senere skal slettes helt kan det angis her. -""" -input HardDeleteUpdateInput { - """ - Oppgi dersom dere ønsker at hard delete skal skeduleres. Vi - tolker relative datoer basert på tidspunkt angitt i kallet eller - når vi mottok kallet, hvis dere ikke har oppgitt det eller det - ikke er mulig å oppgi. - """ - nyTid: FutureTemporalInput! - - """ - hvis det finnes fremtidig sletting hvordan skal vi håndtere dette - """ - strategi: NyTidStrategi! -} - -enum NyTidStrategi { - """ - Vi bruker den tiden som er lengst i fremtiden. - """ - FORLENG - - """ - Vi bruker den nye tiden uansett. - """ - OVERSKRIV -} - -input LokasjonInput { - adresse: String! - postnummer: String! - poststed: String! -} - -""" -Tilstanden til en kalenderavtale. Disse tilstandene er laget basert på eksisterende behov. -Har dere behov for flere tilstander, så ta kontakt med oss. -""" -enum KalenderavtaleTilstand { - """ - Avtalen venter på at brukeren skal svare. Dette er standardtilstanden. - """ - VENTER_SVAR_FRA_ARBEIDSGIVER - """ - Arbeidsgiver har svart at de ønsker å avlyse - """ - ARBEIDSGIVER_VIL_AVLYSE - """ - Arbeidsgiver har svart at de ønsker å endre tid eller sted - """ - ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED - """ - Arbeidsgiver har godtatt avtalen - """ - ARBEIDSGIVER_HAR_GODTATT - """ - Avtalen er avlyst - """ - AVLYST -} - -union NyOppgaveResultat = - | NyOppgaveVellykket - | UgyldigMerkelapp - | UgyldigMottaker - | DuplikatEksternIdOgMerkelapp - | UkjentProdusent - | UkjentRolle - | UgyldigPaaminnelseTidspunkt - -union NyBeskjedResultat = - | NyBeskjedVellykket - | UgyldigMerkelapp - | UgyldigMottaker - | DuplikatEksternIdOgMerkelapp - | UkjentProdusent - | UkjentRolle - -union NyKalenderavtaleResultat = - | NyKalenderavtaleVellykket - | UgyldigKalenderavtale - | UgyldigMerkelapp - | UgyldigMottaker - | DuplikatEksternIdOgMerkelapp - | UkjentProdusent - | SakFinnesIkke - -type NyBeskjedVellykket { - id: ID! - eksterneVarsler: [NyEksterntVarselResultat!]! -} - -type NyEksterntVarselResultat { - id: ID! -} - -type NyOppgaveVellykket { - id: ID! - eksterneVarsler: [NyEksterntVarselResultat!]! - paaminnelse: PaaminnelseResultat -} - -type NyKalenderavtaleVellykket { - id: ID! - eksterneVarsler: [NyEksterntVarselResultat!]! - paaminnelse: PaaminnelseResultat -} - -type PaaminnelseResultat { - eksterneVarsler: [NyEksterntVarselResultat!]! -} - -type Lokasjon { - adresse: String! - postnummer: String! - poststed: String! -} - -""" -Denne feilen returneres dersom en produsent forsøker å benytte en merkelapp som den ikke har tilgang til. -""" -type UgyldigMerkelapp implements Error { - feilmelding: String! -} - -""" -Denne feilen returneres dersom en produsent forsøker å benytte en mottaker som den ikke har tilgang til. -""" -type UgyldigMottaker implements Error { - feilmelding: String! -} - -""" -Denne feilen returneres dersom vi ikke greier å finne dere i produsent-registeret vårt. -""" -type UkjentProdusent implements Error { - feilmelding: String! -} - -""" -Denne feilen returneres dersom du forsøker å gå fra utført til utgått. -""" -type OppgavenErAlleredeUtfoert implements Error { - feilmelding: String! -} - -""" -Denne feilen returneres dersom du prøver å referere til en notifikasjon -som ikke eksisterer. - -Utover at dere kan ha oppgitt feil informasjon, så kan det potensielt være på grunn -av "eventual consistency" i systemet vårt. -""" -type NotifikasjonFinnesIkke implements Error { - feilmelding: String! -} - -type SakFinnesIkke implements Error { - feilmelding: String! -} - -""" -Denne feilen returneres dersom du prøver å opprette en notifikasjon med en eksternId og merkelapp som allerede finnes -""" -type DuplikatEksternIdOgMerkelapp implements Error { - feilmelding: String! - idTilEksisterende: ID! -} - -""" -Denne feilen returneres hvis det allerede eksisterer en sak med denne grupperingsid-en under -merkelappen. -""" -type DuplikatGrupperingsid implements Error { - feilmelding: String! - idTilEksisterende: ID! -} - -""" -Denne feilen returneres hvis det tidligere eksisterte en sak med denne grupperingsid-en under -merkelappen, som har blitt slettet. -""" -type DuplikatGrupperingsidEtterDelete implements Error { - feilmelding: String! -} - -type UkjentRolle implements Error { - feilmelding: String! -} - -""" -Tidpunkt for påminnelse er ugyldig iht grenseverdier. F.eks før opprettelse eller etter frist, eller i fortid. -""" -type UgyldigPaaminnelseTidspunkt implements Error { - feilmelding: String! -} - -""" -Oppgitt informasjon samsvarer ikke med tidligere informasjon som er oppgitt. -""" -type Konflikt implements Error { - feilmelding: String! -} - -""" -Kalenderavtalen er ugyldig. Det kan f.eks være at startTidspunkt er etter sluttTidspunkt. Detaljer kommer i feilmelding. -""" -type UgyldigKalenderavtale implements Error { - feilmelding: String! -} - -interface Error { - feilmelding: String! -} From 9f96cdc3c953aaf69ff0e77691468dd038e19b6e Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Mon, 13 Jan 2025 15:43:44 +0100 Subject: [PATCH 04/27] oppdater skjema i widget og ny patch versjon --- widget/brukerapi-mock/package-lock.json | 4 ++-- widget/brukerapi-mock/package.json | 2 +- widget/component/package-lock.json | 4 ++-- widget/component/package.json | 2 +- widget/component/src/api/graphql-types.ts | 1 + 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/widget/brukerapi-mock/package-lock.json b/widget/brukerapi-mock/package-lock.json index 2c2fcdb1e..4c1440a17 100644 --- a/widget/brukerapi-mock/package-lock.json +++ b/widget/brukerapi-mock/package-lock.json @@ -1,12 +1,12 @@ { "name": "@navikt/arbeidsgiver-notifikasjoner-brukerapi-mock", - "version": "7.2.1", + "version": "7.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@navikt/arbeidsgiver-notifikasjoner-brukerapi-mock", - "version": "7.2.1", + "version": "7.2.2", "license": "MIT", "dependencies": { "apollo-server": "^3.10.2", diff --git a/widget/brukerapi-mock/package.json b/widget/brukerapi-mock/package.json index 9e3b3af28..d471ae46f 100644 --- a/widget/brukerapi-mock/package.json +++ b/widget/brukerapi-mock/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "git+https://github.com/navikt/arbeidsgiver-notifikasjon-widget.git" }, - "version": "7.2.1", + "version": "7.2.2", "license": "MIT", "main": "dist/notifikasjonMockMiddleware.js", "files": [ diff --git a/widget/component/package-lock.json b/widget/component/package-lock.json index fbfe58c72..3c9c69065 100644 --- a/widget/component/package-lock.json +++ b/widget/component/package-lock.json @@ -1,12 +1,12 @@ { "name": "@navikt/arbeidsgiver-notifikasjon-widget", - "version": "7.2.1", + "version": "7.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@navikt/arbeidsgiver-notifikasjon-widget", - "version": "7.2.1", + "version": "7.2.2", "dependencies": { "@apollo/client": "^3.10.8", "graphql": "^16.9.0" diff --git a/widget/component/package.json b/widget/component/package.json index 306b78375..0e8e48905 100644 --- a/widget/component/package.json +++ b/widget/component/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "git+https://github.com/navikt/arbeidsgiver-notifikasjon-widget.git" }, - "version": "7.2.1", + "version": "7.2.2", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", "types": "./lib/esm/index.d.ts", diff --git a/widget/component/src/api/graphql-types.ts b/widget/component/src/api/graphql-types.ts index f34da3272..49ed00805 100644 --- a/widget/component/src/api/graphql-types.ts +++ b/widget/component/src/api/graphql-types.ts @@ -76,6 +76,7 @@ export enum KalenderavtaleTilstand { ArbeidsgiverHarGodtatt = 'ARBEIDSGIVER_HAR_GODTATT', ArbeidsgiverVilAvlyse = 'ARBEIDSGIVER_VIL_AVLYSE', ArbeidsgiverVilEndreTidEllerSted = 'ARBEIDSGIVER_VIL_ENDRE_TID_ELLER_STED', + Avholdt = 'AVHOLDT', Avlyst = 'AVLYST', VenterSvarFraArbeidsgiver = 'VENTER_SVAR_FRA_ARBEIDSGIVER' } From cf3c6a5ac9cf9a935acea54ee35bff3970e6dbd4 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Mon, 13 Jan 2025 16:33:36 +0100 Subject: [PATCH 05/27] =?UTF-8?q?flytt=20dato=20ut=20av=20tittel=20p=C3=A5?= =?UTF-8?q?=20kalenderavtale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- widget/component/package-lock.json | 36 +- widget/component/package.json | 1 + .../NotifikasjonListeElement.tsx | 308 +++++++++--------- widget/demo/package-lock.json | 6 +- 4 files changed, 198 insertions(+), 153 deletions(-) diff --git a/widget/component/package-lock.json b/widget/component/package-lock.json index 3c9c69065..bf1923aa7 100644 --- a/widget/component/package-lock.json +++ b/widget/component/package-lock.json @@ -24,6 +24,7 @@ "@types/node": "^20.14.11", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "prettier": "3.4.2", "prop-types": "15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -6473,9 +6474,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -6483,6 +6484,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" @@ -7624,6 +7626,22 @@ "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", "dev": true }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -13708,9 +13726,9 @@ "dev": true }, "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "peer": true }, @@ -14464,6 +14482,12 @@ "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", "dev": true }, + "prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true + }, "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", diff --git a/widget/component/package.json b/widget/component/package.json index 0e8e48905..b42a279e2 100644 --- a/widget/component/package.json +++ b/widget/component/package.json @@ -32,6 +32,7 @@ "@types/node": "^20.14.11", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "prettier": "3.4.2", "prop-types": "15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/widget/component/src/NotifikasjonWidget/NotifikasjonPanel/NotifikasjonListeElement/NotifikasjonListeElement.tsx b/widget/component/src/NotifikasjonWidget/NotifikasjonPanel/NotifikasjonListeElement/NotifikasjonListeElement.tsx index 742f9778b..f3d7856f5 100644 --- a/widget/component/src/NotifikasjonWidget/NotifikasjonPanel/NotifikasjonListeElement/NotifikasjonListeElement.tsx +++ b/widget/component/src/NotifikasjonWidget/NotifikasjonPanel/NotifikasjonListeElement/NotifikasjonListeElement.tsx @@ -1,79 +1,79 @@ -import React, { FC, ReactElement, ReactNode } from 'react'; -import { Next as HoyreChevron, StopWatch } from '@navikt/ds-icons'; -import { BodyShort, Detail, Tag } from '@navikt/ds-react'; +import React, { FC, ReactElement, ReactNode } from 'react' +import { Next as HoyreChevron, StopWatch } from '@navikt/ds-icons' +import { BodyShort, Detail, Tag } from '@navikt/ds-react' import { formatterDato, sendtDatotekst, - uformellDatotekst, -} from '../dato-funksjoner'; + uformellDatotekst +} from '../dato-funksjoner' import { Kalenderavtale, KalenderavtaleTilstand, Notifikasjon, Oppgave, - OppgaveTilstand, -} from '../../../api/graphql-types'; -import { useAmplitude } from '../../../utils/amplitude'; -import './NotifikasjonListeElement.css'; + OppgaveTilstand +} from '../../../api/graphql-types' +import { useAmplitude } from '../../../utils/amplitude' +import './NotifikasjonListeElement.css' import { BeskjedIkon, KalenderavtaleIkon, NyOppgaveIkon, OppgaveUtfortIkon, - OppgaveUtgaattIkon, -} from './Ikoner'; + OppgaveUtgaattIkon +} from './Ikoner' interface Props { - notifikasjon: Notifikasjon; - antall: number; - onKlikketPaaLenke: (notifikasjon: Notifikasjon) => void; - onTabEvent?: (shiftKey: boolean) => void; - gåTilForrige: () => void; - gåTilNeste: () => void; - erValgt: boolean; + notifikasjon: Notifikasjon + antall: number + onKlikketPaaLenke: (notifikasjon: Notifikasjon) => void + onTabEvent?: (shiftKey: boolean) => void + gåTilForrige: () => void + gåTilNeste: () => void + erValgt: boolean } export const NotifikasjonListeElement = (props: Props) => { - const notifikasjon = props.notifikasjon; + const notifikasjon = props.notifikasjon switch (notifikasjon.__typename) { case 'Beskjed': return ( - } + ikon={} tittel={notifikasjon.tekst} visningstidspunkt={new Date(notifikasjon.opprettetTidspunkt)} /> - ); + ) case 'Oppgave': - const tilstand = notifikasjon.tilstand; + const tilstand = notifikasjon.tilstand switch (tilstand) { case OppgaveTilstand.Ny: return ( - } + ikon={} tittel={notifikasjon.tekst} visningstidspunkt={new Date(notifikasjon.opprettetTidspunkt)} statuslinje={} /> - ); + ) case OppgaveTilstand.Utfoert: return ( - } + ikon={} tittel={notifikasjon.tekst} statuslinje={ - + Utført{' '} {notifikasjon.utfoertTidspunkt ? uformellDatotekst(new Date(notifikasjon.utfoertTidspunkt)) @@ -81,70 +81,73 @@ export const NotifikasjonListeElement = (props: Props) => { } /> - ); + ) case OppgaveTilstand.Utgaatt: return ( - } + ikon={} tittel={notifikasjon.tekst} visningstidspunkt={new Date(notifikasjon.opprettetTidspunkt)} statuslinje={ - notifikasjon.frist !== null ? - + notifikasjon.frist !== null ? ( + Fristen gikk ut{' '} {uformellDatotekst(new Date(notifikasjon.utgaattTidspunkt))} - : - - Utgått {uformellDatotekst(new Date(notifikasjon.utgaattTidspunkt))} + ) : ( + + Utgått{' '} + {uformellDatotekst(new Date(notifikasjon.utgaattTidspunkt))} + ) } /> - ); + ) default: - console.error(`ukjent oppgavetilstand ${tilstand}: ignorerer`); - return null; + console.error(`ukjent oppgavetilstand ${tilstand}: ignorerer`) + return null } case 'Kalenderavtale': - const avtaletilstand = notifikasjon.avtaletilstand; - const harPassert = new Date(notifikasjon.startTidspunkt) < new Date(); - + const avtaletilstand = notifikasjon.avtaletilstand + const harPassert = new Date(notifikasjon.startTidspunkt) < new Date() + const tidpunktFormatert = kalenderavtaleTidspunkt(notifikasjon) switch (avtaletilstand) { case KalenderavtaleTilstand.VenterSvarFraArbeidsgiver: return ( - ) : ( ) } - tittel={kalenderavtaleTekst(notifikasjon)} + tittel={notifikasjon.tekst} + undertittel={tidpunktFormatert} statuslinje={ harPassert ? undefined : ( - + Svar på invitasjonen ) } /> - ); + ) case KalenderavtaleTilstand.ArbeidsgiverHarGodtatt: return ( - { title={'Kalenderavtale som du har svart på.'} /> } - tittel={kalenderavtaleTekst(notifikasjon)} + tittel={notifikasjon.tekst} + undertittel={tidpunktFormatert} statuslinje={ - + Du har takket ja } /> - ); + ) case KalenderavtaleTilstand.ArbeidsgiverVilEndreTidEllerSted: return ( - { title={'Kalenderavtale som du har svart på.'} /> } - tittel={kalenderavtaleTekst(notifikasjon)} + tittel={notifikasjon.tekst} + undertittel={tidpunktFormatert} statuslinje={ - + Du ønsker endre tid eller sted } /> - ); + ) case KalenderavtaleTilstand.ArbeidsgiverVilAvlyse: return ( - { title={'Kalenderavtale som du har svart på.'} /> } - tittel={kalenderavtaleTekst(notifikasjon)} + tittel={notifikasjon.tekst} + undertittel={tidpunktFormatert} statuslinje={ - + Du ønsker å avlyse } /> - ); + ) case KalenderavtaleTilstand.Avlyst: return ( - } - tittel={kalenderavtaleTekst(notifikasjon)} + tittel={notifikasjon.tekst} + undertittel={tidpunktFormatert} statuslinje={ - + Avlyst } /> - ); + ) case KalenderavtaleTilstand.Avholdt: return ( - } - tittel={kalenderavtaleTekst(notifikasjon)} + tittel={notifikasjon.tekst} + undertittel={tidpunktFormatert} statuslinje={ - + Avholdt } /> - ); + ) default: - console.error(`ukjent avtaletilstand ${avtaletilstand}: ignorerer`); - return null; + console.error(`ukjent avtaletilstand ${avtaletilstand}: ignorerer`) + return null } default: console.error( - `ukjent notifikasjonstype ${notifikasjon.__typename}: ignorerer`, - ); - return null; + `ukjent notifikasjonstype ${notifikasjon.__typename}: ignorerer` + ) + return null } -}; +} -const NotifikasjonBeskjed = ({ - notifikasjon, - props, - erTodo, - ikon, - tittel, - visningstidspunkt, - statuslinje, - }: { +const NotifikasjonLenke = ({ + notifikasjon, + props, + erTodo, + ikon, + tittel, + undertittel, + visningstidspunkt, + statuslinje +}: { notifikasjon: Notifikasjon props: Props erTodo: boolean ikon: ReactElement tittel: string + undertittel?: string visningstidspunkt?: Date statuslinje?: ReactElement }) => { - const { loggPilTastNavigasjon } = useAmplitude(); + const { loggPilTastNavigasjon } = useAmplitude() return (
{ - loggPilTastNavigasjon(); + loggPilTastNavigasjon() if (event.key === 'Tab') { - props.onTabEvent?.(event.shiftKey); - event.preventDefault(); + props.onTabEvent?.(event.shiftKey) + event.preventDefault() } if (event.key === 'ArrowUp' || event.key === 'Up') { - props.gåTilForrige(); + props.gåTilForrige() } if (event.key === 'ArrowDown' || event.key === 'Down') { - props.gåTilNeste(); + props.gåTilNeste() } }} onClick={() => { - props.onKlikketPaaLenke(notifikasjon); + props.onKlikketPaaLenke(notifikasjon) }} > - + {notifikasjon.virksomhet.navn.toUpperCase()} - {notifikasjon.sak?.tittel ? (<> - + {notifikasjon.sak?.tittel ? ( + <> + {notifikasjon.brukerKlikk?.klikketPaa ? ( notifikasjon.sak?.tittel ) : ( {notifikasjon.sak?.tittel} )} - {notifikasjon.sak.tilleggsinformasjon ? + {notifikasjon.sak.tilleggsinformasjon ? ( {notifikasjon.sak.tilleggsinformasjon} - : null} + className='notifikasjon_liste_element-lenkepanel-tilleggsinformasjon' + > + {' '} + {notifikasjon.sak.tilleggsinformasjon} + + ) : null} ) : null} -
{ikon}
+
{ikon}
{notifikasjon.brukerKlikk?.klikketPaa ? ( @@ -328,82 +342,86 @@ const NotifikasjonBeskjed = ({ ) : ( Ikke besøkt )} -
- <> - - {notifikasjon.brukerKlikk?.klikketPaa ? ( - tittel - ) : ( - {tittel} - )} +
+ + {tittel} + + {undertittel ? ( + + {undertittel} - + ) : null} + {visningstidspunkt === undefined ? null : ( - {sendtDatotekst(visningstidspunkt)} + {sendtDatotekst(visningstidspunkt)} )}
{statuslinje}
-
+
- ); -}; + ) +} const startTidspunktFormat = new Intl.DateTimeFormat('no', { month: 'long', day: 'numeric', hour: 'numeric', - minute: 'numeric', -}); + minute: 'numeric' +}) const sluttTidsunktFormat = new Intl.DateTimeFormat('no', { hour: 'numeric', - minute: 'numeric', -}); + minute: 'numeric' +}) -const kalenderavtaleTekst = (kalenderavtale: Kalenderavtale) => { - const startTidspunkt = new Date(kalenderavtale.startTidspunkt); - const sluttTidspunkt = - kalenderavtale.sluttTidspunkt === undefined || - kalenderavtale.sluttTidspunkt === null - ? undefined - : new Date(kalenderavtale.sluttTidspunkt); - const tidspunkt = `${startTidspunktFormat.format(startTidspunkt)} ${ - sluttTidspunkt !== undefined - ? `– ${sluttTidsunktFormat.format(sluttTidspunkt)}` - : '' - }`; - return `${kalenderavtale.tekst} ${tidspunkt}`; -}; +const kalenderavtaleTidspunkt = (kalenderavtale: Kalenderavtale) => { + const startTidspunktFormatert = startTidspunktFormat.format( + new Date(kalenderavtale.startTidspunkt) + ) + const sluttTidspunktFormatert = + kalenderavtale.sluttTidspunkt !== undefined && + kalenderavtale.sluttTidspunkt !== null + ? sluttTidsunktFormat.format(new Date(kalenderavtale.sluttTidspunkt)) + : undefined + return `${startTidspunktFormatert} ${sluttTidspunktFormatert !== undefined ? `– ${sluttTidspunktFormatert}` : ''}` +} const StatuslinjeOppgaveNy = ({ notifikasjon }: { notifikasjon: Oppgave }) => { if (!notifikasjon.frist && !notifikasjon.paaminnelseTidspunkt) { - return null; + return null } else { - let innhold; + let innhold if (!notifikasjon.frist && notifikasjon.paaminnelseTidspunkt) { - innhold = <>Påminnelse; + innhold = <>Påminnelse } else if (notifikasjon.frist && !notifikasjon.paaminnelseTidspunkt) { - innhold = <>Frist {formatterDato(new Date(notifikasjon.frist))}; + innhold = <>Frist {formatterDato(new Date(notifikasjon.frist))} } else { innhold = ( <> Påminnelse – Frist {formatterDato(new Date(notifikasjon.frist))} - ); + ) } return ( - {innhold} - ); + {innhold} + ) } -}; +} const StatusIkonMedTekst: FC<{ children: ReactNode variant: 'success' | 'neutral' | 'warning' }> = ({ variant, children }) => ( - + {children} -); +) diff --git a/widget/demo/package-lock.json b/widget/demo/package-lock.json index cf63c35e6..3e0517235 100644 --- a/widget/demo/package-lock.json +++ b/widget/demo/package-lock.json @@ -32,7 +32,7 @@ }, "../brukerapi-mock": { "name": "@navikt/arbeidsgiver-notifikasjoner-brukerapi-mock", - "version": "7.2.0", + "version": "7.2.2", "license": "MIT", "dependencies": { "apollo-server": "^3.10.2", @@ -44,7 +44,7 @@ }, "../component": { "name": "@navikt/arbeidsgiver-notifikasjon-widget", - "version": "7.2.0", + "version": "7.2.2", "dependencies": { "@apollo/client": "^3.10.8", "graphql": "^16.9.0" @@ -62,6 +62,7 @@ "@types/node": "^20.14.11", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "prettier": "3.4.2", "prop-types": "15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -4331,6 +4332,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "graphql": "^16.9.0", + "prettier": "3.4.2", "prop-types": "15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", From 2c70918c05aa125b3db477d9425cabcc23f49d3e Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Mon, 13 Jan 2025 18:23:02 +0100 Subject: [PATCH 06/27] npm version prepatch --- widget/brukerapi-mock/package-lock.json | 4 ++-- widget/brukerapi-mock/package.json | 2 +- widget/component/package-lock.json | 4 ++-- widget/component/package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/widget/brukerapi-mock/package-lock.json b/widget/brukerapi-mock/package-lock.json index 4c1440a17..f742793f7 100644 --- a/widget/brukerapi-mock/package-lock.json +++ b/widget/brukerapi-mock/package-lock.json @@ -1,12 +1,12 @@ { "name": "@navikt/arbeidsgiver-notifikasjoner-brukerapi-mock", - "version": "7.2.2", + "version": "7.2.2-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@navikt/arbeidsgiver-notifikasjoner-brukerapi-mock", - "version": "7.2.2", + "version": "7.2.2-0", "license": "MIT", "dependencies": { "apollo-server": "^3.10.2", diff --git a/widget/brukerapi-mock/package.json b/widget/brukerapi-mock/package.json index d471ae46f..138344596 100644 --- a/widget/brukerapi-mock/package.json +++ b/widget/brukerapi-mock/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "git+https://github.com/navikt/arbeidsgiver-notifikasjon-widget.git" }, - "version": "7.2.2", + "version": "7.2.2-0", "license": "MIT", "main": "dist/notifikasjonMockMiddleware.js", "files": [ diff --git a/widget/component/package-lock.json b/widget/component/package-lock.json index bf1923aa7..184102b0d 100644 --- a/widget/component/package-lock.json +++ b/widget/component/package-lock.json @@ -1,12 +1,12 @@ { "name": "@navikt/arbeidsgiver-notifikasjon-widget", - "version": "7.2.2", + "version": "7.2.2-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@navikt/arbeidsgiver-notifikasjon-widget", - "version": "7.2.2", + "version": "7.2.2-0", "dependencies": { "@apollo/client": "^3.10.8", "graphql": "^16.9.0" diff --git a/widget/component/package.json b/widget/component/package.json index b42a279e2..f7e296044 100644 --- a/widget/component/package.json +++ b/widget/component/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "git+https://github.com/navikt/arbeidsgiver-notifikasjon-widget.git" }, - "version": "7.2.2", + "version": "7.2.2-0", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", "types": "./lib/esm/index.d.ts", From e7ef71414774fdeac9a732e855cee6e413b3c577 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Tue, 14 Jan 2025 08:57:45 +0100 Subject: [PATCH 07/27] release 7.2.2 --- widget/brukerapi-mock/package-lock.json | 4 ++-- widget/brukerapi-mock/package.json | 2 +- widget/component/package-lock.json | 4 ++-- widget/component/package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/widget/brukerapi-mock/package-lock.json b/widget/brukerapi-mock/package-lock.json index f742793f7..4c1440a17 100644 --- a/widget/brukerapi-mock/package-lock.json +++ b/widget/brukerapi-mock/package-lock.json @@ -1,12 +1,12 @@ { "name": "@navikt/arbeidsgiver-notifikasjoner-brukerapi-mock", - "version": "7.2.2-0", + "version": "7.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@navikt/arbeidsgiver-notifikasjoner-brukerapi-mock", - "version": "7.2.2-0", + "version": "7.2.2", "license": "MIT", "dependencies": { "apollo-server": "^3.10.2", diff --git a/widget/brukerapi-mock/package.json b/widget/brukerapi-mock/package.json index 138344596..d471ae46f 100644 --- a/widget/brukerapi-mock/package.json +++ b/widget/brukerapi-mock/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "git+https://github.com/navikt/arbeidsgiver-notifikasjon-widget.git" }, - "version": "7.2.2-0", + "version": "7.2.2", "license": "MIT", "main": "dist/notifikasjonMockMiddleware.js", "files": [ diff --git a/widget/component/package-lock.json b/widget/component/package-lock.json index 184102b0d..bf1923aa7 100644 --- a/widget/component/package-lock.json +++ b/widget/component/package-lock.json @@ -1,12 +1,12 @@ { "name": "@navikt/arbeidsgiver-notifikasjon-widget", - "version": "7.2.2-0", + "version": "7.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@navikt/arbeidsgiver-notifikasjon-widget", - "version": "7.2.2-0", + "version": "7.2.2", "dependencies": { "@apollo/client": "^3.10.8", "graphql": "^16.9.0" diff --git a/widget/component/package.json b/widget/component/package.json index f7e296044..b42a279e2 100644 --- a/widget/component/package.json +++ b/widget/component/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "git+https://github.com/navikt/arbeidsgiver-notifikasjon-widget.git" }, - "version": "7.2.2-0", + "version": "7.2.2", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", "types": "./lib/esm/index.d.ts", From dc5342873a296e3702b3481cdc1ecffc10913235 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Tue, 14 Jan 2025 09:48:36 +0100 Subject: [PATCH 08/27] default valid zip --- test-produsent/src/Komponenter/KalenderAvtale.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-produsent/src/Komponenter/KalenderAvtale.tsx b/test-produsent/src/Komponenter/KalenderAvtale.tsx index 126e0f6e4..5f416e403 100644 --- a/test-produsent/src/Komponenter/KalenderAvtale.tsx +++ b/test-produsent/src/Komponenter/KalenderAvtale.tsx @@ -274,7 +274,7 @@ const Lokasjon = forwardRef((_props, ref) => { Date: Tue, 14 Jan 2025 10:15:36 +0100 Subject: [PATCH 09/27] juster replicas --- app/nais/dev-gcp-skedulert-utgaatt.yaml | 2 +- app/nais/prod-gcp-skedulert-utgaatt.yaml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/nais/dev-gcp-skedulert-utgaatt.yaml b/app/nais/dev-gcp-skedulert-utgaatt.yaml index a1e4e6dcc..18eebac59 100644 --- a/app/nais/dev-gcp-skedulert-utgaatt.yaml +++ b/app/nais/dev-gcp-skedulert-utgaatt.yaml @@ -19,7 +19,7 @@ spec: path: /internal/ready replicas: min: 1 - max: 1 + max: 2 prometheus: enabled: true path: /internal/metrics diff --git a/app/nais/prod-gcp-skedulert-utgaatt.yaml b/app/nais/prod-gcp-skedulert-utgaatt.yaml index bfe9737fb..603e80f49 100644 --- a/app/nais/prod-gcp-skedulert-utgaatt.yaml +++ b/app/nais/prod-gcp-skedulert-utgaatt.yaml @@ -14,9 +14,8 @@ spec: limits: memory: 1024Mi replicas: - disableAutoScaling: true - min: 10 - max: 10 + min: 1 + max: 2 liveness: path: /internal/alive readiness: From eb9ad2ef317b6d84d0b898fd02d721062a7b15aa Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Tue, 14 Jan 2025 13:36:36 +0100 Subject: [PATCH 10/27] =?UTF-8?q?send=20hendelser=20i=20tx=20Uten=20dette?= =?UTF-8?q?=20har=20vi=20et=20potensielt=20case=20hvor=20meldinger=20blir?= =?UTF-8?q?=20mistet.=20Siden=20man=20gj=C3=B8r=20delete=20returning=20s?= =?UTF-8?q?=C3=A5=20er=20det=20viktig=20at=20vi=20sender=20alle=20meldinge?= =?UTF-8?q?r=20p=C3=A5=20kafka=20f=C3=B8r=20commit.=20Dersom=20det=20blir?= =?UTF-8?q?=20en=20feil=20vil=20det=20bli=20muligens=20bli=20sendt=20dobbe?= =?UTF-8?q?lt=20opp,=20men=20det=20er=20bedre=20enn=20ingen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notifikasjon/infrastruktur/Database.kt | 2 +- .../SkedulertUtg\303\245ttRepository.kt" | 71 +++++++++++-------- .../SkedulertUtg\303\245ttService.kt" | 5 +- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt index 614040c4e..41840254f 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt @@ -158,7 +158,7 @@ class Database private constructor( suspend fun transaction( rollback: (e: Exception) -> T = { throw it }, - body: Transaction.() -> T, + body: suspend Transaction.() -> T, ): T = dataSource.withConnection { connection -> val savedAutoCommit = connection.autoCommit diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttRepository.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttRepository.kt" index d66a59bb8..ef0e7a0d1 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttRepository.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttRepository.kt" @@ -173,49 +173,64 @@ class SkedulertUtgåttRepository( } } - suspend fun hentOgFjernAlleUtgåtteOppgaver(localDateNow: LocalDate) = database.nonTransactionalExecuteQuery( - """ + suspend fun hentOgFjernAlleUtgåtteOppgaver( + localDateNow: LocalDate, + action: suspend (Oppgave) -> Unit + ) = database.transaction { + executeQuery( + """ delete from skedulert_utgatt as s using oppgave as o where o.oppgave_id = s.aggregat_id and s.aggregat_type = ? and o.frist < ? returning * """, { - text(AggregatType.OPPGAVE.name) - localDateAsText(localDateNow) - }) { - Oppgave( - oppgaveId = getUUID("oppgave_id"), - frist = getLocalDate("frist"), - virksomhetsnummer = getString("virksomhetsnummer"), - produsentId = getString("produsent_id"), - ) + text(AggregatType.OPPGAVE.name) + localDateAsText(localDateNow) + }) { + Oppgave( + oppgaveId = getUUID("oppgave_id"), + frist = getLocalDate("frist"), + virksomhetsnummer = getString("virksomhetsnummer"), + produsentId = getString("produsent_id"), + ) + }.forEach { + action(it) + } } - suspend fun hentOgFjernAlleAvholdteKalenderavtaler(localDateTimeNow: LocalDateTime) = - database.nonTransactionalExecuteQuery( - """ + + suspend fun hentOgFjernAlleAvholdteKalenderavtaler( + localDateTimeNow: LocalDateTime, + action: suspend (Kalenderavtale) -> Unit + ) = + database.transaction { + executeQuery( + """ delete from skedulert_utgatt as s using kalenderavtale as k where k.kalenderavtale_id = s.aggregat_id and s.aggregat_type = ? and k.start_tidspunkt < ? returning * """, { - text(AggregatType.KALENDERAVTALE.name) - localDateTimeAsText(localDateTimeNow) - }) { - Kalenderavtale( - kalenderavtaleId = getUUID("kalenderavtale_id"), - startTidspunkt = getLocalDateTime("start_tidspunkt"), - virksomhetsnummer = getString("virksomhetsnummer"), - tilstand = getString("tilstand").let { KalenderavtaleTilstand.valueOf(it) }, - produsentId = getString("produsent_id"), - merkelapp = getString("merkelapp"), - grupperingsid = getString("grupperingsid"), - opprettetTidspunkt = Instant.parse( - getString("opprettet_tidspunkt"), + text(AggregatType.KALENDERAVTALE.name) + localDateTimeAsText(localDateTimeNow) + }) { + Kalenderavtale( + kalenderavtaleId = getUUID("kalenderavtale_id"), + startTidspunkt = getLocalDateTime("start_tidspunkt"), + virksomhetsnummer = getString("virksomhetsnummer"), + tilstand = getString("tilstand").let { KalenderavtaleTilstand.valueOf(it) }, + produsentId = getString("produsent_id"), + merkelapp = getString("merkelapp"), + grupperingsid = getString("grupperingsid"), + opprettetTidspunkt = Instant.parse( + getString("opprettet_tidspunkt"), + ) ) - ) + }.forEach { + action(it) + } } private suspend fun upsertSkedulertUtgåttOppgave(oppgave: Oppgave) = database.transaction { diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttService.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttService.kt" index 5d55bf199..a33e714e6 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttService.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245ttService.kt" @@ -19,8 +19,7 @@ class SkedulertUtgåttService( private val osloTid: OsloTid = OsloTidImpl ) { suspend fun settOppgaverUtgåttBasertPåFrist(now: LocalDate = osloTid.localDateNow()) { - val utgåttFrist = repository.hentOgFjernAlleUtgåtteOppgaver(now) - utgåttFrist.forEach { utgått -> + repository.hentOgFjernAlleUtgåtteOppgaver(now) { utgått -> val fristLocalDateTime = LocalDateTime.of(utgått.frist, LocalTime.MAX) hendelseProdusent.send(HendelseModel.OppgaveUtgått( virksomhetsnummer = utgått.virksomhetsnummer, @@ -36,7 +35,7 @@ class SkedulertUtgåttService( } suspend fun settKalenderavtalerAvholdtBasertPåTidspunkt(now: LocalDateTime = osloTid.localDateTimeNow()) { - repository.hentOgFjernAlleAvholdteKalenderavtaler(now).forEach { avholdt -> + repository.hentOgFjernAlleAvholdteKalenderavtaler(now) { avholdt -> hendelseProdusent.send(HendelseModel.KalenderavtaleOppdatert( virksomhetsnummer = avholdt.virksomhetsnummer, notifikasjonId = avholdt.kalenderavtaleId, From add94a4056bf14667b6b33d8506b88661f7d14c7 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Tue, 14 Jan 2025 13:55:07 +0100 Subject: [PATCH 11/27] oops, copy paste av groupId er ikke kjempelurt.. --- .../skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" index 69949a702..84f36ea34 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" @@ -20,7 +20,7 @@ object SkedulertUtgått { private val hendelsesstrøm by lazy { HendelsesstrømKafkaImpl( topic = NOTIFIKASJON_TOPIC, - groupId = "skedulert-harddelete-model-builder-1", + groupId = "skedulert-utgaat-model-builder-2", replayPeriodically = true, ) } From 6c4aa605721bd2ac62c42544156719eb375c4576 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Tue, 14 Jan 2025 13:56:32 +0100 Subject: [PATCH 12/27] oops, copy paste av groupId er ikke kjempelurt.. --- .../skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" index 84f36ea34..c51541df9 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" @@ -20,7 +20,7 @@ object SkedulertUtgått { private val hendelsesstrøm by lazy { HendelsesstrømKafkaImpl( topic = NOTIFIKASJON_TOPIC, - groupId = "skedulert-utgaat-model-builder-2", + groupId = "skedulert-utgatt-model-builder", replayPeriodically = true, ) } From eae5f46483501d120c2ca8e0ff784a622b72ac0f Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Tue, 14 Jan 2025 14:10:51 +0100 Subject: [PATCH 13/27] remove health.database=true --- .../skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" | 4 ---- 1 file changed, 4 deletions(-) diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" index c51541df9..61ffd3b1d 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" @@ -6,8 +6,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database.Companion.openDatabaseAsync -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Health -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Subsystem import no.nav.arbeidsgiver.notifikasjon.infrastruktur.http.launchHttpServer import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.HendelsesstrømKafkaImpl import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.NOTIFIKASJON_TOPIC @@ -26,8 +24,6 @@ object SkedulertUtgått { } fun main(httpPort: Int = 8080) { - Health.subsystemReady[Subsystem.DATABASE] = true - runBlocking(Dispatchers.Default) { val database = openDatabaseAsync(databaseConfig) val repoAsync = async { From 61bbe19f221e6ee66457325b5e1adcfe22925732 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Tue, 14 Jan 2025 14:16:51 +0100 Subject: [PATCH 14/27] use same postgres version across apps --- app/nais/dev-gcp-skedulert-utgaatt.yaml | 2 +- app/nais/prod-gcp-skedulert-utgaatt.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/nais/dev-gcp-skedulert-utgaatt.yaml b/app/nais/dev-gcp-skedulert-utgaatt.yaml index 18eebac59..7f400cd06 100644 --- a/app/nais/dev-gcp-skedulert-utgaatt.yaml +++ b/app/nais/dev-gcp-skedulert-utgaatt.yaml @@ -27,7 +27,7 @@ spec: pool: nav-dev gcp: sqlInstances: - - type: POSTGRES_17 + - type: POSTGRES_12 tier: db-f1-micro diskAutoresize: true highAvailability: false diff --git a/app/nais/prod-gcp-skedulert-utgaatt.yaml b/app/nais/prod-gcp-skedulert-utgaatt.yaml index 603e80f49..e1ff65629 100644 --- a/app/nais/prod-gcp-skedulert-utgaatt.yaml +++ b/app/nais/prod-gcp-skedulert-utgaatt.yaml @@ -27,7 +27,7 @@ spec: pool: nav-prod gcp: sqlInstances: - - type: POSTGRES_17 + - type: POSTGRES_12 tier: db-custom-1-3840 diskAutoresize: true highAvailability: false From ee85294d48430fdb18a783f03159340785609c1d Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Tue, 14 Jan 2025 15:58:21 +0100 Subject: [PATCH 15/27] oppdater vnr til noe vi har tilgang til --- test-produsent/src/Komponenter/KalenderAvtale.tsx | 2 +- test-produsent/src/Komponenter/NyBeskjed.tsx | 2 +- test-produsent/src/Komponenter/NyOppgave.tsx | 2 +- test-produsent/src/Komponenter/NySak.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test-produsent/src/Komponenter/KalenderAvtale.tsx b/test-produsent/src/Komponenter/KalenderAvtale.tsx index 5f416e403..2b47a2443 100644 --- a/test-produsent/src/Komponenter/KalenderAvtale.tsx +++ b/test-produsent/src/Komponenter/KalenderAvtale.tsx @@ -143,7 +143,7 @@ export const NyKalenderAvtale: FunctionComponent = () => { {
- + diff --git a/test-produsent/src/Komponenter/NyOppgave.tsx b/test-produsent/src/Komponenter/NyOppgave.tsx index e9d3b7c84..47d4f6098 100644 --- a/test-produsent/src/Komponenter/NyOppgave.tsx +++ b/test-produsent/src/Komponenter/NyOppgave.tsx @@ -117,7 +117,7 @@ export const NyOppgave: React.FunctionComponent = () => {
- + diff --git a/test-produsent/src/Komponenter/NySak.tsx b/test-produsent/src/Komponenter/NySak.tsx index c36638dce..d626c3b51 100644 --- a/test-produsent/src/Komponenter/NySak.tsx +++ b/test-produsent/src/Komponenter/NySak.tsx @@ -98,7 +98,7 @@ export const NySak: React.FunctionComponent = () => {
- + From 4a1a840a798e30bf0f6ff93d37f70cf0fa4bd718 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Tue, 14 Jan 2025 16:05:12 +0100 Subject: [PATCH 16/27] =?UTF-8?q?bruk=20postgres=2017=20p=C3=A5=20ny=20dat?= =?UTF-8?q?abase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/nais/dev-gcp-skedulert-utgaatt.yaml | 2 +- app/nais/prod-gcp-skedulert-utgaatt.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/nais/dev-gcp-skedulert-utgaatt.yaml b/app/nais/dev-gcp-skedulert-utgaatt.yaml index 7f400cd06..18eebac59 100644 --- a/app/nais/dev-gcp-skedulert-utgaatt.yaml +++ b/app/nais/dev-gcp-skedulert-utgaatt.yaml @@ -27,7 +27,7 @@ spec: pool: nav-dev gcp: sqlInstances: - - type: POSTGRES_12 + - type: POSTGRES_17 tier: db-f1-micro diskAutoresize: true highAvailability: false diff --git a/app/nais/prod-gcp-skedulert-utgaatt.yaml b/app/nais/prod-gcp-skedulert-utgaatt.yaml index e1ff65629..603e80f49 100644 --- a/app/nais/prod-gcp-skedulert-utgaatt.yaml +++ b/app/nais/prod-gcp-skedulert-utgaatt.yaml @@ -27,7 +27,7 @@ spec: pool: nav-prod gcp: sqlInstances: - - type: POSTGRES_12 + - type: POSTGRES_17 tier: db-custom-1-3840 diskAutoresize: true highAvailability: false From 73f4cc825cb201b47533407c19ce8cd872c3ab2d Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Tue, 14 Jan 2025 18:51:58 +0100 Subject: [PATCH 17/27] =?UTF-8?q?Endre=20Database.Config=20til=20=C3=A5=20?= =?UTF-8?q?bruke=20JDBC=5FURL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nyere databaser i nais er satt opp med privat ip og ssl certs. For å lettere støtte dette endres her til å basere oss på en gyldig JDBC_URL. Eksisterende databaser er fortsatt uten ssl og certs, men der er også JDBC_URL tilgjengelig. Skriver om test oppsett til å lene seg på hjelpefunksjoner i Database.Config og JdbcUrl --- .../notifikasjon/infrastruktur/Database.kt | 93 +++++++++++++++---- .../executable/BackupRepositoryPerformance.kt | 4 +- .../infrastruktur/DatabaseConfigTest.kt | 71 ++++++++++++++ .../notifikasjon/util/PostgresTestListener.kt | 22 ++--- 4 files changed, 158 insertions(+), 32 deletions(-) create mode 100644 app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt index 41840254f..da2d4b72d 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt @@ -6,10 +6,13 @@ import kotlinx.coroutines.* import no.nav.arbeidsgiver.notifikasjon.infrastruktur.json.laxObjectMapper import no.nav.arbeidsgiver.notifikasjon.infrastruktur.json.writeValueAsStringSupportingTypeInfoInCollections import no.nav.arbeidsgiver.notifikasjon.infrastruktur.unblocking.NonBlockingDataSource +import org.apache.http.client.utils.URIBuilder +import org.apache.http.message.BasicNameValuePair import org.flywaydb.core.Flyway import org.flywaydb.core.api.configuration.FluentConfiguration import org.intellij.lang.annotations.Language import java.io.Closeable +import java.net.URI import java.sql.* import java.time.* import java.time.temporal.ChronoUnit @@ -24,16 +27,25 @@ class Database private constructor( private val dataSource: NonBlockingDataSource<*>, ) : Closeable { data class Config( - val host: String, - val port: String, - val database: String, - val username: String, - val password: String, + private val jdbcUrl: String, val migrationLocations: String, - val jdbcOpts: Map = mapOf() + val jdbcOpts: Map = mapOf() ) { - val jdbcUrl: String - get() = "jdbc:postgresql://$host:$port/$database?${jdbcOpts.entries.joinToString("&")}" + val url: JdbcUrl = JdbcUrl(jdbcUrl, jdbcOpts) + + val username: String + get() = url.username + val password: String + get() = url.password + val database: String + get() = url.database + + /** + * make a copy but change the database name + */ + fun withDatabase(database: String) = copy( + jdbcUrl = url.withDatabase(database).toString() + ) } override fun close() { @@ -43,12 +55,13 @@ class Database private constructor( companion object { private val log = logger() - fun config(name: String, envPrefix: String = "DB", jdbcOpts: Map = mapOf()) = Config( - host = System.getenv("${envPrefix}_HOST") ?: "localhost", - port = System.getenv("${envPrefix}_PORT") ?: "1337", - username = System.getenv("${envPrefix}_USERNAME") ?: "postgres", - password = System.getenv("${envPrefix}_PASSWORD") ?: "postgres", - database = System.getenv("${envPrefix}_DATABASE") ?: name.replace('_', '-'), + fun config(name: String, envPrefix: String = "DB", jdbcOpts: Map = emptyMap()) = Config( + jdbcUrl = System.getenv("${envPrefix}_JDBC_URL") ?: "jdbc:postgresql://localhost:1337/${ + name.replace( + '_', + '-' + ) + }?password=postgres&user=postgres", migrationLocations = "db/migration/$name", jdbcOpts = jdbcOpts, ) @@ -56,9 +69,7 @@ class Database private constructor( private fun Config.asHikariConfig(): HikariConfig { val config = this return HikariConfig().apply { - jdbcUrl = config.jdbcUrl - username = config.username - password = config.password + jdbcUrl = config.url.toString() driverClassName = "org.postgresql.Driver" metricRegistry = Metrics.meterRegistry minimumIdle = 1 @@ -356,3 +367,51 @@ fun measureSql( @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") return timer.recordCallable(action) } + +/** + * Utility class to aid in constructing and manipulating a jdbc url. + */ +class JdbcUrl( + url: String, + additionalOptions: Map = emptyMap() +) { + + /** + * we need to strip the jdbc: part by using schemeSpecificPart + * so that URI is able to parse correctly. + * the jdbc: prefix is added back in toString() + */ + private val uri = URIBuilder( + URI(url).also { + require(it.scheme == "jdbc") { "not a jdbc url: $url" } + }.schemeSpecificPart + ).also { + if (additionalOptions.isNotEmpty()) { + it.addParameters(additionalOptions.map { (k, v) -> BasicNameValuePair(k, v) }) + } + }.build() + + private val urlParameters = uri.query.split('&').associate { + val parts = it.split('=') + val name = parts.firstOrNull() ?: "" + val value = parts.drop(1).firstOrNull() ?: "" + Pair(name, value) + } + + val username: String + get() = urlParameters["user"]!! + val password: String + get() = urlParameters["password"]!! + val database: String + get() = uri.path.split('/').last() + + override fun toString() = "jdbc:$uri" + + /** + * make a copy but change the database name. used in tests + */ + fun withDatabase(database: String): JdbcUrl { + val newUri = URIBuilder(uri).setPath("/$database").build() + return JdbcUrl("jdbc:$newUri") + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt index 9c0690ebb..2adfb4946 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt @@ -1,10 +1,10 @@ package no.nav.arbeidsgiver.notifikasjon.executable import kotlinx.coroutines.runBlocking -import no.nav.arbeidsgiver.notifikasjon.kafka_backup.KafkaBackup import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database import no.nav.arbeidsgiver.notifikasjon.infrastruktur.json.laxObjectMapper import no.nav.arbeidsgiver.notifikasjon.kafka_backup.BackupRepository +import no.nav.arbeidsgiver.notifikasjon.kafka_backup.KafkaBackup import no.nav.arbeidsgiver.notifikasjon.util.EksempelHendelse import org.apache.kafka.clients.consumer.ConsumerRecord import java.time.Duration @@ -16,7 +16,7 @@ fun main() = runBlocking { Database.openDatabase( KafkaBackup.databaseConfig.copy( // https://github.com/flyway/flyway/issues/2323#issuecomment-804495818 - jdbcOpts = mapOf("preparedStatementCacheQueries" to 0) + jdbcOpts = mapOf("preparedStatementCacheQueries" to "0") ) ).use { database -> diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt new file mode 100644 index 000000000..709d1841e --- /dev/null +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt @@ -0,0 +1,71 @@ +package no.nav.arbeidsgiver.notifikasjon.infrastruktur + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +class DatabaseConfigTest : DescribeSpec({ + + describe("JdbcUrl") { + it("from jdbcUrl yields correct database, username, password and url") { + val url = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val jdbcUrl = JdbcUrl(url) + + jdbcUrl.database shouldBe "mydb" + jdbcUrl.username shouldBe "myuser" + jdbcUrl.password shouldBe "mypassword" + } + + it("supports additional options via map") { + val url = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val jdbcUrl = JdbcUrl(url, mapOf("foo" to "bar")).toString() + + jdbcUrl shouldBe "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword&foo=bar" + } + + it("can create a copy with another database name") { + val url = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val jdbcUrl = JdbcUrl(url) + + jdbcUrl.withDatabase("anotherdb").toString() shouldBe "jdbc:postgresql://localhost:5432/anotherdb?user=myuser&password=mypassword" + + } + } + + describe("Database.Config") { + it("from jdbcUrl yields correct database, username, password and url") { + val jdbcUrl = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val config = Database.Config( + jdbcUrl = jdbcUrl, + migrationLocations = "", + jdbcOpts = mapOf() + ) + + config.database shouldBe "mydb" + config.username shouldBe "myuser" + config.password shouldBe "mypassword" + config.url.toString() shouldBe jdbcUrl + } + + it("supports additional options via map") { + val jdbcUrl = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val config = Database.Config( + jdbcUrl = jdbcUrl, + migrationLocations = "", + jdbcOpts = mapOf("foo" to "bar") + ) + + config.url.toString() shouldBe "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword&foo=bar" + } + + it("can create a copy with another database name") { + val jdbcUrl = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val config = Database.Config( + jdbcUrl = jdbcUrl, + migrationLocations = "", + jdbcOpts = mapOf() + ) + + config.withDatabase("anotherdb").url.toString() shouldBe "jdbc:postgresql://localhost:5432/anotherdb?user=myuser&password=mypassword" + } + } +}) diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt index 6e2d2cd14..5b29bbff0 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt @@ -19,13 +19,14 @@ private suspend fun createDbFromTemplate(config: Database.Config): Database.Conf val database = mutex.withLock { "${config.database}_test-${ids.next()}" } - DriverManager.getConnection(config.jdbcUrl, config.username, config.password).use { conn -> + + DriverManager.getConnection(config.url.toString(), config.username, config.password).use { conn -> conn.createStatement().use { stmt -> @Suppress("SqlSourceToSinkFlow") stmt.executeUpdate("""create database "$database" template "$templateDb"; """) } } - return config.copy(database = database) + return config.withDatabase(database) } /** @@ -33,10 +34,10 @@ private suspend fun createDbFromTemplate(config: Database.Config): Database.Conf * template databasen brukes til å lage ferske databaser for hver test. */ @Suppress("SqlSourceToSinkFlow") -private suspend fun templateDb(config: Database.Config): String { +private fun templateDb(config: Database.Config): String { val templateDb = templateDbs.computeIfAbsent(config) { "${config.database}_template".also { db -> - DriverManager.getConnection(config.jdbcUrl, config.username, config.password).use { conn -> + DriverManager.getConnection(config.url.toString(), config.username, config.password).use { conn -> conn.createStatement().use { stmt -> val resultSet = stmt.executeQuery("SELECT datname FROM pg_database where datname like '${config.database}_test%';") @@ -58,10 +59,7 @@ private suspend fun templateDb(config: Database.Config): String { } runBlocking { Database.openDatabase( - config = config.copy( - port = "1337", - database = db, - ), + config = config.withDatabase(db), flywayAction = { migrate() } @@ -74,13 +72,11 @@ private suspend fun templateDb(config: Database.Config): String { fun TestConfiguration.testDatabase(config: Database.Config): Database = runBlocking { - val database = createDbFromTemplate(config).database + val testConfig = createDbFromTemplate(config) Database.openDatabase( - config = config.copy( + config = testConfig.copy( // https://github.com/flyway/flyway/issues/2323#issuecomment-804495818 - jdbcOpts = mapOf("preparedStatementCacheQueries" to 0), - port = "1337", - database = database, + jdbcOpts = mapOf("preparedStatementCacheQueries" to "0"), ), flywayAction = { /* noop. created from template. */ From 3b18870733527ae3503380504db8ea7d76ff85a4 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Wed, 15 Jan 2025 10:03:57 +0100 Subject: [PATCH 18/27] =?UTF-8?q?Revert=20"Endre=20Database.Config=20til?= =?UTF-8?q?=20=C3=A5=20bruke=20JDBC=5FURL"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 73f4cc825cb201b47533407c19ce8cd872c3ab2d. --- .../notifikasjon/infrastruktur/Database.kt | 93 ++++--------------- .../executable/BackupRepositoryPerformance.kt | 4 +- .../infrastruktur/DatabaseConfigTest.kt | 71 -------------- .../notifikasjon/util/PostgresTestListener.kt | 22 +++-- 4 files changed, 32 insertions(+), 158 deletions(-) delete mode 100644 app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt index da2d4b72d..41840254f 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt @@ -6,13 +6,10 @@ import kotlinx.coroutines.* import no.nav.arbeidsgiver.notifikasjon.infrastruktur.json.laxObjectMapper import no.nav.arbeidsgiver.notifikasjon.infrastruktur.json.writeValueAsStringSupportingTypeInfoInCollections import no.nav.arbeidsgiver.notifikasjon.infrastruktur.unblocking.NonBlockingDataSource -import org.apache.http.client.utils.URIBuilder -import org.apache.http.message.BasicNameValuePair import org.flywaydb.core.Flyway import org.flywaydb.core.api.configuration.FluentConfiguration import org.intellij.lang.annotations.Language import java.io.Closeable -import java.net.URI import java.sql.* import java.time.* import java.time.temporal.ChronoUnit @@ -27,25 +24,16 @@ class Database private constructor( private val dataSource: NonBlockingDataSource<*>, ) : Closeable { data class Config( - private val jdbcUrl: String, + val host: String, + val port: String, + val database: String, + val username: String, + val password: String, val migrationLocations: String, - val jdbcOpts: Map = mapOf() + val jdbcOpts: Map = mapOf() ) { - val url: JdbcUrl = JdbcUrl(jdbcUrl, jdbcOpts) - - val username: String - get() = url.username - val password: String - get() = url.password - val database: String - get() = url.database - - /** - * make a copy but change the database name - */ - fun withDatabase(database: String) = copy( - jdbcUrl = url.withDatabase(database).toString() - ) + val jdbcUrl: String + get() = "jdbc:postgresql://$host:$port/$database?${jdbcOpts.entries.joinToString("&")}" } override fun close() { @@ -55,13 +43,12 @@ class Database private constructor( companion object { private val log = logger() - fun config(name: String, envPrefix: String = "DB", jdbcOpts: Map = emptyMap()) = Config( - jdbcUrl = System.getenv("${envPrefix}_JDBC_URL") ?: "jdbc:postgresql://localhost:1337/${ - name.replace( - '_', - '-' - ) - }?password=postgres&user=postgres", + fun config(name: String, envPrefix: String = "DB", jdbcOpts: Map = mapOf()) = Config( + host = System.getenv("${envPrefix}_HOST") ?: "localhost", + port = System.getenv("${envPrefix}_PORT") ?: "1337", + username = System.getenv("${envPrefix}_USERNAME") ?: "postgres", + password = System.getenv("${envPrefix}_PASSWORD") ?: "postgres", + database = System.getenv("${envPrefix}_DATABASE") ?: name.replace('_', '-'), migrationLocations = "db/migration/$name", jdbcOpts = jdbcOpts, ) @@ -69,7 +56,9 @@ class Database private constructor( private fun Config.asHikariConfig(): HikariConfig { val config = this return HikariConfig().apply { - jdbcUrl = config.url.toString() + jdbcUrl = config.jdbcUrl + username = config.username + password = config.password driverClassName = "org.postgresql.Driver" metricRegistry = Metrics.meterRegistry minimumIdle = 1 @@ -367,51 +356,3 @@ fun measureSql( @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") return timer.recordCallable(action) } - -/** - * Utility class to aid in constructing and manipulating a jdbc url. - */ -class JdbcUrl( - url: String, - additionalOptions: Map = emptyMap() -) { - - /** - * we need to strip the jdbc: part by using schemeSpecificPart - * so that URI is able to parse correctly. - * the jdbc: prefix is added back in toString() - */ - private val uri = URIBuilder( - URI(url).also { - require(it.scheme == "jdbc") { "not a jdbc url: $url" } - }.schemeSpecificPart - ).also { - if (additionalOptions.isNotEmpty()) { - it.addParameters(additionalOptions.map { (k, v) -> BasicNameValuePair(k, v) }) - } - }.build() - - private val urlParameters = uri.query.split('&').associate { - val parts = it.split('=') - val name = parts.firstOrNull() ?: "" - val value = parts.drop(1).firstOrNull() ?: "" - Pair(name, value) - } - - val username: String - get() = urlParameters["user"]!! - val password: String - get() = urlParameters["password"]!! - val database: String - get() = uri.path.split('/').last() - - override fun toString() = "jdbc:$uri" - - /** - * make a copy but change the database name. used in tests - */ - fun withDatabase(database: String): JdbcUrl { - val newUri = URIBuilder(uri).setPath("/$database").build() - return JdbcUrl("jdbc:$newUri") - } -} \ No newline at end of file diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt index 2adfb4946..9c0690ebb 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt @@ -1,10 +1,10 @@ package no.nav.arbeidsgiver.notifikasjon.executable import kotlinx.coroutines.runBlocking +import no.nav.arbeidsgiver.notifikasjon.kafka_backup.KafkaBackup import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database import no.nav.arbeidsgiver.notifikasjon.infrastruktur.json.laxObjectMapper import no.nav.arbeidsgiver.notifikasjon.kafka_backup.BackupRepository -import no.nav.arbeidsgiver.notifikasjon.kafka_backup.KafkaBackup import no.nav.arbeidsgiver.notifikasjon.util.EksempelHendelse import org.apache.kafka.clients.consumer.ConsumerRecord import java.time.Duration @@ -16,7 +16,7 @@ fun main() = runBlocking { Database.openDatabase( KafkaBackup.databaseConfig.copy( // https://github.com/flyway/flyway/issues/2323#issuecomment-804495818 - jdbcOpts = mapOf("preparedStatementCacheQueries" to "0") + jdbcOpts = mapOf("preparedStatementCacheQueries" to 0) ) ).use { database -> diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt deleted file mode 100644 index 709d1841e..000000000 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -package no.nav.arbeidsgiver.notifikasjon.infrastruktur - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe - -class DatabaseConfigTest : DescribeSpec({ - - describe("JdbcUrl") { - it("from jdbcUrl yields correct database, username, password and url") { - val url = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" - val jdbcUrl = JdbcUrl(url) - - jdbcUrl.database shouldBe "mydb" - jdbcUrl.username shouldBe "myuser" - jdbcUrl.password shouldBe "mypassword" - } - - it("supports additional options via map") { - val url = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" - val jdbcUrl = JdbcUrl(url, mapOf("foo" to "bar")).toString() - - jdbcUrl shouldBe "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword&foo=bar" - } - - it("can create a copy with another database name") { - val url = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" - val jdbcUrl = JdbcUrl(url) - - jdbcUrl.withDatabase("anotherdb").toString() shouldBe "jdbc:postgresql://localhost:5432/anotherdb?user=myuser&password=mypassword" - - } - } - - describe("Database.Config") { - it("from jdbcUrl yields correct database, username, password and url") { - val jdbcUrl = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" - val config = Database.Config( - jdbcUrl = jdbcUrl, - migrationLocations = "", - jdbcOpts = mapOf() - ) - - config.database shouldBe "mydb" - config.username shouldBe "myuser" - config.password shouldBe "mypassword" - config.url.toString() shouldBe jdbcUrl - } - - it("supports additional options via map") { - val jdbcUrl = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" - val config = Database.Config( - jdbcUrl = jdbcUrl, - migrationLocations = "", - jdbcOpts = mapOf("foo" to "bar") - ) - - config.url.toString() shouldBe "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword&foo=bar" - } - - it("can create a copy with another database name") { - val jdbcUrl = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" - val config = Database.Config( - jdbcUrl = jdbcUrl, - migrationLocations = "", - jdbcOpts = mapOf() - ) - - config.withDatabase("anotherdb").url.toString() shouldBe "jdbc:postgresql://localhost:5432/anotherdb?user=myuser&password=mypassword" - } - } -}) diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt index 5b29bbff0..6e2d2cd14 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt @@ -19,14 +19,13 @@ private suspend fun createDbFromTemplate(config: Database.Config): Database.Conf val database = mutex.withLock { "${config.database}_test-${ids.next()}" } - - DriverManager.getConnection(config.url.toString(), config.username, config.password).use { conn -> + DriverManager.getConnection(config.jdbcUrl, config.username, config.password).use { conn -> conn.createStatement().use { stmt -> @Suppress("SqlSourceToSinkFlow") stmt.executeUpdate("""create database "$database" template "$templateDb"; """) } } - return config.withDatabase(database) + return config.copy(database = database) } /** @@ -34,10 +33,10 @@ private suspend fun createDbFromTemplate(config: Database.Config): Database.Conf * template databasen brukes til å lage ferske databaser for hver test. */ @Suppress("SqlSourceToSinkFlow") -private fun templateDb(config: Database.Config): String { +private suspend fun templateDb(config: Database.Config): String { val templateDb = templateDbs.computeIfAbsent(config) { "${config.database}_template".also { db -> - DriverManager.getConnection(config.url.toString(), config.username, config.password).use { conn -> + DriverManager.getConnection(config.jdbcUrl, config.username, config.password).use { conn -> conn.createStatement().use { stmt -> val resultSet = stmt.executeQuery("SELECT datname FROM pg_database where datname like '${config.database}_test%';") @@ -59,7 +58,10 @@ private fun templateDb(config: Database.Config): String { } runBlocking { Database.openDatabase( - config = config.withDatabase(db), + config = config.copy( + port = "1337", + database = db, + ), flywayAction = { migrate() } @@ -72,11 +74,13 @@ private fun templateDb(config: Database.Config): String { fun TestConfiguration.testDatabase(config: Database.Config): Database = runBlocking { - val testConfig = createDbFromTemplate(config) + val database = createDbFromTemplate(config).database Database.openDatabase( - config = testConfig.copy( + config = config.copy( // https://github.com/flyway/flyway/issues/2323#issuecomment-804495818 - jdbcOpts = mapOf("preparedStatementCacheQueries" to "0"), + jdbcOpts = mapOf("preparedStatementCacheQueries" to 0), + port = "1337", + database = database, ), flywayAction = { /* noop. created from template. */ From e7059ef148c8531e8b94e6da5bd65fe875ad9983 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Wed, 15 Jan 2025 10:14:28 +0100 Subject: [PATCH 19/27] =?UTF-8?q?noop=20skedulert=20utg=C3=A5tt=20midlerti?= =?UTF-8?q?dig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SkedulertUtg\303\245tt.kt" | 64 +++++++++---------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" index 61ffd3b1d..0c3307782 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" @@ -1,17 +1,11 @@ package no.nav.arbeidsgiver.notifikasjon.skedulert_utgått import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database.Companion.openDatabaseAsync import no.nav.arbeidsgiver.notifikasjon.infrastruktur.http.launchHttpServer import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.HendelsesstrømKafkaImpl import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.NOTIFIKASJON_TOPIC -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.lagKafkaHendelseProdusent -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.launchProcessingLoop -import java.time.Duration object SkedulertUtgått { val databaseConfig = Database.config("skedulert_utgatt_model") @@ -25,35 +19,35 @@ object SkedulertUtgått { fun main(httpPort: Int = 8080) { runBlocking(Dispatchers.Default) { - val database = openDatabaseAsync(databaseConfig) - val repoAsync = async { - SkedulertUtgåttRepository(database.await()) - } - launch { - val repo = repoAsync.await() - hendelsesstrøm.forEach { hendelse, metadata -> - repo.oppdaterModellEtterHendelse(hendelse) - } - } - - val service = async { - SkedulertUtgåttService( - repository = repoAsync.await(), - hendelseProdusent = lagKafkaHendelseProdusent(topic = NOTIFIKASJON_TOPIC) - ) - } - launchProcessingLoop( - "utgaatt-oppgaver-service", - pauseAfterEach = Duration.ofMinutes(1) - ) { - service.await().settOppgaverUtgåttBasertPåFrist() - } - launchProcessingLoop( - "avholdt-kalenderavtaler-service", - pauseAfterEach = Duration.ofMinutes(1) - ) { - service.await().settKalenderavtalerAvholdtBasertPåTidspunkt() - } +// val database = openDatabaseAsync(databaseConfig) +// val repoAsync = async { +// SkedulertUtgåttRepository(database.await()) +// } +// launch { +// val repo = repoAsync.await() +// hendelsesstrøm.forEach { hendelse, metadata -> +// repo.oppdaterModellEtterHendelse(hendelse) +// } +// } +// +// val service = async { +// SkedulertUtgåttService( +// repository = repoAsync.await(), +// hendelseProdusent = lagKafkaHendelseProdusent(topic = NOTIFIKASJON_TOPIC) +// ) +// } +// launchProcessingLoop( +// "utgaatt-oppgaver-service", +// pauseAfterEach = Duration.ofMinutes(1) +// ) { +// service.await().settOppgaverUtgåttBasertPåFrist() +// } +// launchProcessingLoop( +// "avholdt-kalenderavtaler-service", +// pauseAfterEach = Duration.ofMinutes(1) +// ) { +// service.await().settKalenderavtalerAvholdtBasertPåTidspunkt() +// } launchHttpServer(httpPort = httpPort) } From 7fc59869f5e9ee2ab251bb9982579c9eea09989a Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Wed, 15 Jan 2025 10:25:25 +0100 Subject: [PATCH 20/27] ready true --- .../SkedulertUtg\303\245tt.kt" | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" index 0c3307782..ae4751940 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" @@ -2,23 +2,23 @@ package no.nav.arbeidsgiver.notifikasjon.skedulert_utgått import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Health +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Subsystem import no.nav.arbeidsgiver.notifikasjon.infrastruktur.http.launchHttpServer -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.HendelsesstrømKafkaImpl -import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.NOTIFIKASJON_TOPIC object SkedulertUtgått { - val databaseConfig = Database.config("skedulert_utgatt_model") - private val hendelsesstrøm by lazy { - HendelsesstrømKafkaImpl( - topic = NOTIFIKASJON_TOPIC, - groupId = "skedulert-utgatt-model-builder", - replayPeriodically = true, - ) - } +// val databaseConfig = Database.config("skedulert_utgatt_model") +// private val hendelsesstrøm by lazy { +// HendelsesstrømKafkaImpl( +// topic = NOTIFIKASJON_TOPIC, +// groupId = "skedulert-utgatt-model-builder", +// replayPeriodically = true, +// ) +// } fun main(httpPort: Int = 8080) { runBlocking(Dispatchers.Default) { + Health.subsystemReady[Subsystem.DATABASE] = true // val database = openDatabaseAsync(databaseConfig) // val repoAsync = async { // SkedulertUtgåttRepository(database.await()) From d35960b0028a22b1cbba06420ceef8cb8744a2f1 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Wed, 15 Jan 2025 10:29:28 +0100 Subject: [PATCH 21/27] dbconfig exists --- .../skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" index ae4751940..9d4c21fa2 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" @@ -2,12 +2,13 @@ package no.nav.arbeidsgiver.notifikasjon.skedulert_utgått import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Health import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Subsystem import no.nav.arbeidsgiver.notifikasjon.infrastruktur.http.launchHttpServer object SkedulertUtgått { -// val databaseConfig = Database.config("skedulert_utgatt_model") + val databaseConfig = Database.config("skedulert_utgatt_model") // private val hendelsesstrøm by lazy { // HendelsesstrømKafkaImpl( // topic = NOTIFIKASJON_TOPIC, From 1881552be71f415afdbc843e4da25a5df90f85e6 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Wed, 15 Jan 2025 12:25:56 +0100 Subject: [PATCH 22/27] better masking appender and add password mask --- .../infrastruktur/logging/MaskingAppender.kt | 6 ++++-- .../infrastruktur/MaskingAppenderTests.kt | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/logging/MaskingAppender.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/logging/MaskingAppender.kt index f754aa6ef..60ea74c5d 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/logging/MaskingAppender.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/logging/MaskingAppender.kt @@ -32,13 +32,15 @@ class MaskingAppender: AppenderBase() { companion object { val FNR = Regex("""(^|\D)\d{11}(?=$|\D)""") val ORGNR = Regex("""(^|\D)\d{9}(?=$|\D)""") - val EPOST = Regex("""(^|\D)[\w.-]+@[\w.-]+(?=$|\D)""") + val EPOST = Regex("""[\w.%+-]+@[\w.%+-]+\.[a-zA-Z]{2,}""") + val PASSWORD = Regex("""password=.*(?=$)""") fun mask(string: String?): String? { return string?.let { FNR.replace(it, "$1***********") .replace(ORGNR, "$1*********") - .replace(EPOST, "$1********") + .replace(EPOST, "********") + .replace(PASSWORD, "password=********") } } } diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/MaskingAppenderTests.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/MaskingAppenderTests.kt index 5de5bd350..2e72b5898 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/MaskingAppenderTests.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/MaskingAppenderTests.kt @@ -31,6 +31,14 @@ class MaskingAppenderTests: DescribeSpec({ it("works fnr=") { mask("fnr=11223344556") shouldNot contain("11223344556") + mask("fnr=11223344556") shouldBe "fnr=***********" + mask("fnr=11223344556lolwat") shouldBe "fnr=***********lolwat" + } + + it("works epost=") { + mask("wat=navn@domene.no") shouldNot contain("navn@domene.no") + mask("wat=navn12@domene.no") shouldNot contain("navn12@domene.no") + mask("wat=navn12@domene.no&noeannet") shouldBe "wat=********&noeannet" } it("works altinn error message") { @@ -41,6 +49,17 @@ class MaskingAppenderTests: DescribeSpec({ """.trimIndent() } } + + describe("Masking password in connection strings") { + it("works for jdbc:url") { + mask( + "jdbc:postgresql://127.0.0.1:5432/bruker-model?user=notifikasjon-bruker-api&password=foobar&socketFactory=com.google.cloud.sql.postgres.SocketFactory&cloudSqlInstance=lol2%3Anorth-pole1%3Anotifikasjon-bruker-api" + ).let { + it shouldNot contain("foobar") + it shouldBe "jdbc:postgresql://127.0.0.1:5432/bruker-model?user=notifikasjon-bruker-api&password=********" + } + } + } }) /* From 6d3dff3b1327fb3d2930e755bf845415f5abca67 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Wed, 15 Jan 2025 12:26:23 +0100 Subject: [PATCH 23/27] =?UTF-8?q?Revert=20"Revert=20"Endre=20Database.Conf?= =?UTF-8?q?ig=20til=20=C3=A5=20bruke=20JDBC=5FURL""?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 3b18870733527ae3503380504db8ea7d76ff85a4. --- .../notifikasjon/infrastruktur/Database.kt | 93 +++++++++++++++---- .../executable/BackupRepositoryPerformance.kt | 4 +- .../infrastruktur/DatabaseConfigTest.kt | 71 ++++++++++++++ .../notifikasjon/util/PostgresTestListener.kt | 22 ++--- 4 files changed, 158 insertions(+), 32 deletions(-) create mode 100644 app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt diff --git a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt index 41840254f..da2d4b72d 100644 --- a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt +++ b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/Database.kt @@ -6,10 +6,13 @@ import kotlinx.coroutines.* import no.nav.arbeidsgiver.notifikasjon.infrastruktur.json.laxObjectMapper import no.nav.arbeidsgiver.notifikasjon.infrastruktur.json.writeValueAsStringSupportingTypeInfoInCollections import no.nav.arbeidsgiver.notifikasjon.infrastruktur.unblocking.NonBlockingDataSource +import org.apache.http.client.utils.URIBuilder +import org.apache.http.message.BasicNameValuePair import org.flywaydb.core.Flyway import org.flywaydb.core.api.configuration.FluentConfiguration import org.intellij.lang.annotations.Language import java.io.Closeable +import java.net.URI import java.sql.* import java.time.* import java.time.temporal.ChronoUnit @@ -24,16 +27,25 @@ class Database private constructor( private val dataSource: NonBlockingDataSource<*>, ) : Closeable { data class Config( - val host: String, - val port: String, - val database: String, - val username: String, - val password: String, + private val jdbcUrl: String, val migrationLocations: String, - val jdbcOpts: Map = mapOf() + val jdbcOpts: Map = mapOf() ) { - val jdbcUrl: String - get() = "jdbc:postgresql://$host:$port/$database?${jdbcOpts.entries.joinToString("&")}" + val url: JdbcUrl = JdbcUrl(jdbcUrl, jdbcOpts) + + val username: String + get() = url.username + val password: String + get() = url.password + val database: String + get() = url.database + + /** + * make a copy but change the database name + */ + fun withDatabase(database: String) = copy( + jdbcUrl = url.withDatabase(database).toString() + ) } override fun close() { @@ -43,12 +55,13 @@ class Database private constructor( companion object { private val log = logger() - fun config(name: String, envPrefix: String = "DB", jdbcOpts: Map = mapOf()) = Config( - host = System.getenv("${envPrefix}_HOST") ?: "localhost", - port = System.getenv("${envPrefix}_PORT") ?: "1337", - username = System.getenv("${envPrefix}_USERNAME") ?: "postgres", - password = System.getenv("${envPrefix}_PASSWORD") ?: "postgres", - database = System.getenv("${envPrefix}_DATABASE") ?: name.replace('_', '-'), + fun config(name: String, envPrefix: String = "DB", jdbcOpts: Map = emptyMap()) = Config( + jdbcUrl = System.getenv("${envPrefix}_JDBC_URL") ?: "jdbc:postgresql://localhost:1337/${ + name.replace( + '_', + '-' + ) + }?password=postgres&user=postgres", migrationLocations = "db/migration/$name", jdbcOpts = jdbcOpts, ) @@ -56,9 +69,7 @@ class Database private constructor( private fun Config.asHikariConfig(): HikariConfig { val config = this return HikariConfig().apply { - jdbcUrl = config.jdbcUrl - username = config.username - password = config.password + jdbcUrl = config.url.toString() driverClassName = "org.postgresql.Driver" metricRegistry = Metrics.meterRegistry minimumIdle = 1 @@ -356,3 +367,51 @@ fun measureSql( @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") return timer.recordCallable(action) } + +/** + * Utility class to aid in constructing and manipulating a jdbc url. + */ +class JdbcUrl( + url: String, + additionalOptions: Map = emptyMap() +) { + + /** + * we need to strip the jdbc: part by using schemeSpecificPart + * so that URI is able to parse correctly. + * the jdbc: prefix is added back in toString() + */ + private val uri = URIBuilder( + URI(url).also { + require(it.scheme == "jdbc") { "not a jdbc url: $url" } + }.schemeSpecificPart + ).also { + if (additionalOptions.isNotEmpty()) { + it.addParameters(additionalOptions.map { (k, v) -> BasicNameValuePair(k, v) }) + } + }.build() + + private val urlParameters = uri.query.split('&').associate { + val parts = it.split('=') + val name = parts.firstOrNull() ?: "" + val value = parts.drop(1).firstOrNull() ?: "" + Pair(name, value) + } + + val username: String + get() = urlParameters["user"]!! + val password: String + get() = urlParameters["password"]!! + val database: String + get() = uri.path.split('/').last() + + override fun toString() = "jdbc:$uri" + + /** + * make a copy but change the database name. used in tests + */ + fun withDatabase(database: String): JdbcUrl { + val newUri = URIBuilder(uri).setPath("/$database").build() + return JdbcUrl("jdbc:$newUri") + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt index 9c0690ebb..2adfb4946 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/executable/BackupRepositoryPerformance.kt @@ -1,10 +1,10 @@ package no.nav.arbeidsgiver.notifikasjon.executable import kotlinx.coroutines.runBlocking -import no.nav.arbeidsgiver.notifikasjon.kafka_backup.KafkaBackup import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database import no.nav.arbeidsgiver.notifikasjon.infrastruktur.json.laxObjectMapper import no.nav.arbeidsgiver.notifikasjon.kafka_backup.BackupRepository +import no.nav.arbeidsgiver.notifikasjon.kafka_backup.KafkaBackup import no.nav.arbeidsgiver.notifikasjon.util.EksempelHendelse import org.apache.kafka.clients.consumer.ConsumerRecord import java.time.Duration @@ -16,7 +16,7 @@ fun main() = runBlocking { Database.openDatabase( KafkaBackup.databaseConfig.copy( // https://github.com/flyway/flyway/issues/2323#issuecomment-804495818 - jdbcOpts = mapOf("preparedStatementCacheQueries" to 0) + jdbcOpts = mapOf("preparedStatementCacheQueries" to "0") ) ).use { database -> diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt new file mode 100644 index 000000000..709d1841e --- /dev/null +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/DatabaseConfigTest.kt @@ -0,0 +1,71 @@ +package no.nav.arbeidsgiver.notifikasjon.infrastruktur + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +class DatabaseConfigTest : DescribeSpec({ + + describe("JdbcUrl") { + it("from jdbcUrl yields correct database, username, password and url") { + val url = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val jdbcUrl = JdbcUrl(url) + + jdbcUrl.database shouldBe "mydb" + jdbcUrl.username shouldBe "myuser" + jdbcUrl.password shouldBe "mypassword" + } + + it("supports additional options via map") { + val url = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val jdbcUrl = JdbcUrl(url, mapOf("foo" to "bar")).toString() + + jdbcUrl shouldBe "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword&foo=bar" + } + + it("can create a copy with another database name") { + val url = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val jdbcUrl = JdbcUrl(url) + + jdbcUrl.withDatabase("anotherdb").toString() shouldBe "jdbc:postgresql://localhost:5432/anotherdb?user=myuser&password=mypassword" + + } + } + + describe("Database.Config") { + it("from jdbcUrl yields correct database, username, password and url") { + val jdbcUrl = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val config = Database.Config( + jdbcUrl = jdbcUrl, + migrationLocations = "", + jdbcOpts = mapOf() + ) + + config.database shouldBe "mydb" + config.username shouldBe "myuser" + config.password shouldBe "mypassword" + config.url.toString() shouldBe jdbcUrl + } + + it("supports additional options via map") { + val jdbcUrl = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val config = Database.Config( + jdbcUrl = jdbcUrl, + migrationLocations = "", + jdbcOpts = mapOf("foo" to "bar") + ) + + config.url.toString() shouldBe "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword&foo=bar" + } + + it("can create a copy with another database name") { + val jdbcUrl = "jdbc:postgresql://localhost:5432/mydb?user=myuser&password=mypassword" + val config = Database.Config( + jdbcUrl = jdbcUrl, + migrationLocations = "", + jdbcOpts = mapOf() + ) + + config.withDatabase("anotherdb").url.toString() shouldBe "jdbc:postgresql://localhost:5432/anotherdb?user=myuser&password=mypassword" + } + } +}) diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt index 6e2d2cd14..5b29bbff0 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/util/PostgresTestListener.kt @@ -19,13 +19,14 @@ private suspend fun createDbFromTemplate(config: Database.Config): Database.Conf val database = mutex.withLock { "${config.database}_test-${ids.next()}" } - DriverManager.getConnection(config.jdbcUrl, config.username, config.password).use { conn -> + + DriverManager.getConnection(config.url.toString(), config.username, config.password).use { conn -> conn.createStatement().use { stmt -> @Suppress("SqlSourceToSinkFlow") stmt.executeUpdate("""create database "$database" template "$templateDb"; """) } } - return config.copy(database = database) + return config.withDatabase(database) } /** @@ -33,10 +34,10 @@ private suspend fun createDbFromTemplate(config: Database.Config): Database.Conf * template databasen brukes til å lage ferske databaser for hver test. */ @Suppress("SqlSourceToSinkFlow") -private suspend fun templateDb(config: Database.Config): String { +private fun templateDb(config: Database.Config): String { val templateDb = templateDbs.computeIfAbsent(config) { "${config.database}_template".also { db -> - DriverManager.getConnection(config.jdbcUrl, config.username, config.password).use { conn -> + DriverManager.getConnection(config.url.toString(), config.username, config.password).use { conn -> conn.createStatement().use { stmt -> val resultSet = stmt.executeQuery("SELECT datname FROM pg_database where datname like '${config.database}_test%';") @@ -58,10 +59,7 @@ private suspend fun templateDb(config: Database.Config): String { } runBlocking { Database.openDatabase( - config = config.copy( - port = "1337", - database = db, - ), + config = config.withDatabase(db), flywayAction = { migrate() } @@ -74,13 +72,11 @@ private suspend fun templateDb(config: Database.Config): String { fun TestConfiguration.testDatabase(config: Database.Config): Database = runBlocking { - val database = createDbFromTemplate(config).database + val testConfig = createDbFromTemplate(config) Database.openDatabase( - config = config.copy( + config = testConfig.copy( // https://github.com/flyway/flyway/issues/2323#issuecomment-804495818 - jdbcOpts = mapOf("preparedStatementCacheQueries" to 0), - port = "1337", - database = database, + jdbcOpts = mapOf("preparedStatementCacheQueries" to "0"), ), flywayAction = { /* noop. created from template. */ From a635190b1d99e2422a86e4289baa0d635d65d068 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Wed, 15 Jan 2025 12:27:13 +0100 Subject: [PATCH 24/27] fjern eksempel --- .../infrastruktur/MaskingAppenderTests.kt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/MaskingAppenderTests.kt b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/MaskingAppenderTests.kt index 2e72b5898..f5b1bebd3 100644 --- a/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/MaskingAppenderTests.kt +++ b/app/src/test/kotlin/no/nav/arbeidsgiver/notifikasjon/infrastruktur/MaskingAppenderTests.kt @@ -61,15 +61,3 @@ class MaskingAppenderTests: DescribeSpec({ } } }) - -/* -Ikke-retryable feil fra altinn ved sending av notifikasjon: AltinnResponse.Feil( - altinnErrorMessage=The ReceiverAddress/User profile must contain a valid emailaddress. Address: merete@appoint-.no, User: 995536021 - altinnExtendedErrorMessage=No information available - altinnLocalizedErrorMessage=The ReceiverAddress/User profile must contain a valid emailaddress. Address: merete@appoint-.no, User: 995536021 - errorGuid=06722395-14dd-4fa3-a789-882553ded9aa - errorID=30010 - userGuid=-no value- - userId=0 - ): - */ \ No newline at end of file From a469fea321cff43731d0f8f38eff150ab0e3a048 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Wed, 15 Jan 2025 12:29:37 +0100 Subject: [PATCH 25/27] =?UTF-8?q?re=20enable=20skedulert=20utg=C3=A5tt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SkedulertUtg\303\245tt.kt" | 80 ++++++++++--------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" index 9d4c21fa2..ab183ddc5 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" @@ -1,54 +1,62 @@ package no.nav.arbeidsgiver.notifikasjon.skedulert_utgått import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Database.Companion.openDatabaseAsync import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Health import no.nav.arbeidsgiver.notifikasjon.infrastruktur.Subsystem import no.nav.arbeidsgiver.notifikasjon.infrastruktur.http.launchHttpServer +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.HendelsesstrømKafkaImpl +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.NOTIFIKASJON_TOPIC +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.kafka.lagKafkaHendelseProdusent +import no.nav.arbeidsgiver.notifikasjon.infrastruktur.launchProcessingLoop +import java.time.Duration object SkedulertUtgått { val databaseConfig = Database.config("skedulert_utgatt_model") -// private val hendelsesstrøm by lazy { -// HendelsesstrømKafkaImpl( -// topic = NOTIFIKASJON_TOPIC, -// groupId = "skedulert-utgatt-model-builder", -// replayPeriodically = true, -// ) -// } + private val hendelsesstrøm by lazy { + HendelsesstrømKafkaImpl( + topic = NOTIFIKASJON_TOPIC, + groupId = "skedulert-utgatt-model-builder", + replayPeriodically = true, + ) + } fun main(httpPort: Int = 8080) { runBlocking(Dispatchers.Default) { Health.subsystemReady[Subsystem.DATABASE] = true -// val database = openDatabaseAsync(databaseConfig) -// val repoAsync = async { -// SkedulertUtgåttRepository(database.await()) -// } -// launch { -// val repo = repoAsync.await() -// hendelsesstrøm.forEach { hendelse, metadata -> -// repo.oppdaterModellEtterHendelse(hendelse) -// } -// } -// -// val service = async { -// SkedulertUtgåttService( -// repository = repoAsync.await(), -// hendelseProdusent = lagKafkaHendelseProdusent(topic = NOTIFIKASJON_TOPIC) -// ) -// } -// launchProcessingLoop( -// "utgaatt-oppgaver-service", -// pauseAfterEach = Duration.ofMinutes(1) -// ) { -// service.await().settOppgaverUtgåttBasertPåFrist() -// } -// launchProcessingLoop( -// "avholdt-kalenderavtaler-service", -// pauseAfterEach = Duration.ofMinutes(1) -// ) { -// service.await().settKalenderavtalerAvholdtBasertPåTidspunkt() -// } + val database = openDatabaseAsync(databaseConfig) + val repoAsync = async { + SkedulertUtgåttRepository(database.await()) + } + launch { + val repo = repoAsync.await() + hendelsesstrøm.forEach { hendelse, _ -> + repo.oppdaterModellEtterHendelse(hendelse) + } + } + + val service = async { + SkedulertUtgåttService( + repository = repoAsync.await(), + hendelseProdusent = lagKafkaHendelseProdusent(topic = NOTIFIKASJON_TOPIC) + ) + } + launchProcessingLoop( + "utgaatt-oppgaver-service", + pauseAfterEach = Duration.ofMinutes(1) + ) { + service.await().settOppgaverUtgåttBasertPåFrist() + } + launchProcessingLoop( + "avholdt-kalenderavtaler-service", + pauseAfterEach = Duration.ofMinutes(1) + ) { + service.await().settKalenderavtalerAvholdtBasertPåTidspunkt() + } launchHttpServer(httpPort = httpPort) } From a5eb1b748fac226a008641fd53b7f02b80159527 Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Wed, 15 Jan 2025 13:14:04 +0100 Subject: [PATCH 26/27] disable opentelemetry --- app/nais/dev-gcp-bruker-api.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/nais/dev-gcp-bruker-api.yaml b/app/nais/dev-gcp-bruker-api.yaml index bb555eb5d..af8aec84d 100644 --- a/app/nais/dev-gcp-bruker-api.yaml +++ b/app/nais/dev-gcp-bruker-api.yaml @@ -25,10 +25,6 @@ spec: prometheus: enabled: true path: /internal/metrics - observability: - autoInstrumentation: - enabled: true - runtime: java tokenx: enabled: true kafka: From 6a2974b5ff58ccbe39f5cb63b7438b0b3b17ceab Mon Sep 17 00:00:00 2001 From: Ken Gullaksen Date: Wed, 15 Jan 2025 14:30:25 +0100 Subject: [PATCH 27/27] new groupid, start from beginning --- .../skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" index ab183ddc5..6f86fa5e9 100644 --- "a/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" +++ "b/app/src/main/kotlin/no/nav/arbeidsgiver/notifikasjon/skedulert_utg\303\245tt/SkedulertUtg\303\245tt.kt" @@ -20,7 +20,7 @@ object SkedulertUtgått { private val hendelsesstrøm by lazy { HendelsesstrømKafkaImpl( topic = NOTIFIKASJON_TOPIC, - groupId = "skedulert-utgatt-model-builder", + groupId = "skedulert-utgatt-model-builder-0", replayPeriodically = true, ) }