From f1e1ae939fa5ab093cbb664a56c704133a82208d Mon Sep 17 00:00:00 2001 From: "Nicolas N." Date: Tue, 26 Nov 2024 13:18:14 +0100 Subject: [PATCH] =?UTF-8?q?EY-4760=20St=C3=B8tte=20oppdatering=20av=20fnr?= =?UTF-8?q?=20p=C3=A5=20sak=20(#6429)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * EY-4777 Fikser hendelser for endret fnr * oppdatere pdltjenester slik at riktig fnr blir returnert * logg for testing * oppdatere visning av fnr samsvar i frontend * Revert "logg for testing" This reverts commit b740d65c9eec9f15164cfc153ca3e2c98623bc97. * EY-4760 Støtte oppdatering av fnr på sak * fikse småting iht. kommentarer * oppdatere tester + bedre dekning * småting iht kommentarer * oppdatere toggle iht endret bruk av toggles * fjerne console.log --- .../GrunnlagsendringshendelseDao.kt | 9 +- .../GrunnlagsendringshendelseService.kt | 19 ++- .../src/main/kotlin/oppgave/OppgaveDao.kt | 25 ++++ .../oppgave/OppgaveDaoMedEndringssporing.kt | 9 ++ .../src/main/kotlin/oppgave/OppgaveService.kt | 11 ++ .../src/main/kotlin/sak/SakRoutes.kt | 55 +++++++- .../src/main/kotlin/sak/SakService.kt | 37 +++++- .../src/main/kotlin/sak/SakSkrivDao.kt | 20 +++ .../src/test/kotlin/DatabaseExtension.kt | 1 + .../test/kotlin/oppgave/OppgaveServiceTest.kt | 86 ++++++++++++- .../src/test/kotlin/sak/SakRoutesTest.kt | 49 +++++++- .../src/test/kotlin/sak/SakServiceTest.kt | 118 ++++++++++++++++-- .../src/test/kotlin/sak/SakSkrivDaoTest.kt | 49 ++++++++ .../hendelser/NyHendelseExpandableRow.tsx | 5 + .../person/hendelser/OppdaterIdentModal.tsx | 96 ++++++++++++++ .../client/src/shared/api/sak.ts | 4 + .../client/src/useUnleash.ts | 6 + .../behandling/AvbrytBehandlingRequest.kt | 1 + 18 files changed, 569 insertions(+), 31 deletions(-) create mode 100644 apps/etterlatte-saksbehandling-ui/client/src/components/person/hendelser/OppdaterIdentModal.tsx diff --git a/apps/etterlatte-behandling/src/main/kotlin/grunnlagsendring/GrunnlagsendringshendelseDao.kt b/apps/etterlatte-behandling/src/main/kotlin/grunnlagsendring/GrunnlagsendringshendelseDao.kt index 9580efe8a06..000364ec3a7 100644 --- a/apps/etterlatte-behandling/src/main/kotlin/grunnlagsendring/GrunnlagsendringshendelseDao.kt +++ b/apps/etterlatte-behandling/src/main/kotlin/grunnlagsendring/GrunnlagsendringshendelseDao.kt @@ -113,7 +113,10 @@ class GrunnlagsendringshendelseDao( } } - fun arkiverGrunnlagsendringStatus(hendelse: Grunnlagsendringshendelse) { + fun arkiverGrunnlagsendringStatus( + hendelseId: UUID, + kommentar: String?, + ) { connectionAutoclosing.hentConnection { with(it) { prepareStatement( @@ -124,9 +127,9 @@ class GrunnlagsendringshendelseDao( WHERE id = ? """.trimIndent(), ).use { - it.setString(1, hendelse.kommentar) + it.setString(1, kommentar) it.setString(2, GrunnlagsendringStatus.VURDERT_SOM_IKKE_RELEVANT.toString()) - it.setObject(3, hendelse.id) + it.setObject(3, hendelseId) it.executeUpdate() } } diff --git a/apps/etterlatte-behandling/src/main/kotlin/grunnlagsendring/GrunnlagsendringshendelseService.kt b/apps/etterlatte-behandling/src/main/kotlin/grunnlagsendring/GrunnlagsendringshendelseService.kt index fab8efc53e9..f82e756163e 100644 --- a/apps/etterlatte-behandling/src/main/kotlin/grunnlagsendring/GrunnlagsendringshendelseService.kt +++ b/apps/etterlatte-behandling/src/main/kotlin/grunnlagsendring/GrunnlagsendringshendelseService.kt @@ -81,20 +81,19 @@ class GrunnlagsendringshendelseService( } fun arkiverHendelseMedKommentar( - hendelse: Grunnlagsendringshendelse, + hendelseId: UUID, + kommentar: String?, saksbehandler: Saksbehandler, ) { - logger.info("Arkiverer hendelse med id ${hendelse.id}") + logger.info("Arkiverer hendelse med id $hendelseId") - inTransaction { - grunnlagsendringshendelseDao.arkiverGrunnlagsendringStatus(hendelse = hendelse) + grunnlagsendringshendelseDao.arkiverGrunnlagsendringStatus(hendelseId, kommentar) - oppgaveService.ferdigStillOppgaveUnderBehandling( - referanse = hendelse.id.toString(), - type = OppgaveType.VURDER_KONSEKVENS, - saksbehandler = saksbehandler, - ) - } + oppgaveService.ferdigStillOppgaveUnderBehandling( + referanse = hendelseId.toString(), + type = OppgaveType.VURDER_KONSEKVENS, + saksbehandler = saksbehandler, + ) } fun settHendelseTilHistorisk(behandlingId: UUID) { diff --git a/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveDao.kt b/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveDao.kt index 16e540a0090..77b91c97f3a 100644 --- a/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveDao.kt +++ b/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveDao.kt @@ -73,6 +73,11 @@ interface OppgaveDao { enhet: Enhetsnummer, ) + fun oppdaterIdent( + oppgaveId: UUID, + nyttFnr: String, + ) + fun fjernSaksbehandler(oppgaveId: UUID) fun settForrigeSaksbehandlerFraSaksbehandler(oppgaveId: UUID) @@ -471,6 +476,26 @@ class OppgaveDaoImpl( } } + override fun oppdaterIdent( + oppgaveId: UUID, + nyttFnr: String, + ) { + connectionAutoclosing.hentConnection { + with(it) { + prepareStatement( + """ + UPDATE oppgave + SET fnr = ? + WHERE id = ?::UUID + """.trimIndent(), + ).apply { + setString(1, nyttFnr) + setObject(2, oppgaveId) + }.executeUpdate() + } + } + } + override fun fjernForrigeSaksbehandler(oppgaveId: UUID) { connectionAutoclosing.hentConnection { with(it) { diff --git a/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveDaoMedEndringssporing.kt b/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveDaoMedEndringssporing.kt index d3e4f4d4fcd..04347da0c4d 100644 --- a/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveDaoMedEndringssporing.kt +++ b/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveDaoMedEndringssporing.kt @@ -165,6 +165,15 @@ class OppgaveDaoMedEndringssporingImpl( } } + override fun oppdaterIdent( + oppgaveId: UUID, + nyttFnr: String, + ) { + lagreEndringerPaaOppgave(oppgaveId) { + oppgaveDao.oppdaterIdent(oppgaveId, nyttFnr) + } + } + override fun fjernSaksbehandler(oppgaveId: UUID) { lagreEndringerPaaOppgave(oppgaveId) { oppgaveDao.fjernSaksbehandler(oppgaveId) diff --git a/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveService.kt b/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveService.kt index 9d31d382f00..34167ec8e4c 100644 --- a/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveService.kt +++ b/apps/etterlatte-behandling/src/main/kotlin/oppgave/OppgaveService.kt @@ -445,6 +445,17 @@ class OppgaveService( } } + fun oppdaterIdentForOppgaver(sak: Sak) { + logger.info("Oppdaterer ident på oppgaver som ikke er avsluttet på sak ${sak.id}") + + oppgaveDao + .hentOppgaverForSakMedType(sak.id, OppgaveType.entries) + .filterNot(OppgaveIntern::erAvsluttet) + .forEach { + oppgaveDao.oppdaterIdent(it.id, sak.ident) + } + } + fun oppdaterEnhetForRelaterteOppgaver(sakerMedNyEnhet: List) { sakerMedNyEnhet.forEach { fjernSaksbehandlerFraOppgaveVedFlytt(it.id) diff --git a/apps/etterlatte-behandling/src/main/kotlin/sak/SakRoutes.kt b/apps/etterlatte-behandling/src/main/kotlin/sak/SakRoutes.kt index dfaba8b615b..f121675f673 100644 --- a/apps/etterlatte-behandling/src/main/kotlin/sak/SakRoutes.kt +++ b/apps/etterlatte-behandling/src/main/kotlin/sak/SakRoutes.kt @@ -19,6 +19,7 @@ import no.nav.etterlatte.grunnlagsendring.GrunnlagsendringshendelseService import no.nav.etterlatte.grunnlagsendring.SakMedEnhet import no.nav.etterlatte.inTransaction import no.nav.etterlatte.libs.common.Enhetsnummer +import no.nav.etterlatte.libs.common.behandling.AarsakTilAvbrytelse import no.nav.etterlatte.libs.common.behandling.FoersteVirkDto import no.nav.etterlatte.libs.common.behandling.SakType import no.nav.etterlatte.libs.common.behandling.SisteIverksatteBehandling @@ -39,6 +40,7 @@ import no.nav.etterlatte.oppgave.OppgaveService import no.nav.etterlatte.tilgangsstyring.kunSaksbehandlerMedSkrivetilgang import no.nav.etterlatte.tilgangsstyring.withFoedselsnummerInternal import org.slf4j.LoggerFactory +import java.util.UUID const val KJOERING = "kjoering" const val ANTALL = "antall" @@ -208,6 +210,47 @@ internal fun Route.sakWebRoutes( ) } + post("/oppdater-ident") { + kunSaksbehandlerMedSkrivetilgang { saksbehandler -> + val hendelseId = + call.request.queryParameters["hendelseId"]?.let(UUID::fromString) + ?: throw UgyldigForespoerselException("HENDELSE_ID_MANGLER", "HendelseID mangler") + + val oppdatertSak = + inTransaction { + val sak = + sakService.finnSak(sakId) + ?: throw SakIkkeFunnetException("Fant ikke sak med id=$sakId") + + logger.info("Oppdaterer sak ${sak.id} og tilhørende oppgaver med nyeste ident fra PDL") + + val oppdatertSak = sakService.oppdaterIdentForSak(sak) + oppgaveService.oppdaterIdentForOppgaver(oppdatertSak) + + behandlingService.hentAapneBehandlingerForSak(sakId).forEach { + behandlingService.avbrytBehandling( + behandlingId = it.behandlingId, + saksbehandler = brukerTokenInfo, + aarsak = AarsakTilAvbrytelse.ENDRET_FOLKEREGISTERIDENT, + kommentar = + "Avbrytes pga. overføring av sak fra fnr. ${sak.ident} " + + "til ny ident ${oppdatertSak.ident}", + ) + } + + grunnlagsendringshendelseService.arkiverHendelseMedKommentar( + hendelseId = hendelseId, + kommentar = "Sak er oppdatert med ny ident på bruker (fra=${sak.ident}, til=${oppdatertSak.ident})", + saksbehandler = saksbehandler, + ) + + oppdatertSak + } + + call.respond(oppdatertSak) + } + } + post("/endre_enhet") { kunSaksbehandlerMedSkrivetilgang { navIdent -> val enhetrequest = call.receive() @@ -324,10 +367,14 @@ internal fun Route.sakWebRoutes( post("arkivergrunnlagsendringshendelse") { kunSaksbehandler { saksbehandler -> val arkivertHendelse = call.receive() - grunnlagsendringshendelseService.arkiverHendelseMedKommentar( - hendelse = arkivertHendelse, - saksbehandler, - ) + + inTransaction { + grunnlagsendringshendelseService.arkiverHendelseMedKommentar( + hendelseId = arkivertHendelse.id, + kommentar = arkivertHendelse.kommentar, + saksbehandler = saksbehandler, + ) + } call.respond(HttpStatusCode.OK) } } diff --git a/apps/etterlatte-behandling/src/main/kotlin/sak/SakService.kt b/apps/etterlatte-behandling/src/main/kotlin/sak/SakService.kt index 27459647364..67c7ec977da 100644 --- a/apps/etterlatte-behandling/src/main/kotlin/sak/SakService.kt +++ b/apps/etterlatte-behandling/src/main/kotlin/sak/SakService.kt @@ -98,6 +98,8 @@ interface SakService { sakId: SakId, bruker: Systembruker, ): SakMedGraderingOgSkjermet + + fun oppdaterIdentForSak(sak: Sak): Sak } class ManglerTilgangTilEnhet( @@ -218,7 +220,13 @@ class SakServiceImpl( ): Sak { val sak = finnEllerOpprettSak(fnr, type, overstyrendeEnhet) - runBlocking { grunnlagService.leggInnNyttGrunnlagSak(sak, Persongalleri(sak.ident), HardkodaSystembruker.opprettGrunnlag) } + runBlocking { + grunnlagService.leggInnNyttGrunnlagSak( + sak, + Persongalleri(sak.ident), + HardkodaSystembruker.opprettGrunnlag, + ) + } val kilde = Grunnlagsopplysning.Gjenny(Fagsaksystem.EY.navn, Tidspunkt.now()) val spraak = hentSpraak(sak.ident) val spraakOpplysning = lagOpplysning(Opplysningstype.SPRAAK, kilde, spraak.verdi.toJsonNode()) @@ -232,6 +240,33 @@ class SakServiceImpl( return sak } + override fun oppdaterIdentForSak(sak: Sak): Sak { + val identListe = runBlocking { pdltjenesterKlient.hentPdlFolkeregisterIdenter(sak.ident) } + + val alleIdenter = identListe.identifikatorer.map { it.folkeregisterident.value } + if (sak.ident !in alleIdenter) { + sikkerLogg.error("Ident ${sak.ident} fra sak ${sak.id} matcher ingen av identene fra PDL: $alleIdenter") + throw InternfeilException( + "Ident i sak ${sak.id} stemmer ikke overens med identer vi fikk fra PDL", + ) + } + + val gjeldendeIdent = + identListe.identifikatorer.singleOrNull { !it.historisk }?.folkeregisterident + ?: throw InternfeilException("Sak ${sak.id} har flere eller ingen gyldige identer samtidig. Kan ikke oppdatere ident.") + + dao.oppdaterIdent(sak.id, gjeldendeIdent) + + logger.info("Oppdaterte sak ${sak.id} med bruker sin nyeste ident. Se sikkerlogg for detailjer") + sikkerLogg.info( + "Oppdaterte sak ${sak.id}: Endret ident fra ${sak.ident} til ${gjeldendeIdent.value}. " + + "Alle identer fra PDL: ${identListe.identifikatorer.joinToString()}", + ) + + return lesDao.hentSak(sak.id) + ?: throw InternfeilException("Kunne ikke hente ut sak ${sak.id} som nettopp ble endret") + } + private fun hentSpraak(fnr: String): Spraak { val kontaktInfo = runBlocking { diff --git a/apps/etterlatte-behandling/src/main/kotlin/sak/SakSkrivDao.kt b/apps/etterlatte-behandling/src/main/kotlin/sak/SakSkrivDao.kt index 74086931d83..4c19dbed9fb 100644 --- a/apps/etterlatte-behandling/src/main/kotlin/sak/SakSkrivDao.kt +++ b/apps/etterlatte-behandling/src/main/kotlin/sak/SakSkrivDao.kt @@ -5,6 +5,7 @@ import no.nav.etterlatte.libs.common.Enhetsnummer import no.nav.etterlatte.libs.common.behandling.SakType import no.nav.etterlatte.libs.common.feilhaandtering.checkInternFeil import no.nav.etterlatte.libs.common.person.AdressebeskyttelseGradering +import no.nav.etterlatte.libs.common.person.Folkeregisteridentifikator import no.nav.etterlatte.libs.common.sak.Sak import no.nav.etterlatte.libs.common.sak.SakId import no.nav.etterlatte.libs.common.tidspunkt.Tidspunkt @@ -69,6 +70,25 @@ class SakSkrivDao( } } + fun oppdaterIdent( + sakId: SakId, + nyIdent: Folkeregisteridentifikator, + ) { + sakendringerDao.lagreEndringerPaaSak(sakId, "oppdaterIdent") { + it + .prepareStatement( + """ + UPDATE sak + SET fnr = ? + WHERE id = ? + """.trimIndent(), + ).apply { + setString(1, nyIdent.value) + setLong(2, sakId.sakId) + }.executeUpdate() + } + } + fun oppdaterEnheterPaaSaker(saker: List) { sakendringerDao.lagreEndringerPaaSaker(saker.map { it.id }, "oppdaterEnheterPaaSaker") { with(it) { diff --git a/apps/etterlatte-behandling/src/test/kotlin/DatabaseExtension.kt b/apps/etterlatte-behandling/src/test/kotlin/DatabaseExtension.kt index 1fd37b6f2d5..f343aca9f2b 100644 --- a/apps/etterlatte-behandling/src/test/kotlin/DatabaseExtension.kt +++ b/apps/etterlatte-behandling/src/test/kotlin/DatabaseExtension.kt @@ -6,6 +6,7 @@ package no.nav.etterlatte TRUNCATE behandlinghendelse CASCADE; TRUNCATE grunnlagsendringshendelse CASCADE; TRUNCATE sak CASCADE; + TRUNCATE endringer CASCADE; TRUNCATE oppgave CASCADE; TRUNCATE tilbakekrevingsperiode CASCADE; TRUNCATE tilbakekreving CASCADE; diff --git a/apps/etterlatte-behandling/src/test/kotlin/oppgave/OppgaveServiceTest.kt b/apps/etterlatte-behandling/src/test/kotlin/oppgave/OppgaveServiceTest.kt index c73ee4f4a92..6f2efd6bdd6 100644 --- a/apps/etterlatte-behandling/src/test/kotlin/oppgave/OppgaveServiceTest.kt +++ b/apps/etterlatte-behandling/src/test/kotlin/oppgave/OppgaveServiceTest.kt @@ -1,6 +1,7 @@ package no.nav.etterlatte.oppgave import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk @@ -9,6 +10,8 @@ import io.mockk.verify import no.nav.etterlatte.ConnectionAutoclosingTest import no.nav.etterlatte.DatabaseContextTest import no.nav.etterlatte.DatabaseExtension +import no.nav.etterlatte.JOVIAL_LAMA +import no.nav.etterlatte.KONTANT_FOT import no.nav.etterlatte.SaksbehandlerMedEnheterOgRoller import no.nav.etterlatte.SystemUser import no.nav.etterlatte.azureAdAttestantClaim @@ -25,10 +28,12 @@ import no.nav.etterlatte.libs.common.behandling.PaaVentAarsak import no.nav.etterlatte.libs.common.behandling.SakType import no.nav.etterlatte.libs.common.feilhaandtering.InternfeilException import no.nav.etterlatte.libs.common.feilhaandtering.UgyldigForespoerselException +import no.nav.etterlatte.libs.common.oppgave.OppgaveIntern import no.nav.etterlatte.libs.common.oppgave.OppgaveKilde import no.nav.etterlatte.libs.common.oppgave.OppgaveType import no.nav.etterlatte.libs.common.oppgave.Status import no.nav.etterlatte.libs.common.person.AdressebeskyttelseGradering +import no.nav.etterlatte.libs.common.sak.SakId import no.nav.etterlatte.libs.common.tidspunkt.Tidspunkt import no.nav.etterlatte.libs.common.tidspunkt.toLocalDatetimeUTC import no.nav.etterlatte.libs.common.tidspunkt.toTidspunkt @@ -52,6 +57,7 @@ import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith import java.util.UUID import javax.sql.DataSource +import kotlin.random.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) @ExtendWith(DatabaseExtension::class) @@ -223,7 +229,8 @@ internal class OppgaveServiceTest( null, ) - val saksbehandlerto = mockk { every { name() } returns vanligSaksbehandler.ident } + val saksbehandlerto = + mockk { every { name() } returns vanligSaksbehandler.ident } nyKontekstMedBruker(saksbehandlerto) val saksbehandlerMedRoller = generateSaksbehandlerMedRoller(AzureGroup.SAKSBEHANDLER) mockForSaksbehandlerMedRoller(saksbehandlerto, saksbehandlerMedRoller) @@ -256,7 +263,8 @@ internal class OppgaveServiceTest( null, ) - val saksbehandlerto = mockk { every { name() } returns vanligSaksbehandler.ident } + val saksbehandlerto = + mockk { every { name() } returns vanligSaksbehandler.ident } nyKontekstMedBruker(saksbehandlerto) val saksbehandlerMedRoller = generateSaksbehandlerMedRoller(AzureGroup.ATTESTANT) mockForSaksbehandlerMedRoller(saksbehandlerto, saksbehandlerMedRoller) @@ -287,7 +295,8 @@ internal class OppgaveServiceTest( null, ) - val saksbehandlerto = mockk { every { name() } returns vanligSaksbehandler.ident } + val saksbehandlerto = + mockk { every { name() } returns vanligSaksbehandler.ident } nyKontekstMedBruker(saksbehandlerto) val saksbehandlerMedRoller = generateSaksbehandlerMedRoller(AzureGroup.ATTESTANT) mockForSaksbehandlerMedRoller(saksbehandlerto, saksbehandlerMedRoller) @@ -1371,4 +1380,75 @@ internal class OppgaveServiceTest( oppdatertOppgave.status shouldBe Status.FERDIGSTILT forrigeStatus shouldBe Status.UNDER_BEHANDLING } + + @Test + fun `Oppdater ident på oppgaver tilknyttet sak`() { + val opprinneligIdent = KONTANT_FOT + + val sak = + sakSkrivDao.opprettSak( + fnr = opprinneligIdent.value, + type = SakType.OMSTILLINGSSTOENAD, + enhet = Enheter.defaultEnhet.enhetNr, + ) + val sakId = sak.id + + // 5 oppgaver skal få ny ident + opprettOppgave(sakId = sakId, type = OppgaveType.VURDER_KONSEKVENS, status = Status.NY) + opprettOppgave(sakId = sakId, type = OppgaveType.VURDER_KONSEKVENS, status = Status.UNDER_BEHANDLING) + opprettOppgave(sakId = sakId, type = OppgaveType.VURDER_KONSEKVENS, status = Status.UNDERKJENT) + opprettOppgave(sakId = sakId, type = OppgaveType.VURDER_KONSEKVENS, status = Status.PAA_VENT) + opprettOppgave(sakId = sakId, type = OppgaveType.VURDER_KONSEKVENS, status = Status.ATTESTERING) + + // 3 avsluttede oppgaver skal beholde gammel ident for historikk + opprettOppgave(sakId = sakId, type = OppgaveType.VURDER_KONSEKVENS, status = Status.FEILREGISTRERT) + opprettOppgave(sakId = sakId, type = OppgaveType.VURDER_KONSEKVENS, status = Status.FERDIGSTILT) + opprettOppgave(sakId = sakId, type = OppgaveType.VURDER_KONSEKVENS, status = Status.AVBRUTT) + + val nyIdent = JOVIAL_LAMA + sakSkrivDao.oppdaterIdent(sakId, nyIdent) + + val oppdatertSak = sakLesDao.hentSak(sakId)!! + oppdatertSak.ident shouldBe nyIdent.value + opprinneligIdent shouldNotBe nyIdent + + oppgaveService.oppdaterIdentForOppgaver(oppdatertSak) + + val oppgaver = oppgaveService.hentOppgaverForSak(sakId) + + oppgaver.size shouldBe 8 + + oppgaver.forEach { + if (it.erAvsluttet()) { + it.fnr shouldBe opprinneligIdent.value + } else { + it.fnr shouldBe nyIdent.value + } + } + } + + private fun opprettOppgave( + referanse: String = UUID.randomUUID().toString(), + sakId: SakId = SakId(Random.nextLong()), + kilde: OppgaveKilde? = null, + type: OppgaveType, + merknad: String? = "en tilfeldig merknad", + frist: Tidspunkt? = null, + saksbehandler: String? = null, + status: Status? = null, + ): OppgaveIntern = + oppgaveService + .opprettOppgave( + referanse = referanse, + sakId = sakId, + kilde = kilde, + type = type, + merknad = merknad, + frist = frist, + saksbehandler = saksbehandler, + ).also { + if (status != null) { + oppgaveService.oppdaterStatusOgMerknad(it.id, "ny merknad", status) + } + } } diff --git a/apps/etterlatte-behandling/src/test/kotlin/sak/SakRoutesTest.kt b/apps/etterlatte-behandling/src/test/kotlin/sak/SakRoutesTest.kt index b2be5f9893d..4e865dc17a7 100644 --- a/apps/etterlatte-behandling/src/test/kotlin/sak/SakRoutesTest.kt +++ b/apps/etterlatte-behandling/src/test/kotlin/sak/SakRoutesTest.kt @@ -16,6 +16,7 @@ import io.mockk.just import io.mockk.mockk import io.mockk.runs import io.mockk.verify +import no.nav.etterlatte.KONTANT_FOT import no.nav.etterlatte.SaksbehandlerMedEnheterOgRoller import no.nav.etterlatte.attachMockContext import no.nav.etterlatte.behandling.BehandlingRequestLogger @@ -29,12 +30,15 @@ import no.nav.etterlatte.ktor.runServer import no.nav.etterlatte.ktor.startRandomPort import no.nav.etterlatte.ktor.token.issueSaksbehandlerToken import no.nav.etterlatte.libs.common.Enhetsnummer +import no.nav.etterlatte.libs.common.behandling.AarsakTilAvbrytelse import no.nav.etterlatte.libs.common.behandling.SakType import no.nav.etterlatte.libs.common.oppgave.OppgaveIntern import no.nav.etterlatte.libs.common.oppgave.OppgaveSaksbehandler import no.nav.etterlatte.libs.common.oppgave.OppgaveType import no.nav.etterlatte.libs.common.oppgave.Status +import no.nav.etterlatte.libs.common.sak.BehandlingOgSak import no.nav.etterlatte.libs.common.sak.Sak +import no.nav.etterlatte.libs.common.sak.SakId import no.nav.etterlatte.libs.common.tidspunkt.Tidspunkt import no.nav.etterlatte.oppgave.OppgaveService import no.nav.security.mock.oauth2.MockOAuth2Server @@ -45,6 +49,7 @@ import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import java.util.UUID +import kotlin.random.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class SakRoutesTest { @@ -214,8 +219,50 @@ internal class SakRoutesTest { } } + @Test + fun `Oppdater ident på sak`() { + val nyIdent = KONTANT_FOT + val sakId = SakId(Random.nextLong()) + val hendelseId = UUID.randomUUID() + val sak = + Sak( + "ident", + SakType.OMSTILLINGSSTOENAD, + sakId, + Enheter.defaultEnhet.enhetNr, + ) + + every { sakService.finnSak(any()) } returns sak + every { sakService.oppdaterIdentForSak(any()) } returns sak.copy(ident = nyIdent.value) + val behandlingOgSak = BehandlingOgSak(UUID.randomUUID(), sakId) + every { behandlingService.hentAapneBehandlingerForSak(sakId) } returns listOf(behandlingOgSak) + + withTestApplication { client -> + val response = + client.post("/api/sak/$sakId/oppdater-ident?hendelseId=$hendelseId") { + header(HttpHeaders.Authorization, "Bearer $token") + contentType(ContentType.Application.Json) + } + assertEquals(200, response.status.value) + } + + verify(exactly = 1) { + sakService.finnSak(sakId) + sakService.oppdaterIdentForSak(sak) + behandlingService.hentAapneBehandlingerForSak(sakId) + behandlingService.avbrytBehandling( + behandlingOgSak.behandlingId, + any(), + AarsakTilAvbrytelse.ENDRET_FOLKEREGISTERIDENT, + any(), + ) + grunnlagsendringshendelseService.arkiverHendelseMedKommentar(hendelseId, any(), any()) + } + } + private fun withTestApplication(block: suspend (client: HttpClient) -> Unit) { - val user = mockk().also { every { it.name() } returns this::class.java.simpleName } + val user = + mockk().also { every { it.name() } returns this::class.java.simpleName } every { user.enheterMedSkrivetilgang() } returns listOf(Enheter.defaultEnhet.enhetNr) diff --git a/apps/etterlatte-behandling/src/test/kotlin/sak/SakServiceTest.kt b/apps/etterlatte-behandling/src/test/kotlin/sak/SakServiceTest.kt index befcc01a943..24af2fcc9d4 100644 --- a/apps/etterlatte-behandling/src/test/kotlin/sak/SakServiceTest.kt +++ b/apps/etterlatte-behandling/src/test/kotlin/sak/SakServiceTest.kt @@ -2,6 +2,7 @@ package no.nav.etterlatte.sak import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain import io.ktor.client.plugins.ResponseException import io.mockk.Called @@ -11,6 +12,7 @@ import io.mockk.coVerify import io.mockk.confirmVerified import io.mockk.every import io.mockk.just +import io.mockk.justRun import io.mockk.mockk import io.mockk.runs import io.mockk.spyk @@ -764,7 +766,13 @@ internal class SakServiceTest { val ident1 = LITE_BARN.value val ident2 = KONTANT_FOT.value - coEvery { pdlTjenesterKlient.hentPdlFolkeregisterIdenter(any()) } returns dummyPdlResponse(ident1, ident2) + coEvery { pdlTjenesterKlient.hentPdlFolkeregisterIdenter(any()) } returns + PdlFolkeregisterIdentListe( + listOf( + PdlIdentifikator.FolkeregisterIdent(LITE_BARN, true), + PdlIdentifikator.FolkeregisterIdent(KONTANT_FOT, false), + ), + ) every { sakLesDao.finnSaker(ident1, sakType) } returns listOf(dummySak(ident1, sakType)) every { sakLesDao.finnSaker(ident2, sakType) } returns emptyList() @@ -785,7 +793,14 @@ internal class SakServiceTest { val ident1 = LITE_BARN.value val ident2 = KONTANT_FOT.value - coEvery { pdlTjenesterKlient.hentPdlFolkeregisterIdenter(any()) } returns dummyPdlResponse(ident1, ident2) + coEvery { pdlTjenesterKlient.hentPdlFolkeregisterIdenter(any()) } returns + PdlFolkeregisterIdentListe( + listOf( + PdlIdentifikator.FolkeregisterIdent(LITE_BARN, true), + PdlIdentifikator.FolkeregisterIdent(KONTANT_FOT, false), + ), + ) + every { sakLesDao.finnSaker(ident1, sakType) } returns listOf(dummySak(ident1, sakType)) every { sakLesDao.finnSaker(ident2, sakType) } returns listOf(dummySak(ident2, sakType)) @@ -803,6 +818,91 @@ internal class SakServiceTest { } } + @Nested + inner class TestOppdaterIdentForSak { + @Test + fun `Sak ident ikke funnet i PDL`() { + val sak = dummySak(ident = KONTANT_FOT.value, SakType.OMSTILLINGSSTOENAD) + + coEvery { pdlTjenesterKlient.hentPdlFolkeregisterIdenter(any()) } returns + PdlFolkeregisterIdentListe( + emptyList(), + ) + + assertThrows { + service.oppdaterIdentForSak(sak) + } + + coVerify(exactly = 1) { + pdlTjenesterKlient.hentPdlFolkeregisterIdenter(sak.ident) + } + + verify { + sakLesDao wasNot Called + sakSkrivDao wasNot Called + } + } + + @Test + fun `Bruker har flere gyldige identer i PDL`() { + val sak = dummySak(ident = KONTANT_FOT.value, SakType.OMSTILLINGSSTOENAD) + + coEvery { pdlTjenesterKlient.hentPdlFolkeregisterIdenter(any()) } returns + PdlFolkeregisterIdentListe( + listOf( + PdlIdentifikator.FolkeregisterIdent(KONTANT_FOT), + PdlIdentifikator.FolkeregisterIdent(JOVIAL_LAMA), + ), + ) + + assertThrows { + service.oppdaterIdentForSak(sak) + } + + coVerify(exactly = 1) { + pdlTjenesterKlient.hentPdlFolkeregisterIdenter(sak.ident) + } + + verify { + sakLesDao wasNot Called + sakSkrivDao wasNot Called + } + } + + @Test + fun `Bruker har historisk ident`() { + val sak = dummySak(ident = KONTANT_FOT.value, SakType.OMSTILLINGSSTOENAD) + + coEvery { pdlTjenesterKlient.hentPdlFolkeregisterIdenter(any()) } returns + PdlFolkeregisterIdentListe( + listOf( + PdlIdentifikator.FolkeregisterIdent(KONTANT_FOT, true), + PdlIdentifikator.FolkeregisterIdent(JOVIAL_LAMA, false), + ), + ) + justRun { sakSkrivDao.oppdaterIdent(any(), any()) } + every { sakLesDao.hentSak(any()) } returns sak.copy(ident = JOVIAL_LAMA.value) + + val oppdatertSak = service.oppdaterIdentForSak(sak) + + oppdatertSak.id shouldBe sak.id + oppdatertSak.enhet shouldBe sak.enhet + oppdatertSak.sakType shouldBe sak.sakType + + oppdatertSak.ident shouldNotBe sak.ident + oppdatertSak.ident shouldBe JOVIAL_LAMA.value + + coVerify(exactly = 1) { + pdlTjenesterKlient.hentPdlFolkeregisterIdenter(sak.ident) + } + + verify(exactly = 1) { + sakSkrivDao.oppdaterIdent(sak.id, JOVIAL_LAMA) + sakLesDao.hentSak(sak.id) + } + } + } + private fun dummySak( ident: String, sakType: SakType, @@ -814,13 +914,13 @@ internal class SakServiceTest { enhet = enhet.enhetNr, ) - private fun dummyPdlResponse(vararg identer: String) = + private fun dummyPdlResponse(ident: String) = PdlFolkeregisterIdentListe( - identer.mapIndexed { index, ident -> - PdlIdentifikator.FolkeregisterIdent( - folkeregisterident = Folkeregisteridentifikator.of(ident), - historisk = (identer.size > 1 && index == 1), // første i listen vil bli historisk hvis flere enn 1 - ) - }, + identifikatorer = + listOf( + PdlIdentifikator.FolkeregisterIdent( + folkeregisterident = Folkeregisteridentifikator.of(ident), + ), + ), ) } diff --git a/apps/etterlatte-behandling/src/test/kotlin/sak/SakSkrivDaoTest.kt b/apps/etterlatte-behandling/src/test/kotlin/sak/SakSkrivDaoTest.kt index f5047860b8e..fcdb0dc0059 100644 --- a/apps/etterlatte-behandling/src/test/kotlin/sak/SakSkrivDaoTest.kt +++ b/apps/etterlatte-behandling/src/test/kotlin/sak/SakSkrivDaoTest.kt @@ -10,6 +10,7 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import no.nav.etterlatte.ConnectionAutoclosingTest import no.nav.etterlatte.DatabaseExtension +import no.nav.etterlatte.KONTANT_FOT import no.nav.etterlatte.Kontekst import no.nav.etterlatte.behandling.BehandlingDao import no.nav.etterlatte.behandling.kommerbarnettilgode.KommerBarnetTilGodeDao @@ -21,6 +22,7 @@ import no.nav.etterlatte.grunnlagsendring.SakMedEnhet import no.nav.etterlatte.libs.common.behandling.BehandlingType import no.nav.etterlatte.libs.common.behandling.Prosesstype import no.nav.etterlatte.libs.common.behandling.SakType +import no.nav.etterlatte.libs.common.deserialize import no.nav.etterlatte.libs.common.sak.KjoeringRequest import no.nav.etterlatte.libs.common.sak.KjoeringStatus import no.nav.etterlatte.libs.common.sak.Sak @@ -36,6 +38,7 @@ import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.extension.RegisterExtension import java.time.LocalDate import javax.sql.DataSource +import kotlin.random.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class SakSkrivDaoTest( @@ -436,4 +439,50 @@ internal class SakSkrivDaoTest( saker.size shouldBe 2 } } + + @Test + fun `oppdater ident på sak`() { + val opprinneligIdent = Random.nextLong().toString() + val sak = sakRepo.opprettSak(opprinneligIdent, SakType.BARNEPENSJON, Enheter.PORSGRUNN.enhetNr) + + val nyIdent = KONTANT_FOT + + sakRepo.oppdaterIdent(sak.id, nyIdent) + + val oppdatertSak = sakLesDao.hentSak(sak.id)!! + + oppdatertSak.id shouldBe sak.id + oppdatertSak.enhet shouldBe sak.enhet + oppdatertSak.sakType shouldBe sak.sakType + + oppdatertSak.ident shouldNotBe sak.ident + oppdatertSak.ident shouldBe nyIdent.value + + val endringer: List> = + dataSource.connection.use { + it + .prepareStatement( + """ + SELECT foer, etter + FROM endringer + WHERE tabell = 'sak' + AND foer ->> 'id' = '${sak.id}' + """.trimIndent(), + ).executeQuery() + .toList { + deserialize(getString("foer")) to deserialize(getString("etter")) + } + } + + endringer.size shouldBe 1 + + endringer.single().let { (foer, etter) -> + foer.id shouldBe etter.id + foer.enhet shouldBe etter.enhet + foer.sakType shouldBe etter.sakType + + foer.ident shouldBe opprinneligIdent + etter.ident shouldBe nyIdent.value + } + } } diff --git a/apps/etterlatte-saksbehandling-ui/client/src/components/person/hendelser/NyHendelseExpandableRow.tsx b/apps/etterlatte-saksbehandling-ui/client/src/components/person/hendelser/NyHendelseExpandableRow.tsx index f5cd45983d3..7bf61bf94e3 100644 --- a/apps/etterlatte-saksbehandling-ui/client/src/components/person/hendelser/NyHendelseExpandableRow.tsx +++ b/apps/etterlatte-saksbehandling-ui/client/src/components/person/hendelser/NyHendelseExpandableRow.tsx @@ -15,6 +15,7 @@ import { useInnloggetSaksbehandler } from '~components/behandling/useInnloggetSa import { ArkiverHendelseModal } from '~components/person/hendelser/ArkiverHendelseModal' import { useSearchParams } from 'react-router-dom' import { OpprettRevurderingModal } from '~components/person/OpprettRevurderingModal' +import { OppdaterIdentModal } from '~components/person/hendelser/OppdaterIdentModal' interface Props { hendelse: Grunnlagsendringshendelse @@ -64,6 +65,10 @@ export const NyHendelseExpandableRow = ({ hendelse, sak, behandlinger, revurderi revurderingKanOpprettes(behandlinger, sak.enhet, innloggetSaksbehandler.enheter) && ( )} + + {hendelse.samsvarMellomKildeOgGrunnlag.type === 'FOLKEREGISTERIDENTIFIKATOR' && ( + + )} } diff --git a/apps/etterlatte-saksbehandling-ui/client/src/components/person/hendelser/OppdaterIdentModal.tsx b/apps/etterlatte-saksbehandling-ui/client/src/components/person/hendelser/OppdaterIdentModal.tsx new file mode 100644 index 00000000000..a4ad00b063f --- /dev/null +++ b/apps/etterlatte-saksbehandling-ui/client/src/components/person/hendelser/OppdaterIdentModal.tsx @@ -0,0 +1,96 @@ +import { Alert, Button, Heading, HStack, Loader, Modal, Tag, VStack } from '@navikt/ds-react' +import React, { useState } from 'react' +import { useApiCall } from '~shared/hooks/useApiCall' +import { oppdaterIdentPaaSak } from '~shared/api/sak' +import { isPending, isSuccess, mapFailure } from '~shared/api/apiUtils' +import { Folkeregisteridentifikatorsamsvar, Grunnlagsendringshendelse } from '~components/person/typer' +import { ISakMedUtlandstilknytning } from '~shared/types/sak' +import { ApiErrorAlert } from '~ErrorBoundary' +import { useNavigate } from 'react-router-dom' +import { FeatureToggle, useFeaturetoggle } from '~useUnleash' + +export const OppdaterIdentModal = ({ + sak, + hendelse, +}: { + sak: ISakMedUtlandstilknytning + hendelse: Grunnlagsendringshendelse +}) => { + const samsvar = hendelse.samsvarMellomKildeOgGrunnlag as Folkeregisteridentifikatorsamsvar + + const navigate = useNavigate() + const [isOpen, setIsOpen] = useState(false) + + const [oppdaterIdentResult, apiOppdaterIdentPaaSak] = useApiCall(oppdaterIdentPaaSak) + const kanOppdatereIdentPaaSak = useFeaturetoggle(FeatureToggle.pensjon_etterlatte_oppdater_ident_paa_sak) + + const oppdaterIdent = () => { + apiOppdaterIdentPaaSak({ sakId: sak.id, hendelseId: hendelse.id }, (sak) => { + setTimeout(() => navigate('/person', { state: { fnr: sak.ident } }), 3000) + }) + } + + if (!kanOppdatereIdentPaaSak) { + return null + } + + return ( + <> + + + setIsOpen(false)} + aria-labelledby="modal-heading" + header={{ heading: 'Endre til nyeste ident på sak' }} + > + + {isSuccess(oppdaterIdentResult) ? ( + + Sak og tilhørende oppgaver oppdatert med bruker sin nyeste ident. Laster siden på nytt... + + ) : ( + + +
+ Ident i sak + {sak.ident} +
+ +
+ Ident i Grunnlag + {samsvar.fraGrunnlag} +
+ +
+ Ident i PDL + {samsvar.fraPdl} +
+
+ + + Endring av identifikator vil avbryte alle pågående behandlinger for å sikre at grunnlaget blir korrekt +
+ Behandlinger tilknyttet klage, tilbakekreving, eller kravpakke må manuelt avbrytes. +
+ + {mapFailure(oppdaterIdentResult, (error) => ( + {error.detail} + ))} + + + + + + +
+ )} +
+
+ + ) +} diff --git a/apps/etterlatte-saksbehandling-ui/client/src/shared/api/sak.ts b/apps/etterlatte-saksbehandling-ui/client/src/shared/api/sak.ts index e600b3ca633..4dfe01088bb 100644 --- a/apps/etterlatte-saksbehandling-ui/client/src/shared/api/sak.ts +++ b/apps/etterlatte-saksbehandling-ui/client/src/shared/api/sak.ts @@ -42,3 +42,7 @@ export const hentSakForPerson = async (args: { export const byttEnhetPaaSak = async (args: { sakId: number; enhet: string }): Promise> => { return apiClient.post(`sak/${args.sakId}/endre_enhet`, { enhet: args.enhet }) } + +export const oppdaterIdentPaaSak = async (args: { sakId: number; hendelseId: string }): Promise> => { + return apiClient.post(`sak/${args.sakId}/oppdater-ident?hendelseId=${args.hendelseId}`, {}) +} diff --git a/apps/etterlatte-saksbehandling-ui/client/src/useUnleash.ts b/apps/etterlatte-saksbehandling-ui/client/src/useUnleash.ts index ee59b5f6200..6ce007cf908 100644 --- a/apps/etterlatte-saksbehandling-ui/client/src/useUnleash.ts +++ b/apps/etterlatte-saksbehandling-ui/client/src/useUnleash.ts @@ -18,6 +18,7 @@ export const enum FeatureToggle { opprette_generell_oppgave = 'opprette-generell-oppgave', pensjon_etterlatte_klage_delvis_omgjoering = 'pensjon-etterlatte.klage-delvis-omgjoering', pensjon_etterlatte_kan_opprette_vedtak_avvist_klage = 'pensjon-etterlatte.kan-opprette-vedtak-avvist-klage', + pensjon_etterlatte_oppdater_ident_paa_sak = 'pensjon-etterlatte.oppdater-ident-paa-sak', } export interface Toggle { @@ -63,6 +64,10 @@ const pensjon_etterlatte_kan_opprette_vedtak_avvist_klage: Toggle = { togglename: FeatureToggle.pensjon_etterlatte_kan_opprette_vedtak_avvist_klage, enabled: false, } +const pensjon_etterlatte_oppdater_ident_paa_sak: Toggle = { + togglename: FeatureToggle.pensjon_etterlatte_oppdater_ident_paa_sak, + enabled: false, +} export const unleashStartState: Record = { sanksjon: sanksjon, @@ -76,6 +81,7 @@ export const unleashStartState: Record = { 'pensjon-etterlatte.klage-delvis-omgjoering': pensjon_etterlatte_klage_delvis_omgjoering, 'pensjon-etterlatte.kan-opprette-vedtak-avvist-klage': pensjon_etterlatte_kan_opprette_vedtak_avvist_klage, 'overstyr-beregning-knapp': overstyr_beregning_knapp, + [FeatureToggle.pensjon_etterlatte_oppdater_ident_paa_sak]: pensjon_etterlatte_oppdater_ident_paa_sak, } export const Unleashcontext = createContext<{ diff --git a/libs/etterlatte-behandling-model/src/main/kotlin/no/nav/etterlatte/libs/common/behandling/AvbrytBehandlingRequest.kt b/libs/etterlatte-behandling-model/src/main/kotlin/no/nav/etterlatte/libs/common/behandling/AvbrytBehandlingRequest.kt index 82a575c50f8..9babedcde88 100644 --- a/libs/etterlatte-behandling-model/src/main/kotlin/no/nav/etterlatte/libs/common/behandling/AvbrytBehandlingRequest.kt +++ b/libs/etterlatte-behandling-model/src/main/kotlin/no/nav/etterlatte/libs/common/behandling/AvbrytBehandlingRequest.kt @@ -10,5 +10,6 @@ enum class AarsakTilAvbrytelse { SOEKNADEN_ER_IKKE_AKTUELL, FEILREGISTRERT, AVBRUTT_PAA_GRUNN_AV_FEIL, + ENDRET_FOLKEREGISTERIDENT, ANNET, }