diff --git a/buildSrc/src/main/kotlin/common.gradle.kts b/buildSrc/src/main/kotlin/common.gradle.kts index e3f09123..668e8de4 100644 --- a/buildSrc/src/main/kotlin/common.gradle.kts +++ b/buildSrc/src/main/kotlin/common.gradle.kts @@ -1,6 +1,6 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") diff --git a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/Application.kt b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/Application.kt index 3588f3ed..d5b5f442 100644 --- a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/Application.kt +++ b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/Application.kt @@ -47,15 +47,16 @@ fun main() { val cachedInntektsGetter = BehandlingsInntektsGetter(inntektskomponentHttpClient, postgresInntektStore) // Marks inntekt as used val subsumsjonBruktDataConsumer = - KafkaSubsumsjonBruktDataConsumer(config, postgresInntektStore).apply { - listen() - }.also { - Runtime.getRuntime().addShutdownHook( - Thread { - it.stop() - }, - ) - } + KafkaSubsumsjonBruktDataConsumer(config, postgresInntektStore) + .apply { + listen() + }.also { + Runtime.getRuntime().addShutdownHook( + Thread { + it.stop() + }, + ) + } // Provides a HTTP API for getting inntekt embeddedServer(Netty, port = config.application.httpPort) { diff --git a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/BehandlingsInntektsGetter.kt b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/BehandlingsInntektsGetter.kt index 0f43e5a2..94879945 100644 --- a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/BehandlingsInntektsGetter.kt +++ b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/BehandlingsInntektsGetter.kt @@ -22,34 +22,28 @@ class BehandlingsInntektsGetter( suspend fun getKlassifisertInntekt( inntektparametre: Inntektparametre, callId: String? = null, - ): Inntekt { - return klassifiserOgMapInntekt(getSpesifisertInntekt(inntektparametre, callId)) - } + ): Inntekt = klassifiserOgMapInntekt(getSpesifisertInntekt(inntektparametre, callId)) - fun getKlassifisertInntekt(inntektId: InntektId): Inntekt { - return klassifiserOgMapInntekt(inntektStore.getSpesifisertInntekt(inntektId)) - } + fun getKlassifisertInntekt(inntektId: InntektId): Inntekt = klassifiserOgMapInntekt(inntektStore.getSpesifisertInntekt(inntektId)) suspend fun getSpesifisertInntekt( inntektparametre: Inntektparametre, callId: String? = null, - ): SpesifisertInntekt { - return mapToSpesifisertInntekt( + ): SpesifisertInntekt = + mapToSpesifisertInntekt( getBehandlingsInntekt(inntektparametre, callId), inntektparametre.opptjeningsperiode.sisteAvsluttendeKalenderMåned, ) - } internal suspend fun getBehandlingsInntekt( inntektparametre: Inntektparametre, callId: String? = null, - ): StoredInntekt { - return isInntektStored(inntektparametre)?.let { + ): StoredInntekt = + isInntektStored(inntektparametre)?.let { LOGGER.info { "Henter stored inntekt: ${inntektparametre.toDebugString()}" } inntektStore.getInntekt(it) } ?: fetchAndStoreInntekt(inntektparametre, callId) - } private suspend fun fetchAndStoreInntekt( inntektparametre: Inntektparametre, diff --git a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/InntektApi.kt b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/InntektApi.kt index 3d6e6084..5b76106e 100644 --- a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/InntektApi.kt +++ b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/InntektApi.kt @@ -107,7 +107,8 @@ internal fun Application.inntektApi( } exception { call, cause -> val statusCode = - if (HttpStatusCode.fromValue(cause.status) + if (HttpStatusCode + .fromValue(cause.status) .isSuccess() ) { HttpStatusCode.InternalServerError @@ -218,7 +219,7 @@ internal fun Application.inntektApi( routing { route("/v1") { route("/inntekt") { - uklassifisertInntekt(inntektskomponentHttpClient, inntektStore, personOppslag) + uklassifisertInntekt(inntektskomponentHttpClient, inntektStore, personOppslag, enhetsregisterClient) } opptjeningsperiodeApi(inntektStore) enhetsregisteret(enhetsregisterClient) diff --git a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/db/InntektStore.kt b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/db/InntektStore.kt index 978932cf..37ce03b8 100644 --- a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/db/InntektStore.kt +++ b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/db/InntektStore.kt @@ -18,6 +18,8 @@ interface InntektStore { fun getBeregningsdato(inntektId: InntektId): LocalDate + fun getInntektPersonMapping(inntektId: String): InntektPersonMapping + fun storeInntekt( command: StoreInntektCommand, created: ZonedDateTime = ZonedDateTime.now(ZoneOffset.UTC), @@ -26,6 +28,8 @@ interface InntektStore { fun getManueltRedigert(inntektId: InntektId): ManueltRedigert? fun markerInntektBrukt(inntektId: InntektId): Int + + fun getInntektMedPersonFnr(inntektId: InntektId): StoredInntektMedFnr } data class Inntektparametre( @@ -36,12 +40,13 @@ data class Inntektparametre( ) { val opptjeningsperiode: Opptjeningsperiode = Opptjeningsperiode(beregningsdato) - fun toDebugString(): String { - return "Inntektparametre(aktørId='$aktørId', beregningsdato=$beregningsdato, regelkontekst=$regelkontekst)" - } + fun toDebugString(): String = "Inntektparametre(aktørId='$aktørId', beregningsdato=$beregningsdato, regelkontekst=$regelkontekst)" } -data class RegelKontekst(val id: String, val type: String) +data class RegelKontekst( + val id: String, + val type: String, +) data class StoreInntektCommand( val inntektparametre: Inntektparametre, @@ -49,7 +54,19 @@ data class StoreInntektCommand( val manueltRedigert: ManueltRedigert? = null, ) -data class ManueltRedigert(val redigertAv: String) { +data class InntektPersonMapping( + val inntektId: InntektId, + val aktørId: String, + val fnr: String? = null, + val kontekstId: String, + val beregningsdato: LocalDate, + val timestamp: LocalDateTime, + val kontekstType: String, +) + +data class ManueltRedigert( + val redigertAv: String, +) { companion object { fun from( bool: Boolean, @@ -68,9 +85,14 @@ data class StoredInntekt( val timestamp: LocalDateTime? = null, ) -data class DetachedInntekt(val inntekt: InntektkomponentResponse, val manueltRedigert: Boolean) +data class DetachedInntekt( + val inntekt: InntektkomponentResponse, + val manueltRedigert: Boolean, +) -data class InntektId(val id: String) { +data class InntektId( + val id: String, +) { init { try { ULID.parseULID(id) @@ -80,9 +102,23 @@ data class InntektId(val id: String) { } } -class InntektNotFoundException(override val message: String) : RuntimeException(message) +data class StoredInntektMedFnr( + val inntektId: InntektId, + val inntekt: InntektkomponentResponse, + val manueltRedigert: Boolean, + val timestamp: LocalDateTime? = null, + val fødselsnummer: String, +) + +class InntektNotFoundException( + override val message: String, +) : RuntimeException(message) -class StoreException(override val message: String) : RuntimeException(message) +class StoreException( + override val message: String, +) : RuntimeException(message) -class IllegalInntektIdException(override val message: String, override val cause: Throwable?) : - java.lang.RuntimeException(message, cause) +class IllegalInntektIdException( + override val message: String, + override val cause: Throwable?, +) : java.lang.RuntimeException(message, cause) diff --git a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/db/PostgresInntektStore.kt b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/db/PostgresInntektStore.kt index 0ed32a08..573e9c9f 100644 --- a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/db/PostgresInntektStore.kt +++ b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/db/PostgresInntektStore.kt @@ -22,12 +22,16 @@ import java.time.ZonedDateTime import javax.sql.DataSource @Suppress("ktlint:standard:max-line-length") -internal class PostgresInntektStore(private val dataSource: DataSource) : InntektStore, HealthCheck { +internal class PostgresInntektStore( + private val dataSource: DataSource, +) : InntektStore, + HealthCheck { companion object { private val ulidGenerator = ULID() private val LOGGER = KotlinLogging.logger {} private val markerInntektTimer = - Summary.builder() + Summary + .builder() .name("marker_inntekt_brukt") .help("Hvor lang tid det tar å markere en inntekt brukt (i sekunder") .register() @@ -89,6 +93,30 @@ internal class PostgresInntektStore(private val dataSource: DataSource) : Inntek } } + override fun getInntektPersonMapping(inntektId: String): InntektPersonMapping { + @Language("sql") + val statement = "SELECT * FROM inntekt_V1_person_mapping WHERE inntektId = :inntektId)".trimMargin() + + return using(sessionOf(dataSource)) { session -> + session.run( + queryOf( + statement, + mapOf("inntektId" to inntektId), + ).map { row -> + InntektPersonMapping( + inntektId = InntektId(row.string("inntektid")), + aktørId = row.string("aktorid"), + fnr = row.string("fnr"), + kontekstId = row.string("kontekstid"), + beregningsdato = row.zonedDateTime("beregningsdato").toLocalDate(), + timestamp = row.zonedDateTime("timestamp").toLocalDateTime(), + kontekstType = row.string("konteksttype"), + ) + }.asSingle, + ) ?: throw InntektNotFoundException("Inntekt with id $inntektId not found.") + } + } + override fun getBeregningsdato(inntektId: InntektId): LocalDate { @Language("sql") val statement = @@ -110,8 +138,8 @@ internal class PostgresInntektStore(private val dataSource: DataSource) : Inntek } } - override fun getInntekt(inntektId: InntektId): StoredInntekt { - return using(sessionOf(dataSource)) { session -> + override fun getInntekt(inntektId: InntektId): StoredInntekt = + using(sessionOf(dataSource)) { session -> session.run( queryOf( """ SELECT id, inntekt, manuelt_redigert, timestamp from inntekt_V1 where id = ?""", @@ -123,12 +151,10 @@ internal class PostgresInntektStore(private val dataSource: DataSource) : Inntek manueltRedigert = row.boolean("manuelt_redigert"), timestamp = row.zonedDateTime("timestamp").toLocalDateTime(), ) - } - .asSingle, + }.asSingle, ) ?: throw InntektNotFoundException("Inntekt with id $inntektId not found.") } - } override fun getSpesifisertInntekt(inntektId: InntektId): SpesifisertInntekt { @Language("sql") @@ -137,8 +163,9 @@ internal class PostgresInntektStore(private val dataSource: DataSource) : Inntek SELECT inntekt.id, inntekt.inntekt, inntekt.manuelt_redigert, inntekt.timestamp, mapping.beregningsdato from inntekt_V1 inntekt inner join inntekt_V1_person_mapping mapping on inntekt.id = mapping.inntektid - where inntekt.id = ?""" - .trimIndent() + where inntekt.id = ? + + """.trimIndent() val stored = using(sessionOf(dataSource)) { session -> @@ -153,14 +180,40 @@ internal class PostgresInntektStore(private val dataSource: DataSource) : Inntek manueltRedigert = row.boolean("manuelt_redigert"), timestamp = row.zonedDateTime("timestamp").toLocalDateTime(), ) to row.localDate("beregningsdato") - } - .asSingle, + }.asSingle, ) ?: throw InntektNotFoundException("Inntekt with id $inntektId not found.") } return mapToSpesifisertInntekt(stored.first, Opptjeningsperiode(stored.second).sisteAvsluttendeKalenderMåned) } + override fun getInntektMedPersonFnr(inntektId: InntektId): StoredInntektMedFnr { + @Language("sql") + val statement = + """ + SELECT inntekt.id, inntekt.inntekt, inntekt.manuelt_redigert, inntekt.timestamp, mapping.fnr + from inntekt_V1 inntekt + inner join inntekt_V1_person_mapping mapping on inntekt.id = mapping.inntektid + where inntekt.id = ? + + """.trimIndent() + + return using(sessionOf(dataSource)) { session -> + session.run( + queryOf(statement, inntektId.id) + .map { + StoredInntektMedFnr( + inntektId = InntektId(it.string("id")), + inntekt = it.binaryStream("inntekt").use { jacksonObjectMapper.readValue(it) }, + manueltRedigert = it.boolean("manuelt_redigert"), + timestamp = it.zonedDateTime("timestamp").toLocalDateTime(), + fødselsnummer = it.string("fnr"), + ) + }.asSingle, + ) ?: throw InntektNotFoundException("Inntekt with id $inntektId not found.") + } + } + override fun storeInntekt( command: StoreInntektCommand, created: ZonedDateTime, diff --git a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/mapping/GUIInntektsKomponentResponse.kt b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/mapping/GUIInntektsKomponentResponse.kt index f708fe27..ebc6a2c2 100644 --- a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/mapping/GUIInntektsKomponentResponse.kt +++ b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/mapping/GUIInntektsKomponentResponse.kt @@ -20,7 +20,10 @@ data class GUIInntekt( val inntektsmottaker: Inntektsmottaker? = null, ) -data class Inntektsmottaker(val pnr: String?, val navn: String?) +data class Inntektsmottaker( + val pnr: String?, + val navn: String?, +) data class GUIInntektsKomponentResponse( val fraDato: YearMonth?, diff --git a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/mapping/MapFromGUIInntekt.kt b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/mapping/MapFromGUIInntekt.kt index f592a541..497d05d3 100644 --- a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/mapping/MapFromGUIInntekt.kt +++ b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/mapping/MapFromGUIInntekt.kt @@ -1,7 +1,6 @@ package no.nav.dagpenger.inntekt.mapping import de.huxhorn.sulky.ulid.ULID -import mu.KotlinLogging import no.nav.dagpenger.inntekt.db.DetachedInntekt import no.nav.dagpenger.inntekt.db.InntektId import no.nav.dagpenger.inntekt.db.StoredInntekt @@ -12,9 +11,6 @@ import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektkomponentResponse import no.nav.dagpenger.inntekt.inntektskomponenten.v1.TilleggInformasjon import no.nav.dagpenger.inntekt.inntektskomponenten.v1.TilleggInformasjonsDetaljer -private val logg = KotlinLogging.logger {} -private val sikkerlogg = KotlinLogging.logger("tjenestekall.MapFromGUIInntekt") - fun mapToStoredInntekt(guiInntekt: GUIInntekt): StoredInntekt = StoredInntekt( guiInntekt.inntektId ?: InntektId(ULID().nextULID()), @@ -36,13 +32,13 @@ fun mapToDetachedInntekt(guiInntekt: GUIInntekt): DetachedInntekt = guiInntekt.manueltRedigert, ) -private fun mapToArbeidsInntektMaaneder(arbeidsMaaneder: List?): List? { - return arbeidsMaaneder?.map { GUIarbeidsInntektMaaned -> +private fun mapToArbeidsInntektMaaneder(arbeidsMaaneder: List?): List? = + arbeidsMaaneder?.map { guiArbeidsInntektMaaned -> ArbeidsInntektMaaned( - GUIarbeidsInntektMaaned.aarMaaned, - GUIarbeidsInntektMaaned.avvikListe, + guiArbeidsInntektMaaned.aarMaaned, + guiArbeidsInntektMaaned.avvikListe, ArbeidsInntektInformasjon( - GUIarbeidsInntektMaaned.arbeidsInntektInformasjon?.inntektListe?.map { inntekt -> + guiArbeidsInntektMaaned.arbeidsInntektInformasjon?.inntektListe?.map { inntekt -> val datagrunnlagForVerdikode: DatagrunnlagKlassifisering = dataGrunnlag(inntekt.verdikode) Inntekt( inntekt.beloep, @@ -78,4 +74,3 @@ private fun mapToArbeidsInntektMaaneder(arbeidsMaaneder: List, +): InntekterDto { + val inntekt = arbeidsInntektMaaned + val virksomheter: MutableList = mutableListOf() + + inntekt?.forEach { arbeidsInntektMaaned -> + val inntektsInformasjon = arbeidsInntektMaaned.arbeidsInntektInformasjon + inntektsInformasjon?.inntektListe?.forEach { inntekt -> + val virksomhet = inntekt.virksomhet + val virksomhetNavn = organisasjoner.find { it.organisasjonsnummer == virksomhet?.identifikator }?.navn ?: "" + val inntekter = mutableListOf() + inntekter.add( + InntektMaaned( + belop = inntekt.beloep, + inntektskilde = inntekt.inntektskilde, + redigert = false, + begrunnelse = inntekt.beskrivelse.name, + aarMaaned = arbeidsInntektMaaned.aarMaaned, + fordel = inntekt.fordel, + beskrivelse = inntekt.beskrivelse, + inntektsstatus = inntekt.inntektsstatus, + inntektsperiodetype = inntekt.inntektsperiodetype, + utbetaltIMaaned = inntekt.utbetaltIMaaned, + inntektType = inntekt.inntektType, + leveringstidspunkt = inntekt.leveringstidspunkt, + opptjeningsland = inntekt.opptjeningsland, + opptjeningsperiode = inntekt.opptjeningsperiode, + skattemessigBosattLand = inntekt.skattemessigBosattLand, + inntektsinnsender = inntekt.inntektsinnsender, + virksomhet = inntekt.virksomhet, + inntektsmottaker = inntekt.inntektsmottaker, + inngaarIGrunnlagForTrekk = inntekt.inngaarIGrunnlagForTrekk, + utloeserArbeidsgiveravgift = inntekt.utloeserArbeidsgiveravgift, + informasjonsstatus = inntekt.informasjonsstatus, + tilleggsinformasjon = inntekt.tilleggsinformasjon, + ), + ) + + val eksisterendeVirksomhet = + virksomheter.find { it.virksomhetsnummer == virksomhet?.identifikator } + if (eksisterendeVirksomhet != null) { + eksisterendeVirksomhet.inntekter?.addAll(inntekter) + eksisterendeVirksomhet.periode = + InntektPeriode( + fra = eksisterendeVirksomhet.inntekter!!.minOf { it.aarMaaned }, + til = eksisterendeVirksomhet.inntekter.maxOf { it.aarMaaned }, + ) + eksisterendeVirksomhet.totalBeløp = eksisterendeVirksomhet.inntekter.sumOf { it.belop } + } else { + virksomheter.add( + Virksomhet( + virksomhetsnummer = virksomhet?.identifikator ?: "", + virksomhetsnavn = virksomhetNavn, + periode = + InntektPeriode( + fra = arbeidsInntektMaaned.aarMaaned, + til = arbeidsInntektMaaned.aarMaaned, + ), + inntekter = inntekter, + avvikListe = mutableListOf(), + ), + ) + } + } + + arbeidsInntektMaaned.avvikListe?.forEach { avvik -> + val virksomhet = virksomheter.find { it.virksomhetsnummer == avvik.virksomhet?.identifikator } + if (virksomhet != null) { + virksomhet.avvikListe.add(avvik) + } else { + virksomheter.add( + Virksomhet( + virksomhetsnummer = avvik.virksomhet?.identifikator ?: "", + virksomhetsnavn = "", + periode = null, + inntekter = null, + avvikListe = mutableListOf(avvik), + ), + ) + } + } + } + + return InntekterDto( + virksomheter = virksomheter, + mottaker = person, + ) +} + +data class Virksomhet( + val virksomhetsnummer: String, + val virksomhetsnavn: String, + var periode: InntektPeriode?, + val inntekter: MutableList?, + var totalBeløp: BigDecimal? = inntekter?.sumOf { it.belop } ?: BigDecimal.ZERO, + val avvikListe: MutableList, +) + +data class InntektPeriode( + val fra: YearMonth, + val til: YearMonth, +) + +data class InntektMaaned( + val belop: BigDecimal, + val fordel: String, + val beskrivelse: InntektBeskrivelse, + val inntektskilde: String, + val inntektsstatus: String, + val inntektsperiodetype: String?, + val leveringstidspunkt: YearMonth? = null, + val opptjeningsland: String? = null, + val opptjeningsperiode: Periode? = null, + val skattemessigBosattLand: String? = null, + val utbetaltIMaaned: YearMonth, + val opplysningspliktig: Aktoer? = null, + val inntektsinnsender: Aktoer? = null, + val virksomhet: Aktoer? = null, + val inntektsmottaker: Aktoer? = null, + val inngaarIGrunnlagForTrekk: Boolean? = null, + val utloeserArbeidsgiveravgift: Boolean? = null, + val informasjonsstatus: String? = null, + val inntektType: InntektType, + val tilleggsinformasjon: TilleggInformasjon? = null, + val redigert: Boolean, + val begrunnelse: String, + val aarMaaned: YearMonth, +) + +data class Organisasjon( + val organisasjonsnummer: String, + val navn: String, +) diff --git a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/oppslag/PersonOppslag.kt b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/oppslag/PersonOppslag.kt index 0840aec2..235bd1d9 100644 --- a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/oppslag/PersonOppslag.kt +++ b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/oppslag/PersonOppslag.kt @@ -4,7 +4,10 @@ interface PersonOppslag { suspend fun hentPerson(ident: String): Person } -class PersonNotFoundException(val ident: String?, msg: String = "Fant ikke person") : RuntimeException(msg) +class PersonNotFoundException( + val ident: String?, + msg: String = "Fant ikke person", +) : RuntimeException(msg) data class Person( val fødselsnummer: String, diff --git a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/v1/UklassifisertInntektRoute.kt b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/v1/UklassifisertInntektRoute.kt index 02c8f190..8982ab00 100644 --- a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/v1/UklassifisertInntektRoute.kt +++ b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/v1/UklassifisertInntektRoute.kt @@ -18,102 +18,191 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import mu.KotlinLogging import mu.withLoggingContext +import no.nav.dagpenger.inntekt.db.InntektId import no.nav.dagpenger.inntekt.db.InntektNotFoundException import no.nav.dagpenger.inntekt.db.InntektStore import no.nav.dagpenger.inntekt.db.Inntektparametre import no.nav.dagpenger.inntekt.db.ManueltRedigert import no.nav.dagpenger.inntekt.db.RegelKontekst import no.nav.dagpenger.inntekt.db.StoreInntektCommand +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.AktoerType import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektkomponentRequest import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektskomponentClient import no.nav.dagpenger.inntekt.mapping.GUIInntekt import no.nav.dagpenger.inntekt.mapping.Inntektsmottaker +import no.nav.dagpenger.inntekt.mapping.Organisasjon import no.nav.dagpenger.inntekt.mapping.dataGrunnlagKlassifiseringToVerdikode import no.nav.dagpenger.inntekt.mapping.mapToDetachedInntekt +import no.nav.dagpenger.inntekt.mapping.mapToFrontend import no.nav.dagpenger.inntekt.mapping.mapToGUIInntekt import no.nav.dagpenger.inntekt.mapping.mapToStoredInntekt import no.nav.dagpenger.inntekt.oppslag.Person import no.nav.dagpenger.inntekt.oppslag.PersonOppslag +import no.nav.dagpenger.inntekt.oppslag.enhetsregister.EnhetsregisterClient import no.nav.dagpenger.inntekt.opptjeningsperiode.Opptjeningsperiode +import no.nav.dagpenger.inntekt.v1.models.InntekterDto +import no.nav.dagpenger.inntekt.v1.models.mapToStoredInntekt import java.time.LocalDate +import kotlin.coroutines.CoroutineContext private val logger = KotlinLogging.logger {} const val INNTEKT_KORRIGERING = "inntekt_korrigering" private val inntektKorrigeringCounter = - Counter.builder().name(INNTEKT_KORRIGERING).help("Antall ganger saksbehandler har korrigert inntekter").register() + Counter + .builder() + .name(INNTEKT_KORRIGERING) + .help("Antall ganger saksbehandler har korrigert inntekter") + .register() const val INNTEKT_OPPFRISKING = "inntekt_oppfrisking" private val inntektOppfriskingCounter = - Counter.builder().name(INNTEKT_OPPFRISKING).help("Antall ganger saksbehandler har oppdatert inntekter").register() + Counter + .builder() + .name(INNTEKT_OPPFRISKING) + .help("Antall ganger saksbehandler har oppdatert inntekter") + .register() const val INNTEKT_OPPFRISKING_BRUKT = "inntekt_oppfrisking_brukt" private val inntektOppfriskingBruktCounter = - Counter.builder().name(INNTEKT_OPPFRISKING_BRUKT).help("Antall ganger saksbehandler har brukt oppdaterte inntekter") + Counter + .builder() + .name(INNTEKT_OPPFRISKING_BRUKT) + .help("Antall ganger saksbehandler har brukt oppdaterte inntekter") .register() fun Route.uklassifisertInntekt( inntektskomponentClient: InntektskomponentClient, inntektStore: InntektStore, personOppslag: PersonOppslag, + enhetsregisterClient: EnhetsregisterClient, + coroutineContext: CoroutineContext = Dispatchers.IO, ) { authenticate("azure") { route("/uklassifisert/{aktørId}/{kontekstType}/{kontekstId}/{beregningsDato}") { get { - withContext(Dispatchers.IO) { + withContext(coroutineContext) { call.withInntektRequest("GET /uklassifisert/") { val person = personOppslag.hentPerson(this.aktørId) - inntektStore.getInntektId( - Inntektparametre( - aktørId = person.aktørId, - fødselsnummer = person.fødselsnummer, - regelkontekst = RegelKontekst(this.kontekstId, this.kontekstType), - beregningsdato = this.beregningsDato, - ), - )?.let { - inntektStore.getInntekt(it) - }?.let { - val inntektsmottaker = Inntektsmottaker(person.fødselsnummer, person.sammensattNavn()) - mapToGUIInntekt(it, Opptjeningsperiode(this.beregningsDato), inntektsmottaker) - }?.let { - call.respond(HttpStatusCode.OK, it) - } ?: throw InntektNotFoundException("Inntekt with for $this not found.") + inntektStore + .getInntektId( + Inntektparametre( + aktørId = person.aktørId, + fødselsnummer = person.fødselsnummer, + regelkontekst = RegelKontekst(this.kontekstId, this.kontekstType), + beregningsdato = this.beregningsDato, + ), + )?.let { + inntektStore.getInntekt(it) + }?.let { + val inntektsmottaker = Inntektsmottaker(person.fødselsnummer, person.sammensattNavn()) + mapToGUIInntekt(it, Opptjeningsperiode(this.beregningsDato), inntektsmottaker) + }?.let { + call.respond(HttpStatusCode.OK, it) + } ?: throw InntektNotFoundException("Inntekt with for $this not found.") } } } post { - withContext(Dispatchers.IO) { + withContext(coroutineContext) { call.withInntektRequest("POST /uklassifisert/") { val person = personOppslag.hentPerson(this.aktørId) val guiInntekt = call.receive() - mapToStoredInntekt(guiInntekt).let { + mapToStoredInntekt(guiInntekt) + .let { + inntektStore.storeInntekt( + StoreInntektCommand( + inntektparametre = + Inntektparametre( + aktørId = person.aktørId, + fødselsnummer = person.fødselsnummer, + regelkontekst = RegelKontekst(this.kontekstId, this.kontekstType), + beregningsdato = this.beregningsDato, + ), + inntekt = it.inntekt, + manueltRedigert = + ManueltRedigert.from( + guiInntekt.redigertAvSaksbehandler, + call.getSubject(), + ), + ), + ) + }.let { + call.respond( + HttpStatusCode.OK, + mapToGUIInntekt( + it, + Opptjeningsperiode(this.beregningsDato), + guiInntekt.inntektsmottaker, + ), + ) + }.also { + inntektKorrigeringCounter.inc() + } + } + } + } + } + + route("/uklassifisert/{inntektId}") { + get { + withContext(coroutineContext) { + val inntektId = InntektId(call.parameters["inntektId"]!!) + inntektStore + .getInntektMedPersonFnr(inntektId) + .let { + val person = personOppslag.hentPerson(it.fødselsnummer) + val inntektsmottaker = Inntektsmottaker(it.fødselsnummer, person.sammensattNavn()) + val organisasjoner = + hentOrganisasjoner( + enhetsregisterClient, + it.inntekt.arbeidsInntektMaaned + ?.flatMap { it.arbeidsInntektInformasjon?.inntektListe.orEmpty() } + ?.filter { inntekt -> + inntekt.virksomhet?.aktoerType == AktoerType.ORGANISASJON && + (inntekt.opptjeningsland == "NO" || inntekt.opptjeningsland == null) + }?.mapNotNull { it.virksomhet?.identifikator } + ?.toTypedArray() + ?.toList() ?: emptyList(), + ) + it.inntekt.mapToFrontend(inntektsmottaker, organisasjoner) + }.let { + call.respond(HttpStatusCode.OK, it) + } + } + } + post { + withContext(coroutineContext) { + val inntektId = call.parameters["inntektId"]!! + call + .receive() + .mapToStoredInntekt( + inntektId = inntektId, + ).let { + val inntektPersonMapping = inntektStore.getInntektPersonMapping(inntektId) inntektStore.storeInntekt( StoreInntektCommand( inntektparametre = Inntektparametre( - aktørId = person.aktørId, - fødselsnummer = person.fødselsnummer, - regelkontekst = RegelKontekst(this.kontekstId, this.kontekstType), - beregningsdato = this.beregningsDato, + aktørId = inntektPersonMapping.aktørId, + fødselsnummer = it.inntekt.ident.identifikator, + regelkontekst = + RegelKontekst( + inntektPersonMapping.kontekstId, + inntektPersonMapping.kontekstType, + ), + beregningsdato = inntektPersonMapping.beregningsdato, ), inntekt = it.inntekt, manueltRedigert = ManueltRedigert.from( - guiInntekt.redigertAvSaksbehandler, + true, call.getSubject(), ), ), ) }.let { - call.respond( - HttpStatusCode.OK, - mapToGUIInntekt( - it, - Opptjeningsperiode(this.beregningsDato), - guiInntekt.inntektsmottaker, - ), - ) + call.respond(HttpStatusCode.OK, it.inntektId.id) }.also { inntektKorrigeringCounter.inc() } - } } } } @@ -121,62 +210,64 @@ fun Route.uklassifisertInntekt( route("/uklassifisert/uncached/{aktørId}/{kontekstType}/{kontekstId}/{beregningsDato}") { get { val callId = call.callId - withContext(Dispatchers.IO) { + withContext(coroutineContext) { call.withInntektRequest("GET /uklassifisert/uncached/") { val person = personOppslag.hentPerson(this.aktørId) val opptjeningsperiode = Opptjeningsperiode(this.beregningsDato) - toInntektskomponentRequest(person, opptjeningsperiode).let { - logger.info { "Henter nye inntekter fra inntektskomponenten" } - inntektskomponentClient.getInntekt(it, callId = callId) - }.let { - logger.info { "Fikk nye inntekter fra inntektskomponenten" } - val inntektsmottaker = - Inntektsmottaker(person.fødselsnummer, person.sammensattNavn()) - mapToGUIInntekt(it, opptjeningsperiode, inntektsmottaker) - }.let { - call.respond(HttpStatusCode.OK, it) - }.also { - inntektOppfriskingCounter.inc() - } + toInntektskomponentRequest(person, opptjeningsperiode) + .let { + logger.info { "Henter nye inntekter fra inntektskomponenten" } + inntektskomponentClient.getInntekt(it, callId = callId) + }.let { + logger.info { "Fikk nye inntekter fra inntektskomponenten" } + val inntektsmottaker = + Inntektsmottaker(person.fødselsnummer, person.sammensattNavn()) + mapToGUIInntekt(it, opptjeningsperiode, inntektsmottaker) + }.let { + call.respond(HttpStatusCode.OK, it) + }.also { + inntektOppfriskingCounter.inc() + } } } } post { - withContext(Dispatchers.IO) { + withContext(coroutineContext) { call.withInntektRequest("POST /uklassifisert/uncached/") { val guiInntekt = call.receive() val person = personOppslag.hentPerson(this.aktørId) - mapToDetachedInntekt(guiInntekt).let { - inntektStore.storeInntekt( - StoreInntektCommand( - inntektparametre = - Inntektparametre( - aktørId = person.aktørId, - fødselsnummer = person.fødselsnummer, - regelkontekst = RegelKontekst(this.kontekstId, this.kontekstType), - beregningsdato = this.beregningsDato, - ), - inntekt = it.inntekt, - manueltRedigert = - ManueltRedigert.from( - guiInntekt.redigertAvSaksbehandler, - call.getSubject(), - ), - ), - ) - }.let { - call.respond( - HttpStatusCode.OK, - mapToGUIInntekt( - it, - Opptjeningsperiode(this.beregningsDato), - guiInntekt.inntektsmottaker, - ), - ) - }.also { - inntektOppfriskingBruktCounter.inc() - } + mapToDetachedInntekt(guiInntekt) + .let { + inntektStore.storeInntekt( + StoreInntektCommand( + inntektparametre = + Inntektparametre( + aktørId = person.aktørId, + fødselsnummer = person.fødselsnummer, + regelkontekst = RegelKontekst(this.kontekstId, this.kontekstType), + beregningsdato = this.beregningsDato, + ), + inntekt = it.inntekt, + manueltRedigert = + ManueltRedigert.from( + guiInntekt.redigertAvSaksbehandler, + call.getSubject(), + ), + ), + ) + }.let { + call.respond( + HttpStatusCode.OK, + mapToGUIInntekt( + it, + Opptjeningsperiode(this.beregningsDato), + guiInntekt.inntektsmottaker, + ), + ) + }.also { + inntektOppfriskingBruktCounter.inc() + } } } } @@ -184,16 +275,40 @@ fun Route.uklassifisertInntekt( } route("/verdikoder") { get { - withContext(Dispatchers.IO) { - call.respond(HttpStatusCode.OK, dataGrunnlagKlassifiseringToVerdikode.values) - } + call.respond(HttpStatusCode.OK, dataGrunnlagKlassifiseringToVerdikode.values) } } } +private suspend fun hentOrganisasjoner( + enhetsregisterClient: EnhetsregisterClient, + organisasjonsNummerListe: List?, +): List { + val organisasjoner = mutableListOf() + organisasjonsNummerListe?.forEach { orgNr -> + runCatching { + enhetsregisterClient.hentEnhet(orgNr) + }.onFailure { + logger.error(it) { "Feil ved henting av organisasjonsnavn for $it" } + }.onSuccess { + val organisasjonsNavnOgIdMapping = + Organisasjon( + organisasjonsnummer = orgNr, + navn = it, + ) + organisasjoner.add(organisasjonsNavnOgIdMapping) + } + } + + return organisasjoner +} + private fun ApplicationCall.getSubject(): String { return runCatching { - this.authentication.principal()?.payload?.subject + this.authentication + .principal() + ?.payload + ?.subject ?: throw JWTDecodeException("Unable to get subject from JWT") }.getOrElse { logger.error(it) { "Unable to get subject" } diff --git a/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/v1/models/InntekterDto.kt b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/v1/models/InntekterDto.kt new file mode 100644 index 00000000..01357aac --- /dev/null +++ b/dp-inntekt-api/src/main/kotlin/no/nav/dagpenger/inntekt/v1/models/InntekterDto.kt @@ -0,0 +1,84 @@ +package no.nav.dagpenger.inntekt.v1.models + +import no.nav.dagpenger.inntekt.db.InntektId +import no.nav.dagpenger.inntekt.db.StoredInntekt +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.Aktoer +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.AktoerType.NATURLIG_IDENT +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.ArbeidsInntektInformasjon +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.ArbeidsInntektMaaned +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.Avvik +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.Inntekt +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektkomponentResponse +import no.nav.dagpenger.inntekt.mapping.Inntektsmottaker +import no.nav.dagpenger.inntekt.mapping.Virksomhet +import java.time.LocalDateTime.now +import java.time.YearMonth + +data class InntekterDto( + val virksomheter: List, + val mottaker: Inntektsmottaker, +) + +fun InntekterDto.mapToStoredInntekt(inntektId: String): StoredInntekt = + StoredInntekt( + inntektId = InntektId(id = inntektId), + inntekt = mapToInntektkomponentResponse(this), + manueltRedigert = true, + timestamp = now(), + ) + +private fun mapToInntektkomponentResponse(inntekterDto: InntekterDto): InntektkomponentResponse { + val inntektPerÅrOgMåned = mutableMapOf?, MutableList?>>() + inntekterDto.virksomheter.forEach { virksomhet -> + virksomhet.inntekter?.map { inntektMaaned -> + val inntekter = inntektPerÅrOgMåned[inntektMaaned.aarMaaned]?.first ?: mutableListOf() + inntekter.add( + Inntekt( + inntektMaaned.belop, + inntektMaaned.fordel, + inntektMaaned.beskrivelse, + inntektMaaned.inntektskilde, + inntektMaaned.inntektsstatus, + inntektMaaned.inntektsperiodetype ?: "Maaned", + inntektMaaned.leveringstidspunkt, + inntektMaaned.opptjeningsland, + inntektMaaned.opptjeningsperiode, + inntektMaaned.skattemessigBosattLand, + inntektMaaned.utbetaltIMaaned, + inntektMaaned.opplysningspliktig, + inntektMaaned.inntektsinnsender, + inntektMaaned.virksomhet, + inntektMaaned.inntektsmottaker, + inntektMaaned.inngaarIGrunnlagForTrekk, + inntektMaaned.utloeserArbeidsgiveravgift, + inntektMaaned.informasjonsstatus, + inntektMaaned.inntektType, + inntektMaaned.tilleggsinformasjon, + ), + ) + inntektPerÅrOgMåned.put( + inntektMaaned.aarMaaned, + Pair(inntekter, inntektPerÅrOgMåned[inntektMaaned.aarMaaned]?.second), + ) + } + virksomhet.avvikListe.map { + val avvik = inntektPerÅrOgMåned[it.avvikPeriode]?.second ?: mutableListOf() + avvik.add(it) + inntektPerÅrOgMåned.put( + it.avvikPeriode, + Pair(inntektPerÅrOgMåned[it.avvikPeriode]?.first, avvik), + ) + } + } + + return InntektkomponentResponse( + inntektPerÅrOgMåned.map { (yearMonth, inntekter) -> + ArbeidsInntektMaaned( + aarMaaned = yearMonth, + arbeidsInntektInformasjon = ArbeidsInntektInformasjon(inntekter.first), + avvikListe = inntekter.second, + ) + }, + Aktoer(NATURLIG_IDENT, inntekterDto.mottaker.pnr ?: throw IllegalArgumentException("Fødselsenummer mangler")), + ) +} diff --git a/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/mapping/MapToFromGUIInntektTest.kt b/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/mapping/MapToFromGUIInntektTest.kt index 192694fe..e8642e23 100644 --- a/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/mapping/MapToFromGUIInntektTest.kt +++ b/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/mapping/MapToFromGUIInntektTest.kt @@ -127,27 +127,41 @@ internal class KategoriseringTest { val guiInntekt = mapToGUIInntekt(storedInntekt, Opptjeningsperiode(LocalDate.now()), inntektsmottaker) assertEquals( "Aksjer/grunnfondsbevis til underkurs", - guiInntekt.inntekt.arbeidsInntektMaaned?.first()?.arbeidsInntektInformasjon?.inntektListe?.first()?.verdikode, + guiInntekt.inntekt.arbeidsInntektMaaned + ?.first() + ?.arbeidsInntektInformasjon + ?.inntektListe + ?.first() + ?.verdikode, ) assertEquals( "Fastlønn", - guiInntekt.inntekt.arbeidsInntektMaaned?.first { - it.aarMaaned == - YearMonth.of( - 2019, - 6, - ) - }?.arbeidsInntektInformasjon?.inntektListe?.first()?.verdikode, + guiInntekt.inntekt.arbeidsInntektMaaned + ?.first { + it.aarMaaned == + YearMonth.of( + 2019, + 6, + ) + }?.arbeidsInntektInformasjon + ?.inntektListe + ?.first() + ?.verdikode, ) assertEquals( "Hyre - Annet", - guiInntekt.inntekt.arbeidsInntektMaaned?.filter { - it.aarMaaned == - YearMonth.of( - 2019, - 6, - ) - }?.first()?.arbeidsInntektInformasjon?.inntektListe?.last()?.verdikode, + guiInntekt.inntekt.arbeidsInntektMaaned + ?.filter { + it.aarMaaned == + YearMonth.of( + 2019, + 6, + ) + }?.first() + ?.arbeidsInntektInformasjon + ?.inntektListe + ?.last() + ?.verdikode, ) } @@ -217,15 +231,32 @@ internal class KategoriseringTest { assertEquals( InntektBeskrivelse.ANNET, - mappedInntekt.inntekt.arbeidsInntektMaaned?.first()?.arbeidsInntektInformasjon?.inntektListe?.first()?.beskrivelse, + mappedInntekt.inntekt.arbeidsInntektMaaned + ?.first() + ?.arbeidsInntektInformasjon + ?.inntektListe + ?.first() + ?.beskrivelse, ) assertEquals( InntektType.LOENNSINNTEKT, - mappedInntekt.inntekt.arbeidsInntektMaaned?.first()?.arbeidsInntektInformasjon?.inntektListe?.first()?.inntektType, + mappedInntekt.inntekt.arbeidsInntektMaaned + ?.first() + ?.arbeidsInntektInformasjon + ?.inntektListe + ?.first() + ?.inntektType, ) assertEquals( SpesielleInntjeningsforhold.HYRE_TIL_MANNSKAP_PAA_FISKE_SMAAHVALFANGST_OG_SELFANGSTFARTOEY, - mappedInntekt.inntekt.arbeidsInntektMaaned?.first()?.arbeidsInntektInformasjon?.inntektListe?.first()?.tilleggsinformasjon?.tilleggsinformasjonDetaljer?.spesielleInntjeningsforhold, + mappedInntekt.inntekt.arbeidsInntektMaaned + ?.first() + ?.arbeidsInntektInformasjon + ?.inntektListe + ?.first() + ?.tilleggsinformasjon + ?.tilleggsinformasjonDetaljer + ?.spesielleInntjeningsforhold, ) } diff --git a/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/mapping/MapToInntektFrontendTest.kt b/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/mapping/MapToInntektFrontendTest.kt new file mode 100644 index 00000000..505cc1a5 --- /dev/null +++ b/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/mapping/MapToInntektFrontendTest.kt @@ -0,0 +1,348 @@ +package no.nav.dagpenger.inntekt.mapping + +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.Aktoer +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.AktoerType +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.ArbeidsInntektInformasjon +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.ArbeidsInntektMaaned +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.Inntekt +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektBeskrivelse +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektType +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektkomponentResponse +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.TilleggInformasjon +import no.nav.dagpenger.inntekt.serder.jacksonObjectMapper +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.YearMonth +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +val inntektkomponentResponse = + InntektkomponentResponse( + ident = + Aktoer( + aktoerType = AktoerType.AKTOER_ID, + identifikator = "2044350291600", + ), + arbeidsInntektMaaned = + listOf( + ArbeidsInntektMaaned( + aarMaaned = YearMonth.of(2025, 1), + arbeidsInntektInformasjon = + ArbeidsInntektInformasjon( + inntektListe = + listOf( + Inntekt( + beloep = BigDecimal(50000), + fordel = "kontantytelse", + virksomhet = + Aktoer( + aktoerType = AktoerType.ORGANISASJON, + identifikator = "896929119", + ), + beskrivelse = InntektBeskrivelse.FASTLOENN, + inntektType = InntektType.LOENNSINNTEKT, + inntektskilde = "A-ordningen", + inntektsstatus = "LoependeInnrapportert", + opptjeningsland = "NO", + utbetaltIMaaned = YearMonth.of(2025, 1), + inntektsmottaker = + Aktoer( + aktoerType = AktoerType.AKTOER_ID, + identifikator = "2044350291600", + ), + informasjonsstatus = "InngaarAlltid", + opplysningspliktig = + Aktoer( + aktoerType = AktoerType.ORGANISASJON, + identifikator = "963743254", + ), + inntektsperiodetype = "Maaned", + tilleggsinformasjon = + TilleggInformasjon( + kategori = "NorskKontinentalsokkel", + tilleggsinformasjonDetaljer = null, + ), + skattemessigBosattLand = "NO", + inngaarIGrunnlagForTrekk = true, + utloeserArbeidsgiveravgift = true, + ), + ), + ), + avvikListe = emptyList(), + ), + ArbeidsInntektMaaned( + aarMaaned = YearMonth.of(2025, 2), + arbeidsInntektInformasjon = + ArbeidsInntektInformasjon( + inntektListe = + listOf( + Inntekt( + beloep = BigDecimal(50000), + fordel = "kontantytelse", + virksomhet = + Aktoer( + aktoerType = AktoerType.ORGANISASJON, + identifikator = "896929119", + ), + beskrivelse = InntektBeskrivelse.FASTLOENN, + inntektType = InntektType.LOENNSINNTEKT, + inntektskilde = "A-ordningen", + inntektsstatus = "LoependeInnrapportert", + opptjeningsland = "NO", + utbetaltIMaaned = YearMonth.of(2025, 2), + inntektsmottaker = + Aktoer( + aktoerType = AktoerType.AKTOER_ID, + identifikator = "2044350291600", + ), + informasjonsstatus = "InngaarAlltid", + opplysningspliktig = + Aktoer( + aktoerType = AktoerType.ORGANISASJON, + identifikator = "963743254", + ), + inntektsperiodetype = "Maaned", + tilleggsinformasjon = + TilleggInformasjon( + kategori = "NorskKontinentalsokkel", + tilleggsinformasjonDetaljer = null, + ), + skattemessigBosattLand = "NO", + inngaarIGrunnlagForTrekk = true, + utloeserArbeidsgiveravgift = true, + ), + ), + ), + avvikListe = emptyList(), + ), + ArbeidsInntektMaaned( + aarMaaned = YearMonth.of(2025, 3), + arbeidsInntektInformasjon = + ArbeidsInntektInformasjon( + inntektListe = + listOf( + Inntekt( + beloep = BigDecimal(50000), + fordel = "kontantytelse", + virksomhet = + Aktoer( + aktoerType = AktoerType.ORGANISASJON, + identifikator = "896929120", + ), + beskrivelse = InntektBeskrivelse.FASTLOENN, + inntektType = InntektType.LOENNSINNTEKT, + inntektskilde = "A-ordningen", + inntektsstatus = "LoependeInnrapportert", + opptjeningsland = "NO", + utbetaltIMaaned = YearMonth.of(2025, 3), + inntektsmottaker = + Aktoer( + aktoerType = AktoerType.AKTOER_ID, + identifikator = "2044350291600", + ), + informasjonsstatus = "InngaarAlltid", + opplysningspliktig = + Aktoer( + aktoerType = AktoerType.ORGANISASJON, + identifikator = "963743254", + ), + inntektsperiodetype = "Maaned", + tilleggsinformasjon = + TilleggInformasjon( + kategori = "NorskKontinentalsokkel", + tilleggsinformasjonDetaljer = null, + ), + skattemessigBosattLand = "NO", + inngaarIGrunnlagForTrekk = true, + utloeserArbeidsgiveravgift = true, + ), + ), + ), + avvikListe = emptyList(), + ), + ), + ) + +val mottaker = + Inntektsmottaker( + pnr = "2044350291600", + navn = "Ola Nordmann", + ) + +val organisasjoner = + mutableListOf( + Organisasjon( + organisasjonsnummer = "896929119", + navn = "Test Org 119", + ), + Organisasjon( + organisasjonsnummer = "896929120", + navn = "Test Org 120", + ), + ) + +class MapToInntektFrontendTest { + @Test + fun `Map inntekt til InntektForVirksomhetMedPersonInformasjon`() { + val mappedToInntektFrontend = + inntektkomponentResponse.mapToFrontend( + mottaker, + organisasjoner, + ) + + assertEquals(2, mappedToInntektFrontend.virksomheter.size) + assertEquals(mottaker, mappedToInntektFrontend.mottaker) + + assertTrue { mappedToInntektFrontend.virksomheter.any { it.virksomhetsnummer == "896929119" } } + assertTrue { mappedToInntektFrontend.virksomheter.any { it.virksomhetsnummer == "896929120" } } + assertFalse { mappedToInntektFrontend.virksomheter.any { it.virksomhetsnummer == "8969291001" } } + + assertEquals( + 2, + mappedToInntektFrontend.virksomheter + .filter { it.virksomhetsnummer == "896929119" }[0] + .inntekter + ?.size, + ) + + assertEquals( + BigDecimal(100000), + mappedToInntektFrontend.virksomheter + .filter { it.virksomhetsnummer == "896929119" }[0] + .totalBeløp, + ) + + assertEquals( + 1, + mappedToInntektFrontend.virksomheter + .filter { it.virksomhetsnummer == "896929120" }[0] + .inntekter + ?.size, + ) + + assertEquals( + BigDecimal(50000), + mappedToInntektFrontend.virksomheter + .filter { it.virksomhetsnummer == "896929120" }[0] + .totalBeløp, + ) + } + + @Test + fun `Map inntekt til Inntekt med tom virksomhetsdata og få tom navn`() { + val inntektkomponentResponseMedTomVirksomhet = + InntektkomponentResponse( + ident = + Aktoer( + aktoerType = AktoerType.AKTOER_ID, + identifikator = "2044350291600", + ), + arbeidsInntektMaaned = + listOf( + ArbeidsInntektMaaned( + aarMaaned = YearMonth.of(2025, 1), + arbeidsInntektInformasjon = + ArbeidsInntektInformasjon( + inntektListe = + listOf( + Inntekt( + beloep = BigDecimal(50000), + fordel = "kontantytelse", + virksomhet = null, + beskrivelse = InntektBeskrivelse.FASTLOENN, + inntektType = InntektType.LOENNSINNTEKT, + inntektskilde = "A-ordningen", + inntektsstatus = "LoependeInnrapportert", + utbetaltIMaaned = YearMonth.of(2025, 1), + inntektsperiodetype = "Maaned", + ), + ), + ), + avvikListe = emptyList(), + ), + ArbeidsInntektMaaned( + aarMaaned = YearMonth.of(2025, 1), + arbeidsInntektInformasjon = + ArbeidsInntektInformasjon( + inntektListe = + listOf( + Inntekt( + beloep = BigDecimal(2), + fordel = "kontantytelse", + virksomhet = null, + beskrivelse = InntektBeskrivelse.FASTLOENN, + inntektType = InntektType.LOENNSINNTEKT, + inntektskilde = "A-ordningen", + inntektsstatus = "LoependeInnrapportert", + utbetaltIMaaned = YearMonth.of(2025, 1), + inntektsperiodetype = "Maaned", + ), + ), + ), + avvikListe = emptyList(), + ), + ArbeidsInntektMaaned( + aarMaaned = YearMonth.of(2025, 3), + arbeidsInntektInformasjon = + ArbeidsInntektInformasjon( + inntektListe = + listOf( + Inntekt( + beloep = BigDecimal(50000), + fordel = "kontantytelse", + virksomhet = + Aktoer( + aktoerType = AktoerType.ORGANISASJON, + identifikator = "896929120", + ), + beskrivelse = InntektBeskrivelse.FASTLOENN, + inntektType = InntektType.LOENNSINNTEKT, + inntektskilde = "A-ordningen", + inntektsstatus = "LoependeInnrapportert", + opptjeningsland = "NO", + utbetaltIMaaned = YearMonth.of(2025, 3), + inntektsmottaker = + Aktoer( + aktoerType = AktoerType.AKTOER_ID, + identifikator = "2044350291600", + ), + informasjonsstatus = "InngaarAlltid", + opplysningspliktig = + Aktoer( + aktoerType = AktoerType.ORGANISASJON, + identifikator = "963743254", + ), + inntektsperiodetype = "Maaned", + tilleggsinformasjon = + TilleggInformasjon( + kategori = "NorskKontinentalsokkel", + tilleggsinformasjonDetaljer = null, + ), + skattemessigBosattLand = "NO", + inngaarIGrunnlagForTrekk = true, + utloeserArbeidsgiveravgift = true, + ), + ), + ), + avvikListe = emptyList(), + ), + ), + ) + + val mapTilFrontendMedNullVirksomhet = + inntektkomponentResponseMedTomVirksomhet.mapToFrontend(mottaker, organisasjoner) + assertEquals(3, mapTilFrontendMedNullVirksomhet.virksomheter.size) + assertEquals(2, mapTilFrontendMedNullVirksomhet.virksomheter.filter { it.virksomhetsnummer == "" }.size) + assertEquals(1, mapTilFrontendMedNullVirksomhet.virksomheter.filter { it.virksomhetsnummer == "896929120" }.size) + + jacksonObjectMapper.writeValueAsString(mapTilFrontendMedNullVirksomhet) + println( + "mapTilFrontendMedNullVirksomhetjson: ${ + jacksonObjectMapper.writeValueAsString( + mapTilFrontendMedNullVirksomhet, + ) + }", + ) + } +} diff --git a/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/v1/TestApplication.kt b/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/v1/TestApplication.kt index 0cafcee9..b0c6a89e 100644 --- a/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/v1/TestApplication.kt +++ b/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/v1/TestApplication.kt @@ -29,6 +29,8 @@ import no.nav.security.mock.oauth2.MockOAuth2Server @Suppress("ktlint:standard:function-naming") internal object TestApplication { private const val ISSUER_ID = "default" + const val TEST_OAUTH_USER = "user" + val mockOAuth2Server: MockOAuth2Server by lazy { MockOAuth2Server().also { it.start() @@ -37,7 +39,7 @@ internal object TestApplication { val testOAuthToken: String by lazy { mockOAuth2Server.issueToken( issuerId = ISSUER_ID, - subject = "user", + subject = TEST_OAUTH_USER, ).serialize() } diff --git a/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/v1/UklassifisertInntektRouteTest.kt b/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/v1/UklassifisertInntektRouteTest.kt index 7e775557..25634e43 100644 --- a/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/v1/UklassifisertInntektRouteTest.kt +++ b/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/v1/UklassifisertInntektRouteTest.kt @@ -2,23 +2,32 @@ package no.nav.dagpenger.inntekt.v1 import com.fasterxml.jackson.module.kotlin.readValue import de.huxhorn.sulky.ulid.ULID +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.ktor.client.request.get import io.ktor.client.statement.bodyAsText import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode +import io.ktor.http.HttpStatusCode.Companion.BadRequest +import io.ktor.http.HttpStatusCode.Companion.OK +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify import kotlinx.coroutines.runBlocking import no.nav.dagpenger.inntekt.Problem import no.nav.dagpenger.inntekt.db.DetachedInntekt import no.nav.dagpenger.inntekt.db.InntektId +import no.nav.dagpenger.inntekt.db.InntektPersonMapping import no.nav.dagpenger.inntekt.db.InntektStore import no.nav.dagpenger.inntekt.db.Inntektparametre import no.nav.dagpenger.inntekt.db.ManueltRedigert import no.nav.dagpenger.inntekt.db.RegelKontekst import no.nav.dagpenger.inntekt.db.StoreInntektCommand import no.nav.dagpenger.inntekt.db.StoredInntekt +import no.nav.dagpenger.inntekt.db.StoredInntektMedFnr import no.nav.dagpenger.inntekt.inntektskomponenten.v1.Aktoer import no.nav.dagpenger.inntekt.inntektskomponenten.v1.AktoerType import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektBeskrivelse @@ -33,10 +42,13 @@ import no.nav.dagpenger.inntekt.mapping.GUIInntektsKomponentResponse import no.nav.dagpenger.inntekt.mapping.InntektMedVerdikode import no.nav.dagpenger.inntekt.oppslag.Person import no.nav.dagpenger.inntekt.oppslag.PersonOppslag +import no.nav.dagpenger.inntekt.oppslag.enhetsregister.EnhetsregisterClient import no.nav.dagpenger.inntekt.serder.jacksonObjectMapper +import no.nav.dagpenger.inntekt.v1.TestApplication.TEST_OAUTH_USER import no.nav.dagpenger.inntekt.v1.TestApplication.autentisert import no.nav.dagpenger.inntekt.v1.TestApplication.mockInntektApi import no.nav.dagpenger.inntekt.v1.TestApplication.withMockAuthServerAndTestApplication +import no.nav.dagpenger.inntekt.v1.models.InntekterDto import org.junit.jupiter.api.Test import java.math.BigDecimal import java.time.LocalDate @@ -203,7 +215,7 @@ internal class UklassifisertInntektRouteTest { "$uklassifisertInntekt/${foundQuery.aktørId}/${foundQuery.regelkontekst.type}/${foundQuery.regelkontekst.id}/blabla", ) - assertEquals(HttpStatusCode.BadRequest, response.status) + assertEquals(BadRequest, response.status) } @Test @@ -221,7 +233,7 @@ internal class UklassifisertInntektRouteTest { endepunkt = "$uklassifisertInntekt/${foundQuery.aktørId}/${foundQuery.regelkontekst.type}/${foundQuery.regelkontekst.id}/${foundQuery.beregningsdato}", ) - assertEquals(HttpStatusCode.OK, response.status) + assertEquals(OK, response.status) val storedInntekt = jacksonObjectMapper.readValue(response.bodyAsText()) assertEquals(storedInntekt.inntektId, inntektId) @@ -241,7 +253,7 @@ internal class UklassifisertInntektRouteTest { httpMethod = HttpMethod.Get, endepunkt = "$uklassifisertInntekt/uncached/${foundQuery.aktørId}/${foundQuery.regelkontekst.type}/${foundQuery.regelkontekst.id}/${foundQuery.beregningsdato}", ) - assertEquals(HttpStatusCode.OK, response.status) + assertEquals(OK, response.status) val uncachedInntekt = jacksonObjectMapper.readValue(response.bodyAsText()) assertEquals(emptyInntekt.ident, uncachedInntekt.inntekt.ident) @@ -271,7 +283,7 @@ internal class UklassifisertInntektRouteTest { endepunkt = "v1/inntekt/uklassifisert/${foundQuery.aktørId}/${foundQuery.regelkontekst.type}/${foundQuery.regelkontekst.id}/${foundQuery.beregningsdato}", body = jacksonObjectMapper.writeValueAsString(guiInntekt), ) - assertEquals(HttpStatusCode.OK, response.status) + assertEquals(OK, response.status) val uncachedInntekt = jacksonObjectMapper.readValue(response.bodyAsText()) assertEquals(emptyInntekt.ident, uncachedInntekt.inntekt.ident) @@ -301,7 +313,7 @@ internal class UklassifisertInntektRouteTest { endepunkt = "v1/inntekt/uklassifisert/${foundQuery.aktørId}/${foundQuery.regelkontekst.type}/${foundQuery.regelkontekst.id}/${foundQuery.beregningsdato}", body = jacksonObjectMapper.writeValueAsString(guiInntekt), ) - assertEquals(HttpStatusCode.OK, response.status) + assertEquals(OK, response.status) val storedInntekt = jacksonObjectMapper.readValue(response.bodyAsText()) assertEquals(storedInntekt.inntektId, inntektId) @@ -363,7 +375,7 @@ internal class UklassifisertInntektRouteTest { body = body.replace(oldValue = "123", newValue = ""), ) - response.status shouldBe HttpStatusCode.BadRequest + response.status shouldBe BadRequest } @Test @@ -390,7 +402,7 @@ internal class UklassifisertInntektRouteTest { endepunkt = "v1/inntekt/uklassifisert/uncached/${foundQuery.aktørId}/${foundQuery.regelkontekst.type}/${foundQuery.regelkontekst.id}/${foundQuery.beregningsdato}", body = jacksonObjectMapper.writeValueAsString(guiInntekt), ) - assertEquals(HttpStatusCode.OK, response.status) + assertEquals(OK, response.status) val storedInntekt = jacksonObjectMapper.readValue(response.bodyAsText()) assertEquals(storedInntekt.inntektId, inntektId) @@ -421,7 +433,7 @@ internal class UklassifisertInntektRouteTest { endepunkt = "v1/inntekt/uklassifisert/uncached/${foundQuery.aktørId}/${foundQuery.regelkontekst.type}/${foundQuery.regelkontekst.id}/${foundQuery.beregningsdato}", body = jacksonObjectMapper.writeValueAsString(guiInntekt), ) - assertEquals(HttpStatusCode.OK, response.status) + assertEquals(OK, response.status) val storedInntekt = jacksonObjectMapper.readValue(response.bodyAsText()) assertEquals(storedInntekt.inntektId, inntektId) @@ -442,8 +454,120 @@ internal class UklassifisertInntektRouteTest { httpMethod = HttpMethod.Get, endepunkt = "v1/inntekt/verdikoder", ) - assertEquals(HttpStatusCode.OK, response.status) + assertEquals(OK, response.status) assertEquals("application/json; charset=UTF-8", response.headers["Content-Type"]) assertTrue(runCatching { jacksonObjectMapper.readValue>(response.bodyAsText()) }.isSuccess) } + + @Test + fun `Get request for uklassifisert inntekt med ugyldig inntektID returnerer 400 BAD REQUEST`() = + withMockAuthServerAndTestApplication( + mockInntektApi( + inntektskomponentClient = inntektskomponentClientMock, + inntektStore = inntektStoreMock, + personOppslag = personOppslagMock, + enhetsregisterClient = mockk(), + ), + ) { + val response = + autentisert( + httpMethod = HttpMethod.Get, + endepunkt = "$uklassifisertInntekt/UGYLDIG_ID", + ) + + response.status shouldBe BadRequest + } + + @Test + fun `Get request for uklassifisert inntekt med inntektID returnerer 200 ok`() { + val enhetsregisterClientMock = mockk(relaxed = true) + return withMockAuthServerAndTestApplication( + mockInntektApi( + inntektskomponentClient = inntektskomponentClientMock, + inntektStore = inntektStoreMock, + personOppslag = personOppslagMock, + enhetsregisterClient = enhetsregisterClientMock, + ), + ) { + coEvery { enhetsregisterClientMock.hentEnhet("1111111") } returns "Test Org" + val body = + UklassifisertInntektRouteTest::class.java + .getResource("/test-data/example-inntekt-med-inntektId-payload.json") + ?.readText() + every { + inntektStoreMock.getInntektMedPersonFnr(inntektId) + } returns + StoredInntektMedFnr( + inntektId, + inntekt = + jacksonObjectMapper.readValue(body!!), + manueltRedigert = false, + timestamp = LocalDateTime.now(), + fødselsnummer = fødselsnummer, + ) + + val response = + autentisert( + httpMethod = HttpMethod.Get, + endepunkt = "$uklassifisertInntekt/${inntektId.id}", + ) + + response.status shouldBe OK + val storedInntekt = jacksonObjectMapper.readValue(response.bodyAsText()) + storedInntekt.virksomheter shouldHaveSize 2 + storedInntekt.virksomheter[0].inntekter?.shouldHaveSize(4) + storedInntekt.virksomheter.first().virksomhetsnummer shouldBe "1111111" + storedInntekt.virksomheter.first().virksomhetsnavn shouldBe "Test Org" + storedInntekt.virksomheter[1].virksomhetsnummer shouldBe "2222222" + storedInntekt.virksomheter[1].virksomhetsnavn shouldBe "" + } + } + + @Test + fun `Post request for uklassifisert inntekt med inntektId lagrer og returnerer ny ID`() = + withMockAuthServerAndTestApplication( + moduleFunction = + mockInntektApi( + inntektskomponentClient = inntektskomponentClientMock, + inntektStore = inntektStoreMock, + ), + ) { + val body = + UklassifisertInntektRouteTest::class.java + .getResource("/test-data/expected-uklassifisert-post-body.json") + ?.readText() + val inntekterDto = jacksonObjectMapper.readValue(body!!) + + val inntektPersonMapping = + InntektPersonMapping( + inntektId = inntektId, + aktørId = "123456789", + fnr = null, + kontekstId = "kontekstId", + beregningsdato = LocalDate.now(), + timestamp = LocalDateTime.now(), + kontekstType = "kontekstType", + ) + every { inntektStoreMock.getInntektPersonMapping(any()) } returns inntektPersonMapping + + val storeInntektCommandSlot = slot() + every { inntektStoreMock.storeInntekt(capture(storeInntektCommandSlot), any()) } returns storedInntekt + + val response = + autentisert( + httpMethod = HttpMethod.Post, + endepunkt = "$uklassifisertInntekt/${inntektId.id}", + body = body, + ) + + response.bodyAsText() shouldBe storedInntekt.inntektId.id + verify(exactly = 1) { inntektStoreMock.storeInntekt(any(), any()) } + storeInntektCommandSlot.captured.inntektparametre.aktørId shouldBe inntektPersonMapping.aktørId + storeInntektCommandSlot.captured.inntektparametre.fødselsnummer shouldBe inntekterDto.mottaker.pnr + storeInntektCommandSlot.captured.inntektparametre.regelkontekst.id shouldBe inntektPersonMapping.kontekstId + storeInntektCommandSlot.captured.inntektparametre.regelkontekst.type shouldBe inntektPersonMapping.kontekstType + storeInntektCommandSlot.captured.inntektparametre.beregningsdato shouldBe inntektPersonMapping.beregningsdato + storeInntektCommandSlot.captured.manueltRedigert.shouldNotBeNull() + storeInntektCommandSlot.captured.manueltRedigert!!.redigertAv shouldBe TEST_OAUTH_USER + } } diff --git a/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/v1/models/InntekterDtoTest.kt b/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/v1/models/InntekterDtoTest.kt new file mode 100644 index 00000000..77ebcda5 --- /dev/null +++ b/dp-inntekt-api/src/test/kotlin/no/nav/dagpenger/inntekt/v1/models/InntekterDtoTest.kt @@ -0,0 +1,130 @@ +package no.nav.dagpenger.inntekt.v1.models + +import com.fasterxml.jackson.module.kotlin.readValue +import de.huxhorn.sulky.ulid.ULID +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.Aktoer +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.AktoerType.AKTOER_ID +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.AktoerType.NATURLIG_IDENT +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.AktoerType.ORGANISASJON +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.Avvik +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.Inntekt +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektBeskrivelse.BIL +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.InntektType.LOENNSINNTEKT +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.Periode +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.SpesielleInntjeningsforhold.LOENN_VED_KONKURS_ELLER_STATSGARANTI_OSV +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.TilleggInformasjon +import no.nav.dagpenger.inntekt.inntektskomponenten.v1.TilleggInformasjonsDetaljer +import no.nav.dagpenger.inntekt.serder.jacksonObjectMapper +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class InntekterDtoTest { + @Test + fun `mapToStoredInntekt mapper til forventet resultat`() { + val inntektId = ULID().nextULID() + val inntekterDto = + jacksonObjectMapper.readValue( + this::class.java.getResource("/test-data/expected-uklassifisert-post-body.json")?.readText()!!, + ) + + val storedInntekt = inntekterDto.mapToStoredInntekt(inntektId = inntektId) + + storedInntekt.manueltRedigert shouldBe true + storedInntekt.timestamp.shouldNotBeNull() + storedInntekt.inntektId.id shouldBe inntektId + storedInntekt.inntekt.ident.aktoerType shouldBe NATURLIG_IDENT + storedInntekt.inntekt.ident.identifikator shouldBe inntekterDto.mottaker.pnr + storedInntekt.inntekt.arbeidsInntektMaaned?.shouldHaveSize(5) + storedInntekt.inntekt.arbeidsInntektMaaned?.first().let { + it?.aarMaaned shouldBe YearMonth.of(2023, 1) + it?.avvikListe.shouldContainExactly( + Avvik( + ident = Aktoer(AKTOER_ID, "287631"), + opplysningspliktig = Aktoer(NATURLIG_IDENT, "06221841830"), + virksomhet = Aktoer(ORGANISASJON, "123456789"), + avvikPeriode = YearMonth.of(2023, 1), + tekst = "Dette er et avvik for 2023-01", + ), + ) + it?.arbeidsInntektInformasjon?.inntektListe?.shouldHaveSize(1) + it?.arbeidsInntektInformasjon?.inntektListe?.shouldContain( + Inntekt( + beloep = BigDecimal("50000.00"), + fordel = "kontantytelse", + beskrivelse = BIL, + inntektskilde = "A-ordningen", + inntektsstatus = "LoependeInnrapportert", + inntektsperiodetype = "Maaned", + leveringstidspunkt = YearMonth.of(2023, 1), + opptjeningsland = "NO", + opptjeningsperiode = Periode(LocalDate.of(2023, 1, 4), LocalDate.of(2023, 1, 25)), + skattemessigBosattLand = "NO", + utbetaltIMaaned = YearMonth.of(2023, 2), + opplysningspliktig = Aktoer(NATURLIG_IDENT, "06221841830"), + inntektsinnsender = Aktoer(ORGANISASJON, "561235623"), + virksomhet = Aktoer(ORGANISASJON, "123456789"), + inntektsmottaker = Aktoer(AKTOER_ID, "287631"), + inngaarIGrunnlagForTrekk = true, + utloeserArbeidsgiveravgift = false, + informasjonsstatus = "InngaarAlltid", + inntektType = LOENNSINNTEKT, + tilleggsinformasjon = + TilleggInformasjon( + kategori = "enKategori", + tilleggsinformasjonDetaljer = + TilleggInformasjonsDetaljer( + detaljerType = "enDetaljerType", + spesielleInntjeningsforhold = LOENN_VED_KONKURS_ELLER_STATSGARANTI_OSV, + ), + ), + ), + ) + } + storedInntekt.inntekt.arbeidsInntektMaaned?.get(1).let { + it?.aarMaaned shouldBe YearMonth.of(2024, 1) + it?.avvikListe.shouldContainExactly( + Avvik( + ident = Aktoer(AKTOER_ID, "287631"), + opplysningspliktig = Aktoer(NATURLIG_IDENT, "06221841830"), + virksomhet = Aktoer(ORGANISASJON, "123456789"), + avvikPeriode = YearMonth.of(2024, 1), + tekst = "Dette er et avvik", + ), + Avvik( + ident = Aktoer(AKTOER_ID, "287631"), + opplysningspliktig = Aktoer(NATURLIG_IDENT, "06221841830"), + virksomhet = Aktoer(ORGANISASJON, "987654321"), + avvikPeriode = YearMonth.of(2024, 1), + tekst = "Dette er et annet avvik", + ), + ) + it?.arbeidsInntektInformasjon?.inntektListe?.shouldHaveSize(2) + } + storedInntekt.inntekt.arbeidsInntektMaaned?.get(2).let { + it?.aarMaaned shouldBe YearMonth.of(2025, 2) + it?.avvikListe shouldBe null + it?.arbeidsInntektInformasjon?.inntektListe?.shouldHaveSize(1) + it?.arbeidsInntektInformasjon?.inntektListe?.first().let { inntekt -> + inntekt?.tilleggsinformasjon shouldBe null + } + } + storedInntekt.inntekt.arbeidsInntektMaaned?.get(3).let { + it?.aarMaaned shouldBe YearMonth.of(2025, 3) + it?.avvikListe shouldBe null + it?.arbeidsInntektInformasjon?.inntektListe?.shouldHaveSize(1) + } + storedInntekt.inntekt.arbeidsInntektMaaned?.get(4).let { + it?.aarMaaned shouldBe YearMonth.of(2000, 12) + it?.avvikListe?.shouldHaveSize(1) + it?.arbeidsInntektInformasjon?.inntektListe?.shouldBeEmpty() + } + } +} diff --git a/dp-inntekt-api/src/test/resources/test-data/example-inntekt-med-inntektId-payload.json b/dp-inntekt-api/src/test/resources/test-data/example-inntekt-med-inntektId-payload.json new file mode 100644 index 00000000..2141ca53 --- /dev/null +++ b/dp-inntekt-api/src/test/resources/test-data/example-inntekt-med-inntektId-payload.json @@ -0,0 +1,167 @@ +{ + "arbeidsInntektMaaned": [ + { + "aarMaaned": "2019-01", + "arbeidsInntektInformasjon": { + "inntektListe": [ + { + "inntektType": "NAERINGSINNTEKT", + "beloep": 250000, + "fordel": "kontantytelse", + "inntektskilde": "A-ordningen", + "inntektsperiodetype": "Maaned", + "inntektsstatus": "LoependeInnrapportert", + "leveringstidspunkt": "2019-02", + "utbetaltIMaaned": "2018-03", + "opplysningspliktig": { + "identifikator": "1111111", + "aktoerType": "ORGANISASJON" + }, + "virksomhet": { + "identifikator": "1111111", + "aktoerType": "ORGANISASJON" + }, + "inntektsmottaker": { + "identifikator": "99999999999", + "aktoerType": "NATURLIG_IDENT" + }, + "inngaarIGrunnlagForTrekk": true, + "utloeserArbeidsgiveravgift": true, + "informasjonsstatus": "InngaarAlltid", + "beskrivelse": "lottKunTrygdeavgift" + } + ] + } + }, + { + "aarMaaned": "2018-03", + "arbeidsInntektInformasjon": { + "inntektListe": [ + { + "inntektType": "NAERINGSINNTEKT", + "beloep": 250000, + "fordel": "kontantytelse", + "inntektskilde": "A-ordningen", + "inntektsperiodetype": "Maaned", + "inntektsstatus": "LoependeInnrapportert", + "leveringstidspunkt": "2019-02", + "utbetaltIMaaned": "2018-03", + "opplysningspliktig": { + "identifikator": "1111111", + "aktoerType": "ORGANISASJON" + }, + "virksomhet": { + "identifikator": "1111111", + "aktoerType": "ORGANISASJON" + }, + "inntektsmottaker": { + "identifikator": "99999999999", + "aktoerType": "NATURLIG_IDENT" + }, + "inngaarIGrunnlagForTrekk": true, + "utloeserArbeidsgiveravgift": true, + "informasjonsstatus": "InngaarAlltid", + "beskrivelse": "lottKunTrygdeavgift" + } + ] + } + }, + { + "aarMaaned": "2017-04", + "arbeidsInntektInformasjon": { + "inntektListe": [ + { + "inntektType": "NAERINGSINNTEKT", + "beloep": 250000, + "fordel": "kontantytelse", + "inntektskilde": "A-ordningen", + "inntektsperiodetype": "Maaned", + "inntektsstatus": "LoependeInnrapportert", + "leveringstidspunkt": "2019-02", + "utbetaltIMaaned": "2018-03", + "opplysningspliktig": { + "identifikator": "1111111", + "aktoerType": "ORGANISASJON" + }, + "virksomhet": { + "identifikator": "1111111", + "aktoerType": "ORGANISASJON" + }, + "inntektsmottaker": { + "identifikator": "99999999999", + "aktoerType": "NATURLIG_IDENT" + }, + "inngaarIGrunnlagForTrekk": true, + "utloeserArbeidsgiveravgift": true, + "informasjonsstatus": "InngaarAlltid", + "beskrivelse": "lottKunTrygdeavgift" + } + ] + } + }, + { + "aarMaaned": "2017-12", + "arbeidsInntektInformasjon": { + "inntektListe": [ + { + "inntektType": "NAERINGSINNTEKT", + "beloep": 250000, + "fordel": "kontantytelse", + "inntektskilde": "A-ordningen", + "inntektsperiodetype": "Maaned", + "inntektsstatus": "LoependeInnrapportert", + "leveringstidspunkt": "2019-02", + "utbetaltIMaaned": "2018-03", + "opplysningspliktig": { + "identifikator": "1111111", + "aktoerType": "ORGANISASJON" + }, + "virksomhet": { + "identifikator": "1111111", + "aktoerType": "ORGANISASJON" + }, + "inntektsmottaker": { + "identifikator": "99999999999", + "aktoerType": "NATURLIG_IDENT" + }, + "inngaarIGrunnlagForTrekk": true, + "utloeserArbeidsgiveravgift": true, + "informasjonsstatus": "InngaarAlltid", + "beskrivelse": "lottKunTrygdeavgift" + }, + { + "inntektType": "NAERINGSINNTEKT", + "beloep": 250000, + "fordel": "kontantytelse", + "inntektskilde": "A-ordningen", + "inntektsperiodetype": "Maaned", + "inntektsstatus": "LoependeInnrapportert", + "leveringstidspunkt": "2019-02", + "utbetaltIMaaned": "2018-03", + "opptjeningsland": "SE", + "opplysningspliktig": { + "identifikator": "2222222", + "aktoerType": "ORGANISASJON" + }, + "virksomhet": { + "identifikator": "2222222", + "aktoerType": "ORGANISASJON" + }, + "inntektsmottaker": { + "identifikator": "-1", + "aktoerType": "NATURLIG_IDENT" + }, + "inngaarIGrunnlagForTrekk": true, + "utloeserArbeidsgiveravgift": true, + "informasjonsstatus": "InngaarAlltid", + "beskrivelse": "lottKunTrygdeavgift" + } + ] + } + } + ], + "ident": { + "identifikator": "-1", + "aktoerType": "NATURLIG_IDENT" + } +} \ No newline at end of file diff --git a/dp-inntekt-api/src/test/resources/test-data/expected-uklassifisert-post-body.json b/dp-inntekt-api/src/test/resources/test-data/expected-uklassifisert-post-body.json new file mode 100644 index 00000000..66ea7149 --- /dev/null +++ b/dp-inntekt-api/src/test/resources/test-data/expected-uklassifisert-post-body.json @@ -0,0 +1,313 @@ +{ + "virksomheter": [ + { + "virksomhetsnummer": "123456789", + "virksomhetsnavn": "Test Virksomhet", + "periode": { + "fra": "2023-01", + "til": "2024-12" + }, + "inntekter": [ + { + "belop": 50000.00, + "fordel": "kontantytelse", + "beskrivelse": "bil", + "inntektskilde": "A-ordningen", + "inntektsstatus": "LoependeInnrapportert", + "inntektsperiodetype": "Maaned", + "leveringstidspunkt": "2023-01", + "opptjeningsland": "NO", + "opptjeningsperiode": { + "startDato": "2023-01-04T00:00:00", + "sluttDato": "2023-01-25T00:00:00" + }, + "skattemessigBosattLand": "NO", + "utbetaltIMaaned": "2023-02", + "opplysningspliktig": { + "aktoerType": "NATURLIG_IDENT", + "identifikator": "06221841830" + }, + "inntektsinnsender": { + "aktoerType": "ORGANISASJON", + "identifikator": "561235623" + }, + "virksomhet": { + "aktoerType": "ORGANISASJON", + "identifikator": "123456789" + }, + "inntektsmottaker": { + "aktoerType": "AKTOER_ID", + "identifikator": "287631" + }, + "inngaarIGrunnlagForTrekk": true, + "utloeserArbeidsgiveravgift": false, + "informasjonsstatus": "InngaarAlltid", + "inntektType": "LOENNSINNTEKT", + "tilleggsinformasjon": { + "kategori": "enKategori", + "tilleggsinformasjonDetaljer": { + "detaljerType": "enDetaljerType", + "spesielleInntjeningsforhold": "loennVedKonkursEllerStatsgarantiOsv" + } + }, + "redigert": true, + "begrunnelse": "Standard", + "aarMaaned": "2023-01" + }, + { + "belop": 10000.00, + "fordel": "kontantytelse", + "beskrivelse": "bil", + "inntektskilde": "A-ordningen", + "inntektsstatus": "LoependeInnrapportert", + "inntektsperiodetype": "Maaned", + "leveringstidspunkt": "2024-01", + "opptjeningsland": "SE", + "opptjeningsperiode": { + "startDato": "2024-01-01T00:00:00", + "sluttDato": "2024-01-31T00:00:00" + }, + "skattemessigBosattLand": "NO", + "utbetaltIMaaned": "2024-01", + "opplysningspliktig": { + "aktoerType": "NATURLIG_IDENT", + "identifikator": "06221841830" + }, + "inntektsinnsender": { + "aktoerType": "ORGANISASJON", + "identifikator": "123456789" + }, + "virksomhet": { + "aktoerType": "ORGANISASJON", + "identifikator": "123456789" + }, + "inntektsmottaker": { + "aktoerType": "AKTOER_ID", + "identifikator": "287631" + }, + "inngaarIGrunnlagForTrekk": true, + "utloeserArbeidsgiveravgift": true, + "informasjonsstatus": "InngaarAlltid", + "inntektType": "NAERINGSINNTEKT", + "tilleggsinformasjon": null, + "redigert": false, + "begrunnelse": "Ikke Standard", + "aarMaaned": "2024-01" + } + ], + "totalBeløp": 60000.00, + "avvikListe": [ + { + "ident": { + "aktoerType": "AKTOER_ID", + "identifikator": "287631" + }, + "opplysningspliktig": { + "aktoerType": "NATURLIG_IDENT", + "identifikator": "06221841830" + }, + "virksomhet": { + "aktoerType": "ORGANISASJON", + "identifikator": "123456789" + }, + "avvikPeriode": "2024-01", + "tekst": "Dette er et avvik" + }, + { + "ident": { + "aktoerType": "AKTOER_ID", + "identifikator": "287631" + }, + "opplysningspliktig": { + "aktoerType": "NATURLIG_IDENT", + "identifikator": "06221841830" + }, + "virksomhet": { + "aktoerType": "ORGANISASJON", + "identifikator": "123456789" + }, + "avvikPeriode": "2023-01", + "tekst": "Dette er et avvik for 2023-01" + } + ] + }, + { + "virksomhetsnummer": "987654321", + "virksomhetsnavn": "Virksomheten Test", + "periode": { + "fra": "2024-01", + "til": "2025-03" + }, + "inntekter": [ + { + "belop": 40000.00, + "fordel": "kontantytelse", + "beskrivelse": "bil", + "inntektskilde": "A-ordningen", + "inntektsstatus": "LoependeInnrapportert", + "inntektsperiodetype": "Maaned", + "leveringstidspunkt": "2023-01", + "opptjeningsland": "NO", + "opptjeningsperiode": { + "startDato": "2025-03-15T00:00:00", + "sluttDato": "2025-03-15T00:00:00" + }, + "skattemessigBosattLand": "NO", + "utbetaltIMaaned": "2025-03", + "opplysningspliktig": { + "aktoerType": "NATURLIG_IDENT", + "identifikator": "06221841830" + }, + "inntektsinnsender": { + "aktoerType": "ORGANISASJON", + "identifikator": "987654321" + }, + "virksomhet": { + "aktoerType": "ORGANISASJON", + "identifikator": "987654321" + }, + "inntektsmottaker": { + "aktoerType": "AKTOER_ID", + "identifikator": "287631" + }, + "inngaarIGrunnlagForTrekk": true, + "utloeserArbeidsgiveravgift": true, + "informasjonsstatus": "InngaarAlltid", + "inntektType": "LOENNSINNTEKT", + "tilleggsinformasjon": null, + "redigert": true, + "begrunnelse": "Standard", + "aarMaaned": "2025-02" + }, + { + "belop": 70000.00, + "fordel": "kontantytelse", + "beskrivelse": "bil", + "inntektskilde": "A-ordningen", + "inntektsstatus": "LoependeInnrapportert", + "inntektsperiodetype": "Maaned", + "leveringstidspunkt": "2024-01", + "opptjeningsland": "SE", + "opptjeningsperiode": { + "startDato": "2025-04-03T00:00:00", + "sluttDato": "2025-04-18T00:00:00" + }, + "skattemessigBosattLand": "NO", + "utbetaltIMaaned": "2025-04", + "opplysningspliktig": { + "aktoerType": "NATURLIG_IDENT", + "identifikator": "06221841830" + }, + "inntektsinnsender": { + "aktoerType": "ORGANISASJON", + "identifikator": "123456789" + }, + "virksomhet": { + "aktoerType": "ORGANISASJON", + "identifikator": "123456789" + }, + "inntektsmottaker": { + "aktoerType": "AKTOER_ID", + "identifikator": "287631" + }, + "inngaarIGrunnlagForTrekk": true, + "utloeserArbeidsgiveravgift": true, + "informasjonsstatus": "InngaarAlltid", + "inntektType": "NAERINGSINNTEKT", + "tilleggsinformasjon": null, + "redigert": true, + "begrunnelse": "Ikke Standard", + "aarMaaned": "2025-03" + }, + { + "belop": 20000.00, + "fordel": "kontantytelse", + "beskrivelse": "bil", + "inntektskilde": "A-ordningen", + "inntektsstatus": "LoependeInnrapportert", + "inntektsperiodetype": "Maaned", + "leveringstidspunkt": "2024-01", + "opptjeningsland": "SE", + "opptjeningsperiode": { + "startDato": "2024-01-01T00:00:00", + "sluttDato": "2024-01-31T00:00:00" + }, + "skattemessigBosattLand": "NO", + "utbetaltIMaaned": "2024-01", + "opplysningspliktig": null, + "inntektsinnsender": { + "aktoerType": "ORGANISASJON", + "identifikator": "987654321" + }, + "virksomhet": { + "aktoerType": "ORGANISASJON", + "identifikator": "987654321" + }, + "inntektsmottaker": { + "aktoerType": "AKTOER_ID", + "identifikator": "287631" + }, + "inngaarIGrunnlagForTrekk": true, + "utloeserArbeidsgiveravgift": true, + "informasjonsstatus": "InngaarAlltid", + "inntektType": "NAERINGSINNTEKT", + "tilleggsinformasjon": null, + "redigert": false, + "begrunnelse": "Ikke Standard", + "aarMaaned": "2024-01" + } + ], + "totalBeløp": 130000.00, + "avvikListe": [ + { + "ident": { + "aktoerType": "AKTOER_ID", + "identifikator": "287631" + }, + "opplysningspliktig": { + "aktoerType": "NATURLIG_IDENT", + "identifikator": "06221841830" + }, + "virksomhet": { + "aktoerType": "ORGANISASJON", + "identifikator": "987654321" + }, + "avvikPeriode": "2024-01", + "tekst": "Dette er et annet avvik" + } + ] + }, + { + "virksomhetsnummer": "550475189", + "virksomhetsnavn": "Virksomheten Test", + "periode": { + "fra": "2000-12", + "til": "2000-12" + }, + "inntekter": [], + "totalBeløp": 0, + "avvikListe": [ + { + "ident": { + "aktoerType": "AKTOER_ID", + "identifikator": "287632" + }, + "opplysningspliktig": { + "aktoerType": "NATURLIG_IDENT", + "identifikator": "25317924499" + }, + "virksomhet": { + "aktoerType": "ORGANISASJON", + "identifikator": "550475189" + }, + "avvikPeriode": "2000-12", + "tekst": "Dette er et Y2K-avvik" + } + ] + } + ], + "mottaker": { + "pnr": "19876543210", + "navn": "Navn Navnesen" + } +} \ No newline at end of file diff --git a/dp-inntekt-kontrakter/src/main/kotlin/no/nav/dagpenger/inntekt/v1/SpesifisertInntekt.kt b/dp-inntekt-kontrakter/src/main/kotlin/no/nav/dagpenger/inntekt/v1/SpesifisertInntekt.kt index 0658090d..3b2d81ca 100644 --- a/dp-inntekt-kontrakter/src/main/kotlin/no/nav/dagpenger/inntekt/v1/SpesifisertInntekt.kt +++ b/dp-inntekt-kontrakter/src/main/kotlin/no/nav/dagpenger/inntekt/v1/SpesifisertInntekt.kt @@ -46,7 +46,9 @@ data class Postering( val posteringsType: PosteringsType, ) -data class InntektId(val id: String) { +data class InntektId( + val id: String, +) { init { try { ULID.parseULID(id) @@ -72,4 +74,7 @@ enum class AktørType { ORGANISASJON, } -class IllegalInntektIdException(override val message: String, override val cause: Throwable?) : java.lang.RuntimeException(message, cause) +class IllegalInntektIdException( + override val message: String, + override val cause: Throwable?, +) : java.lang.RuntimeException(message, cause)