diff --git a/src/main/kotlin/no/nav/familie/ba/infotrygd/repository/SakRepository.kt b/src/main/kotlin/no/nav/familie/ba/infotrygd/repository/SakRepository.kt index ccb57163..d9376172 100644 --- a/src/main/kotlin/no/nav/familie/ba/infotrygd/repository/SakRepository.kt +++ b/src/main/kotlin/no/nav/familie/ba/infotrygd/repository/SakRepository.kt @@ -47,6 +47,16 @@ interface SakRepository : JpaRepository { ) fun hentUtvidetBarnetrygdsakerForStønad(stonad: TrunkertStønad): List - - + @Query( + """ + SELECT s FROM Sak s + WHERE s.personKey = :#{#stonad.personKey} + AND s.kapittelNr = 'BA' + AND s.valg IN ('OR','UT') + AND s.saksblokk = :#{#stonad.saksblokk} + AND s.saksnummer = :#{#stonad.sakNr} + AND s.region = :#{#stonad.region} + AND s.type IN ('S', 'R', 'K', 'A', 'FL', 'AS')""" + ) + fun hentBarnetrygdsakerForStønad(stonad: TrunkertStønad): List } \ No newline at end of file diff --git a/src/main/kotlin/no/nav/familie/ba/infotrygd/rest/controller/PensjonController.kt b/src/main/kotlin/no/nav/familie/ba/infotrygd/rest/controller/PensjonController.kt new file mode 100644 index 00000000..a06d97d4 --- /dev/null +++ b/src/main/kotlin/no/nav/familie/ba/infotrygd/rest/controller/PensjonController.kt @@ -0,0 +1,96 @@ +package no.nav.familie.ba.infotrygd.rest.controller + +import com.fasterxml.jackson.annotation.JsonProperty +import io.micrometer.core.annotation.Timed +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.ExampleObject +import io.swagger.v3.oas.annotations.media.Schema +import no.nav.commons.foedselsnummer.FoedselsNr +import no.nav.familie.ba.infotrygd.service.BarnetrygdService +import no.nav.familie.ba.infotrygd.service.ClientValidator +import no.nav.security.token.support.core.api.Protected +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException +import java.time.LocalDate +import java.time.YearMonth +import io.swagger.v3.oas.annotations.parameters.RequestBody as ApiRequestBody + +@Protected +@RestController +@Timed(value = "infotrygd_historikk_pensjon_controller", percentiles = [0.5, 0.95]) +@RequestMapping("/infotrygd/barnetrygd") +class PensjonController( + private val barnetrygdService: BarnetrygdService, + private val clientValidator: ClientValidator, +) { + + @Operation(summary = "Uttrekk barnetrygdperioder på en person fra en bestemet måned. Maks 2 år tilbake i tid") + @PostMapping(path = ["pensjon"], consumes = ["application/json"]) + @ApiRequestBody(content = [Content(examples = [ExampleObject(value = """{"ident": "12345678910", "fraDato": "2022-12-01"}""")])]) + fun hentBarnetrygd(@RequestBody request: BarnetrygdTilPensjonRequest): BarnetrygdTilPensjonResponse { + clientValidator.authorizeClient() + + val fraDato = YearMonth.of(request.fraDato.year, request.fraDato.month) + + if (fraDato.isBefore(YearMonth.now().minusYears(2))) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "fraDato kan ikke være lenger enn 2 år tilbake i tid") + } + + val bruker = FoedselsNr(request.ident) + + return BarnetrygdTilPensjonResponse( + saker = barnetrygdService.finnBarnetrygdForPensjon(bruker, fraDato) + ) + } + + @Operation(summary = "Finner alle personer med barnetrygd innenfor et bestemt år på vegne av Psys") + @GetMapping(path = ["pensjon"]) + fun personerMedBarnetrygd(@Parameter(name = "aar") @RequestParam("aar") år: String): List { + clientValidator.authorizeClient() + return barnetrygdService.finnPersonerBarnetrygdPensjon(år) + } + + + data class BarnetrygdTilPensjonRequest( + val ident: String, + @Schema(implementation = String::class, example = "2020-12-01") val fraDato: LocalDate, + ) + + data class BarnetrygdTilPensjonResponse( + @JsonProperty("fagsaker") val saker: List + ) + + data class BarnetrygdTilPensjon( + @JsonProperty("fagsakEiersIdent") val fnr: String, + val barnetrygdPerioder: List, + ) + + data class BarnetrygdPeriode( + val personIdent: String, + val delingsprosentYtelse: YtelseProsent, + val ytelseTypeEkstern: YtelseTypeEkstern?, + val stønadFom: YearMonth, + val stønadTom: YearMonth, + val kildesystem: String = "Infotrygd" + ) + + enum class YtelseTypeEkstern { + ORDINÆR_BARNETRYGD, + UTVIDET_BARNETRYGD, + SMÅBARNSTILLEGG, + } + + enum class YtelseProsent { + FULL, + DELT, + USIKKER + } +} diff --git a/src/main/kotlin/no/nav/familie/ba/infotrygd/service/BarnetrygdService.kt b/src/main/kotlin/no/nav/familie/ba/infotrygd/service/BarnetrygdService.kt index 74da81b5..21eb378e 100644 --- a/src/main/kotlin/no/nav/familie/ba/infotrygd/service/BarnetrygdService.kt +++ b/src/main/kotlin/no/nav/familie/ba/infotrygd/service/BarnetrygdService.kt @@ -21,6 +21,10 @@ import no.nav.familie.ba.infotrygd.rest.controller.BisysController.InfotrygdUtvi import no.nav.familie.ba.infotrygd.rest.controller.BisysController.Stønadstype.SMÅBARNSTILLEGG import no.nav.familie.ba.infotrygd.rest.controller.BisysController.Stønadstype.UTVIDET import no.nav.familie.ba.infotrygd.rest.controller.BisysController.UtvidetBarnetrygdPeriode +import no.nav.familie.ba.infotrygd.rest.controller.PensjonController.BarnetrygdPeriode +import no.nav.familie.ba.infotrygd.rest.controller.PensjonController.BarnetrygdTilPensjon +import no.nav.familie.ba.infotrygd.rest.controller.PensjonController.YtelseProsent +import no.nav.familie.ba.infotrygd.rest.controller.PensjonController.YtelseTypeEkstern import no.nav.familie.ba.infotrygd.utils.DatoUtils import no.nav.familie.ba.infotrygd.utils.DatoUtils.isSameOrAfter import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPeriode @@ -33,6 +37,7 @@ import org.springframework.core.env.Environment import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service +import java.time.LocalDate import java.time.LocalDateTime import java.time.YearMonth import java.time.format.DateTimeFormatter @@ -151,6 +156,31 @@ class BarnetrygdService( return personer.map { person -> vedtakRepository.tellAntallÅpneSakerPåPerson(person) }.sum() } + fun finnBarnetrygdForPensjon( + brukerFnr: FoedselsNr, + fraDato: YearMonth + ): List { + val barnetrygdStønader = stonadRepository.findStønadByFnr(listOf(brukerFnr)).filter { it.antallBarn > 0 } + .map { it.tilTrunkertStønad() } + .filter { erRelevantStønadForPensjon(it) } + .filter { filtrerStønaderSomErFeilregistrert(it) } + + val perioder = konverterTilDtoForPensjon(barnetrygdStønader, fraDato.year).filter { + skalFiltreresPåDato(fraDato, it.stønadFom, it.stønadTom) + } + + if (perioder.isEmpty()) { + return emptyList() + } + + return listOf( + BarnetrygdTilPensjon( + fnr = brukerFnr.asString, + barnetrygdPerioder = perioder + ) + ) + } + fun finnUtvidetBarnetrygdBisys( brukerFnr: FoedselsNr, fraDato: YearMonth @@ -219,6 +249,23 @@ class BarnetrygdService( } } + @Cacheable(cacheManager = "personerCacheManager", value = ["pensjon_personer"], unless = "#result == null") + fun finnPersonerBarnetrygdPensjon(år: String): List { + val stønaderMedAktuelleKoder = stonadRepository.findStønadByÅrAndStatusKoder(år.toInt(), "00", "01", "02") + .filter { erRelevantStønadForPensjon(it) } + .filter { filtrerStønaderSomErFeilregistrert(it) } + .filter { + val sisteMåned = DatoUtils.stringDatoMMyyyyTilYearMonth(it.opphørtFom)?.minusMonths(1) + sisteMåned == null || sisteMåned.year >= år.toInt() + } + .filter { + utbetalingRepository.hentUtbetalingerByStønad(it).any { it.tom() == null || it.tom()!!.year >= år.toInt() } + } + + return stønaderMedAktuelleKoder.mapNotNull { + it.fnr + } + } fun listUtvidetStønadstyperForPerson(år: Int, fnr:String): List { val utvidetBarnetrygdStønader = stonadRepository.findStønadByÅrAndStatusKoderAndFnr(FoedselsNr(fnr), år, "00", "02", "03").map { it.tilTrunkertStønad() } @@ -314,6 +361,27 @@ class BarnetrygdService( } } + private fun erRelevantStønadForPensjon( + stønad: TrunkertStønad + ): Boolean { + return when (stønad.status.toLong()) { + 0L -> { // Manuell beregning ved Stønadsklasse BA OR/UT MB/MD/ME. + + if (stønad.fnr == null) { + logger.info("stønad.fnr var null for stønad med id ${stønad.id}") + return false + } + val undervalg = hentValgOgUndervalg(stønad).second + undervalg in arrayOf(MANUELL_BEREGNING, MANUELL_BEREGNING_DELT_BOSTED, MANUELL_BEREGNING_EØS) + } + 1L -> true // Ordinær barnetrygd - Maskinell beregning + 2L -> true // Utvidet barnetrygd - Maskinell beregning. + 3L -> false // Sykt barn (Ikke lenger i bruk, kan forekomme i gamle tilfeller), + 4L -> false // Ordinær barnetrygd - Institusjon + else -> false + } + } + private fun konverterTilDtoUtvidetBarnetrygdForSkatteetaten( brukerFnr: FoedselsNr, utvidetBarnetrygdStønader: List, år: Int @@ -365,6 +433,52 @@ class BarnetrygdService( } } + private fun konverterTilDtoForPensjon( + barnetrygdStønader: List, + år: Int + ): List { + if (barnetrygdStønader.isEmpty()) { + return emptyList() + } + + val allePerioder = mutableListOf() + + barnetrygdStønader.forEach { + val utbetalinger = utbetalingRepository.hentUtbetalingerByStønad(it) + allePerioder.addAll(utbetalinger.map { utbetaling -> + + val (valg, undervalg) = hentValgOgUndervalg(it) + + BarnetrygdPeriode( + ytelseTypeEkstern = when { + utbetaling.erSmåbarnstillegg() -> YtelseTypeEkstern.SMÅBARNSTILLEGG + valg == "UT" -> YtelseTypeEkstern.UTVIDET_BARNETRYGD + else -> YtelseTypeEkstern.ORDINÆR_BARNETRYGD + }, + stønadFom = utbetaling.fom()!!, + stønadTom = utbetaling.tom() ?: YearMonth.from(LocalDate.MAX), + personIdent = utbetaling.fnr.asString, + delingsprosentYtelse = ytelseProsent(it, undervalg, år) + ) + }) + } + + val perioder = + allePerioder.filter { it.erOrdinærBarnetrygd }.groupBy { it.delingsprosentYtelse }.values + .flatMap(::slåSammenSammenhengende).toMutableList() + + perioder.addAll( + allePerioder.filter { it.erUtvidetBarnetrygd }.groupBy { it.delingsprosentYtelse }.values + .flatMap(::slåSammenSammenhengende) + ) + perioder.addAll( + allePerioder.filter { it.erSmåbarnstillegg }.groupBy { it.delingsprosentYtelse }.values + .flatMap(::slåSammenSammenhengende) + ) + + return perioder + } + private fun delingsprosent(stønad: TrunkertStønad, år: Int): SkatteetatenPeriode.Delingsprosent { val undervalg = hentUndervalg(stønad) var delingsprosent = SkatteetatenPeriode.Delingsprosent.usikker @@ -388,6 +502,27 @@ class BarnetrygdService( return delingsprosent } + private fun ytelseProsent(stønad: TrunkertStønad, undervalg: String?, år: Int): YtelseProsent { + if (stønad.status.toInt() != 0 ) { + return YtelseProsent.FULL + } else if (undervalg == MANUELL_BEREGNING_DELT_BOSTED) { + if (stønad.antallBarn == 1) { + return YtelseProsent.DELT + } else if (stønad.antallBarn < 7) { + val sumUtbetaltBeløp = utbetalingRepository.hentUtbetalingerByStønad(stønad).sumOf { it.beløp } + val gyldigeBeløp = utledListeMedGyldigeUtbetalingsbeløp(stønad.antallBarn, år) + + if (gyldigeBeløp.contains(sumUtbetaltBeløp.roundToInt())) { + return YtelseProsent.DELT + } else { + secureLogger.info("Ytelseprosent usikker, ident ${stønad.fnr}, sumUtbetaltBeløp: $sumUtbetaltBeløp, gyldigeBeløp: $gyldigeBeløp" + + ", antallBarn: ${stønad.antallBarn}, år: $år") + } + } + } + return YtelseProsent.USIKKER + } + fun utledListeMedGyldigeUtbetalingsbeløp(antallBarn: Int, år: Int): Set { val gyldigeBeløp = mutableSetOf() for (i in 0..antallBarn) { @@ -467,6 +602,31 @@ class BarnetrygdService( hentUtvidetBarnetrygdUndervalgFraDb2(stønad).filterNotNull() } + private fun hentValgOgUndervalg(stønad: TrunkertStønad) = + sakRepository.hentBarnetrygdsakerForStønad(stønad).map { + it.valg to it.undervalg + }.filter { it.second != null }.ifEmpty { + hentBarnetrygdValgOgUndervalgFraDb2(stønad) + }.distinct().singleOrNull() ?: run { + secureLogger.info("Manglende/tvetydig stønadsklassifisering for stønad $stønad") + (null to null) + } + + private fun slåSammenSammenhengende(perioderMedLikProsentandel: List): List { + require(perioderMedLikProsentandel.all { it.delingsprosentYtelse == perioderMedLikProsentandel.first().delingsprosentYtelse }) + + return perioderMedLikProsentandel.sortedBy { it.stønadFom } + .fold(mutableListOf()) { sammenslåttePerioder, nestePeriode -> + val forrigePeriode = sammenslåttePerioder.lastOrNull() + + if (forrigePeriode?.stønadTom?.isSameOrAfter(nestePeriode.stønadFom.minusMonths(1)) == true) { + sammenslåttePerioder.apply { add(removeLast().copy(stønadTom = nestePeriode.stønadTom)) } + } else { + sammenslåttePerioder.apply { add(nestePeriode) } + } + } + } + private fun slåSammenSammenhengendePerioder(utbetalingerAvEtGittBeløp: List): List { return utbetalingerAvEtGittBeløp.sortedBy { it.fomMåned } .fold(mutableListOf()) { sammenslåttePerioder, nesteUtbetaling -> @@ -507,6 +667,19 @@ class BarnetrygdService( .map { it.kodeNivå3 } } ?: emptyList() + private fun hentBarnetrygdValgOgUndervalgFraDb2( + stønad: TrunkertStønad + ) = stønad.fnr?.let { + vedtakRepository.hentStønadsklassifisering( + fnr = stønad.fnr.asString, + tkNr = stønad.personKey.toString().padStart(15, '0').substring(0, 4), + saksblokk = stønad.saksblokk, + saksnummer = stønad.sakNr.toLong() + ).groupBy { stønadsklasse -> stønadsklasse.vedtakId }.values + .filter { !it.kodeNivå2.isNullOrBlank() } + .map { it.kodeNivå2!! to it.kodeNivå3 } + } ?: emptyList() + private val List.kodeNivå2: String? get() { return find { it.kodeNivaa == "02" }?.kodeKlasse // vil f.eks være "OR" for en sak av type BA OR OS @@ -531,3 +704,12 @@ class BarnetrygdService( const val PREPROD = "preprod" } } + +private val BarnetrygdPeriode.erOrdinærBarnetrygd: Boolean + get() = ytelseTypeEkstern == YtelseTypeEkstern.ORDINÆR_BARNETRYGD + +private val BarnetrygdPeriode.erUtvidetBarnetrygd: Boolean + get() = ytelseTypeEkstern == YtelseTypeEkstern.UTVIDET_BARNETRYGD + +private val BarnetrygdPeriode.erSmåbarnstillegg: Boolean + get() = ytelseTypeEkstern == YtelseTypeEkstern.SMÅBARNSTILLEGG \ No newline at end of file diff --git "a/src/test/kotlin/no/nav/familie/ba/infotrygd/repository/St\303\270nadRepositoryTest.kt" "b/src/test/kotlin/no/nav/familie/ba/infotrygd/repository/St\303\270nadRepositoryTest.kt" index 126aaf28..3f4061fa 100644 --- "a/src/test/kotlin/no/nav/familie/ba/infotrygd/repository/St\303\270nadRepositoryTest.kt" +++ "b/src/test/kotlin/no/nav/familie/ba/infotrygd/repository/St\303\270nadRepositoryTest.kt" @@ -49,6 +49,26 @@ class StønadRepositoryTest { ) } + @Test + fun `sjekk at antall personer med barnetrygd til pensjon er riktig innenfor hvert av årene 2021, 2022 og 2023`() { + stønadRepository.saveAll(listOf( + TestData.stønad(TestData.person(), opphørtFom = "122021", status = "01"), // ordinær barnetrygd opphørt 2021 + TestData.stønad(TestData.person(), opphørtFom = "122022", status = "02"), // utvidet opphørt 2022 + TestData.stønad(TestData.person(), status = "02") // løpende utvidet + )).also { stønader -> + utbetalingRepository.saveAll(stønader.map { TestData.utbetaling(it) }) + } + barnetrygdService.finnPersonerBarnetrygdPensjon("2021").also { + assertThat(it).hasSize(3) + } + barnetrygdService.finnPersonerBarnetrygdPensjon("2022").also { + assertThat(it).hasSize(2) + } + barnetrygdService.finnPersonerBarnetrygdPensjon("2023").also { + assertThat(it).hasSize(1) + } + } + @Test fun `sjekk at antall personer med utvidet barnetrygd er riktig innenfor hvert av årene 2019, 2020 og 2021`() { val personFraInneværendeÅr = TestData.person() diff --git a/src/test/kotlin/no/nav/familie/ba/infotrygd/service/BarnetrygdServiceTest.kt b/src/test/kotlin/no/nav/familie/ba/infotrygd/service/BarnetrygdServiceTest.kt index 2cc5c73d..a6e35fab 100644 --- a/src/test/kotlin/no/nav/familie/ba/infotrygd/service/BarnetrygdServiceTest.kt +++ b/src/test/kotlin/no/nav/familie/ba/infotrygd/service/BarnetrygdServiceTest.kt @@ -18,6 +18,9 @@ import no.nav.familie.ba.infotrygd.repository.StønadsklasseRepository import no.nav.familie.ba.infotrygd.repository.UtbetalingRepository import no.nav.familie.ba.infotrygd.repository.VedtakRepository import no.nav.familie.ba.infotrygd.rest.controller.BisysController +import no.nav.familie.ba.infotrygd.rest.controller.PensjonController +import no.nav.familie.ba.infotrygd.rest.controller.PensjonController.YtelseProsent +import no.nav.familie.ba.infotrygd.rest.controller.PensjonController.YtelseTypeEkstern import no.nav.familie.ba.infotrygd.testutil.TestData import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPeriode import org.assertj.core.api.Assertions.assertThat @@ -31,6 +34,7 @@ import org.springframework.core.env.Environment import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.junit4.SpringRunner import java.sql.SQLException +import java.time.LocalDate import java.time.LocalDateTime import java.time.YearMonth import java.time.format.DateTimeFormatter @@ -159,6 +163,51 @@ internal class BarnetrygdServiceTest { assertThat(barnetrygdService.tellAntallÅpneSaker(emptyList(), emptyList())).isEqualTo(0) } + @Test + fun `finn barnetrygd for pensjon - finner full ordniær barnetrygd`() { + val person = settOppLøpendeOrdinærBarnetrygd(ORDINÆR_BARNETRYGD_STATUS) + + val response = barnetrygdService.finnBarnetrygdForPensjon(person.fnr, YearMonth.now()).single() + assertThat(response.barnetrygdPerioder).contains( + PensjonController.BarnetrygdPeriode( + personIdent = person.fnr.asString, + delingsprosentYtelse = YtelseProsent.FULL, + ytelseTypeEkstern = YtelseTypeEkstern.ORDINÆR_BARNETRYGD, + stønadFom = YearMonth.of(2020, 5), + stønadTom = YearMonth.from(LocalDate.MAX), + kildesystem = "Infotrygd", + ) + ) + } + + @Test + fun `finn barnetrygd for pensjon - finner løpende småbarnstillegg, og løpende utvidet fra og med dato gitt av foregående periode`() { + val person = settOppLøpendeUtvidetBarnetrygd(MANUELT_BEREGNET_STATUS) + leggTilUtgåttUtvidetBarnetrygdSak(person) //2019-05 - 2020-04 + + + val response = barnetrygdService.finnBarnetrygdForPensjon(person.fnr, YearMonth.now()).single() + assertThat(response.barnetrygdPerioder).contains( + PensjonController.BarnetrygdPeriode( + personIdent = person.fnr.asString, + delingsprosentYtelse = YtelseProsent.USIKKER, + ytelseTypeEkstern = YtelseTypeEkstern.UTVIDET_BARNETRYGD, + stønadFom = YearMonth.of(2019, 5), + stønadTom = YearMonth.from(LocalDate.MAX), + kildesystem = "Infotrygd", + ) + ) + assertThat(response.barnetrygdPerioder).contains( + PensjonController.BarnetrygdPeriode( + personIdent = person.fnr.asString, + delingsprosentYtelse = YtelseProsent.USIKKER, + ytelseTypeEkstern = YtelseTypeEkstern.SMÅBARNSTILLEGG, + stønadFom = YearMonth.of(2020, 5), + stønadTom = YearMonth.from(LocalDate.MAX), + kildesystem = "Infotrygd", + ) + ) + } @Test fun `hent utvidet barnetrygd for stønad med status 0, utvidet barnetrygdsak og inputdato med dato nå, som kun henter aktiv stønad, manuelt beregnet`() { @@ -673,6 +722,14 @@ internal class BarnetrygdServiceTest { assertThat(barnetrygdService.harSendtBrevForrigeMåned(listOf(person.fnr), listOf("B001"))).hasSize(1) } + private fun settOppLøpendeOrdinærBarnetrygd(stønadStatus: String): Person { + val person = personRepository.save(TestData.person()) + val løpendeStønad = stonadRepository.save(TestData.stønad(person, status = stønadStatus, opphørtFom = "000000")) + sakRepository.save(TestData.sak(person, løpendeStønad.saksblokk, løpendeStønad.sakNr, valg = "OR", undervalg = "OS")) + utbetalingRepository.saveAll(listOf(TestData.utbetaling(løpendeStønad))) + return person + + } private fun settOppLøpendeUtvidetBarnetrygd(stønadStatus: String): Person { val person = personRepository.save(TestData.person()) @@ -725,7 +782,7 @@ internal class BarnetrygdServiceTest { companion object { const val MANUELT_BEREGNET_STATUS = "0" - const val UTVIDET_BARNETRYGD_STATUS = "2" + const val ORDINÆR_BARNETRYGD_STATUS = "1" const val SATS_BARNETRYGD_OVER_6 = 1054.0 const val SATS_BARNETRYGD_UNDER_6_2021 = 1654.0 const val SATS_BARNETRYGD_UNDER_6_2022 = 1676.0