diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/domain/dto/BestillingProgressDTO.java b/apps/dolly-backend/src/main/java/no/nav/dolly/domain/dto/BestillingProgressDTO.java new file mode 100644 index 00000000000..e44ff12c3ca --- /dev/null +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/domain/dto/BestillingProgressDTO.java @@ -0,0 +1,58 @@ +package no.nav.dolly.domain.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import no.nav.dolly.domain.jpa.Testident.Master; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BestillingProgressDTO implements Serializable { + + private LocalDateTime sistOppdatert; + private LocalDate bestillingDato; + + private Long bestillingId; + + private String ident; + private Master master; + + private String aaregStatus; + private String arbeidsplassencvStatus; + private String arbeidssoekerregisteretStatus; + private String arenaforvalterStatus; + private String brregstubStatus; + private String dokarkivStatus; + private String etterlatteStatus; + private String feil; + private String fullmaktStatus; + private String histarkStatus; + private String inntektsmeldingStatus; + private String inntektsstubStatus; + private String instdataStatus; + private String kelvinAapStatus; + private String kontoregisterStatus; + private String krrstubStatus; + private String medlStatus; + private String nomStatus; + private String pdlForvalterStatus; + private String pdlImportStatus; + private String pdlOrdreStatus; + private String pdlPersonStatus; + private String pensjonforvalterStatus; + private String sigrunstubStatus; + private String skattekortStatus; + private String skjermingsregisterStatus; + private String sykemeldingStatus; + private String udistubStatus; + private String yrkesskadeStatus; +} diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/domain/dto/DashboardOversiktDTO.java b/apps/dolly-backend/src/main/java/no/nav/dolly/domain/dto/DashboardOversiktDTO.java new file mode 100644 index 00000000000..f400d10507b --- /dev/null +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/domain/dto/DashboardOversiktDTO.java @@ -0,0 +1,25 @@ +package no.nav.dolly.domain.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Month; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DashboardOversiktDTO { + + @JsonIgnore + private String aarManed; + + private Integer aar; + private Month maaned; + private Integer totaltAntallPersoner; + private Integer nye; + private Integer gjenopprettede; +} diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/domain/dto/DashboardPersonerDTO.java b/apps/dolly-backend/src/main/java/no/nav/dolly/domain/dto/DashboardPersonerDTO.java index 67696058884..150654b47f4 100644 --- a/apps/dolly-backend/src/main/java/no/nav/dolly/domain/dto/DashboardPersonerDTO.java +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/domain/dto/DashboardPersonerDTO.java @@ -17,6 +17,4 @@ public class DashboardPersonerDTO { private Long personerTotalt; private Long nye; private Long gjenopprettede; - private Long pdlFeil; - private Long andreFeil; } diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/domain/projection/BestillingerFragment.java b/apps/dolly-backend/src/main/java/no/nav/dolly/domain/projection/BestillingerFragment.java index d65efb6f674..96552648d0c 100644 --- a/apps/dolly-backend/src/main/java/no/nav/dolly/domain/projection/BestillingerFragment.java +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/domain/projection/BestillingerFragment.java @@ -16,6 +16,4 @@ public class BestillingerFragment { private LocalDate dato; private Long personer; private String gjenopprettstatus; - private String pdlstatus; - private String annenstatus; } diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/domain/projection/OversiktFragment.java b/apps/dolly-backend/src/main/java/no/nav/dolly/domain/projection/OversiktFragment.java new file mode 100644 index 00000000000..3da7e3eeb34 --- /dev/null +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/domain/projection/OversiktFragment.java @@ -0,0 +1,17 @@ +package no.nav.dolly.domain.projection; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OversiktFragment { + + private String maaned; + private Long antall; + private String gjenopprettstatus; +} diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/provider/DashboardController.java b/apps/dolly-backend/src/main/java/no/nav/dolly/provider/DashboardController.java index 79380472a02..b55f0a80f6f 100644 --- a/apps/dolly-backend/src/main/java/no/nav/dolly/provider/DashboardController.java +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/provider/DashboardController.java @@ -4,13 +4,18 @@ import lombok.RequiredArgsConstructor; import no.nav.dolly.domain.dto.DashboardDollyTeamsDTO; import no.nav.dolly.domain.dto.DashboardOrganisasjonerDTO; +import no.nav.dolly.domain.dto.DashboardOversiktDTO; import no.nav.dolly.domain.dto.DashboardPersonerDTO; import no.nav.dolly.domain.dto.DashboardTeamsDTO; import no.nav.dolly.service.DashboardService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; +import tools.jackson.databind.JsonNode; + +import java.time.Month; @RequestMapping("/api/v1/dashboard") @RestController @@ -26,6 +31,29 @@ public Flux getDashboardPersoner() { return dashboardService.getPersonerStatus(); } + @GetMapping(value = "/feil/detaljert") + @Operation(description = "Henter detaljert feilstatus for personer opprettet.") + public Flux getDashboardFeilDetaljert(@RequestParam int year, + @RequestParam Month month, + @RequestParam int day) { + + return dashboardService.getFeilstatusDetaljert(year, month, day); + } + + @GetMapping(value = "/feil/summert") + @Operation(description = "Henter summert feilstatus for personer opprettet.") + public Flux getDashboardFeil(@RequestParam int year, @RequestParam Month month) { + + return dashboardService.getFeilstatusSummert(year, month); + } + + @GetMapping(value = "/oversikt") + @Operation(description = "Henter tilgjengelige perioder.") + public Flux getDashboardFeilOversikt() { + + return dashboardService.getPerioderOversikt(); + } + @GetMapping(value = "/teams") @Operation(description = "Henter status per team i hht Teamkatalogen og antall unike personer som har bestilt. Gjelder AZURE-brukere.") public Flux getDashboardTeams() { diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/provider/advice/HttpExceptionAdvice.java b/apps/dolly-backend/src/main/java/no/nav/dolly/provider/advice/HttpExceptionAdvice.java index 16651b9651a..80d026f73fa 100644 --- a/apps/dolly-backend/src/main/java/no/nav/dolly/provider/advice/HttpExceptionAdvice.java +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/provider/advice/HttpExceptionAdvice.java @@ -30,8 +30,6 @@ @ControllerAdvice public class HttpExceptionAdvice extends DefaultErrorWebExceptionHandler { - private static final String GATEWAY_ORIGINAL_REQUEST_URL = "org.springframework.web.reactive.HandlerMapping.pathWithinHandlerMapping"; - public HttpExceptionAdvice(ErrorAttributes errorAttributes, WebProperties webProperties, ErrorProperties errorProperties, @@ -47,7 +45,7 @@ private ExceptionInformation informationForException(RuntimeException exception, .error(status.getReasonPhrase()) .status(status.value()) .message(exception.getMessage()) - .path(serverWebExchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL)) + .path(serverWebExchange.getRequest().getURI().getPath()) .timestamp(LocalDateTime.now()) .build(); log.warn("HttpException: {}", exceptionInfo); @@ -86,12 +84,12 @@ ExceptionInformation notFoundRequest(ServerWebExchange serverWebExchange, Runtim @ExceptionHandler(Exception.class) @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) ExceptionInformation internalServerError(ServerWebExchange serverWebExchange, Exception exception) { - log.error("Uventet feil ved request til {}", serverWebExchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL), exception); + log.error("Uventet feil ved request til {}", serverWebExchange.getRequest().getURI().getPath(), exception); return ExceptionInformation.builder() .error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()) .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) .message(exception.getMessage()) - .path(serverWebExchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL)) + .path(serverWebExchange.getRequest().getURI().getPath()) .timestamp(LocalDateTime.now()) .build(); } diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/repository/BestillingProgressRepository.java b/apps/dolly-backend/src/main/java/no/nav/dolly/repository/BestillingProgressRepository.java index 06218996c77..ecbe6a7de08 100644 --- a/apps/dolly-backend/src/main/java/no/nav/dolly/repository/BestillingProgressRepository.java +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/repository/BestillingProgressRepository.java @@ -52,4 +52,15 @@ where bp.bestilling_id in (select b.id @Query(value = "select * from bestilling_progress where id = :id for update") Mono findByIdAndLock(@Param("id") Long id); + + @Query(""" + SELECT DISTINCT column_name + FROM information_schema.columns + WHERE table_name = 'bestilling_progress' + AND column_name NOT LIKE 'tps_messaging_status' + AND (column_name LIKE '%status%' + OR column_name = 'feil') + ORDER BY column_name; + """) + Flux findStatusColumns(); } diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/repository/BestillingRepository.java b/apps/dolly-backend/src/main/java/no/nav/dolly/repository/BestillingRepository.java index 617f2fa8cd7..da91fe49c8b 100644 --- a/apps/dolly-backend/src/main/java/no/nav/dolly/repository/BestillingRepository.java +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/repository/BestillingRepository.java @@ -4,6 +4,7 @@ import no.nav.dolly.domain.projection.BestillingerFragment; import no.nav.dolly.domain.projection.DollyTeamFragment; import no.nav.dolly.domain.projection.OrganisasjonFragment; +import no.nav.dolly.domain.projection.OversiktFragment; import no.nav.dolly.domain.projection.RsBestillingFragment; import no.nav.dolly.domain.projection.TeamFragment; import org.springframework.data.domain.Pageable; @@ -144,33 +145,14 @@ select count(*) personer, when b.gjenopprettet_fra_ident is not null then 'GJENOPPRETTING' when b.opprett_fra_gruppe is not null then 'GJENOPPRETTING' else 'NYBESTILLING' - end as gjenopprettStatus, - case - when lower(bp.pdl_person_status) not like 'synkronisering%' then 'FEIL' - when lower(bp.pdl_forvalter_status) like '%feil%' then 'FEIL' - when lower(bp.pdl_ordre_status) like '%feil%' then 'FEIL' - else 'OK' - end as pdlStatus, - case - when lower(bp.pensjonforvalter_status) like '%feil%' then 'FEIL' - when lower(bp.aareg_status) like '%feil%' then 'FEIL' - when lower(bp.arenaforvalter_status) like '%feil%' then 'FEIL' - when lower(bp.instdata_status) like '%feil%' then 'FEIL' - when lower(bp.inntektsstub_status) like '%feil%' then 'FEIL' - when lower(bp.inntektsmelding_status) like '%feil%' then 'FEIL' - when lower(bp.sigrunstub_status) like '%feil%' then 'FEIL' - when lower(bp.dokarkiv_status) like '%feil%' then 'FEIL' - when lower(bp.feil) like '%feil%' then 'FEIL' - else 'OK' - end as annenStatus + end as gjenopprettStatus from bestilling b join bestilling_progress bp on b.id = bp.bestilling_id - group by dato, gjenopprettStatus, pdlStatus, annenStatus + group by dato, gjenopprettStatus order by dato desc """) Flux findBestillingerOrderBySistOppdatert(); - @Query(""" select count(*) antall, to_char(b.sist_oppdatert, 'YYYY-MM') interval, br.epost from bestilling b @@ -203,4 +185,20 @@ select count(*) antall, to_char(b.sist_oppdatert, 'YYYY-MM') interval, order by interval desc; """) Flux findBestillingerForDollyTeamsOrderBySistOppdatert(); + + @Query(""" + SELECT TO_CHAR(b.sist_oppdatert, 'YYYY-MM') maaned, + COUNT(*) antall, + CASE + when b.opprettet_fra_id is not null then 'GJENOPPRETTING' + when b.gjenopprettet_fra_ident is not null then 'GJENOPPRETTING' + when b.opprett_fra_gruppe is not null then 'GJENOPPRETTING' + else 'NYBESTILLING' + END gjenopprettStatus + FROM bestilling b + JOIN bestilling_progress bp ON b.id = bp.bestilling_id + GROUP BY maaned, gjenopprettStatus + ORDER BY maaned DESC; + """) + Flux findByAvailIntervals(); } \ No newline at end of file diff --git a/apps/dolly-backend/src/main/java/no/nav/dolly/service/DashboardService.java b/apps/dolly-backend/src/main/java/no/nav/dolly/service/DashboardService.java index 991fe9f23c3..aef18ff6462 100644 --- a/apps/dolly-backend/src/main/java/no/nav/dolly/service/DashboardService.java +++ b/apps/dolly-backend/src/main/java/no/nav/dolly/service/DashboardService.java @@ -8,8 +8,10 @@ import no.nav.dolly.consumer.brukerservice.dto.BrukerDTO; import no.nav.dolly.consumer.teamkatalog.TeamkatalogConsumer; import no.nav.dolly.consumer.teamkatalog.dto.TeamkatalogDTO; +import no.nav.dolly.domain.dto.BestillingProgressDTO; import no.nav.dolly.domain.dto.DashboardDollyTeamsDTO; import no.nav.dolly.domain.dto.DashboardOrganisasjonerDTO; +import no.nav.dolly.domain.dto.DashboardOversiktDTO; import no.nav.dolly.domain.dto.DashboardPersonerDTO; import no.nav.dolly.domain.dto.DashboardTeamsDTO; import no.nav.dolly.domain.jpa.Bruker; @@ -17,21 +19,33 @@ import no.nav.dolly.domain.projection.DollyTeam2Fragment; import no.nav.dolly.domain.projection.DollyTeamFragment; import no.nav.dolly.domain.projection.OrganisasjonFragment; +import no.nav.dolly.domain.projection.OversiktFragment; import no.nav.dolly.domain.projection.TeamFragment; +import no.nav.dolly.repository.BestillingProgressRepository; import no.nav.dolly.repository.BestillingRepository; import no.nav.dolly.repository.BrukerRepository; import no.nav.dolly.repository.TeamRepository; +import no.nav.testnav.libs.dto.pdlforvalter.v1.OrdreResponseDTO; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuples; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ObjectNode; +import java.time.Month; +import java.time.YearMonth; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.StringJoiner; import java.util.function.Function; import java.util.stream.Collectors; @@ -44,11 +58,15 @@ public class DashboardService { private static final String INGEN_TEAM = "Tilhører ikke noe team"; + private static final Set IDENTITETSFELT = Set.of("sistOppdatert", "bestillingId", "ident", "master"); private final Altinn3TilgangServiceConsumer altinn3TilgangServiceConsumer; private final BestillingRepository bestillingRepository; + private final BestillingProgressRepository bestillingProgressRepository; private final BrukerRepository brukerRepository; private final BrukerServiceConsumer brukerServiceConsumer; + private final R2dbcEntityTemplate entityTemplate; + private final JsonMapper jsonMapper; private final TeamkatalogConsumer teamkatalogConsumer; private final TeamRepository teamRepository; @@ -64,8 +82,6 @@ public Flux getPersonerStatus() { .mapToLong(BestillingerFragment::getPersoner).sum()) .nye(sumByStatus(fragmentliste, BestillingerFragment::getGjenopprettstatus, "NYBESTILLING")) .gjenopprettede(sumByStatus(fragmentliste, BestillingerFragment::getGjenopprettstatus, "GJENOPPRETTING")) - .pdlFeil(sumByStatus(fragmentliste, BestillingerFragment::getPdlstatus, "FEIL")) - .andreFeil(sumByStatus(fragmentliste, BestillingerFragment::getAnnenstatus, "FEIL")) .build()) .sort(Comparator.comparing(DashboardPersonerDTO::getDato).reversed()); } @@ -82,8 +98,7 @@ public Flux getTeamsStatus() { .flatMapMany(teams -> bestillingRepository.findBestillingerForTeamsOrderBySistOppdatert() .groupBy(TeamFragment::getInterval) .flatMap(Flux::collectList) - .flatMap(fragments -> Mono.just( - Tuples.of(groupFragmentsByTeam(fragments, teams), fragments.getFirst().getInterval())))) + .map(fragments -> Tuples.of(groupFragmentsByTeam(fragments, teams), fragments.getFirst().getInterval()))) .map(tuple -> DashboardTeamsDTO.builder() .interval(tuple.getT2()) @@ -109,8 +124,7 @@ public Flux getOrganisasjonerStatus() { .flatMapMany(oppslag -> bestillingRepository.findBestillingerForOrganisasjonerOrderBySistOppdatert() .groupBy(OrganisasjonFragment::getInterval) .flatMap(Flux::collectList) - .flatMap(fragments -> Mono.just( - Tuples.of(groupFragmentsByOrganisasjon(fragments, oppslag.getT2()), fragments.getFirst().getInterval()))) + .map(fragments -> Tuples.of(groupFragmentsByOrganisasjon(fragments, oppslag.getT2()), fragments.getFirst().getInterval())) .map(tuple -> DashboardOrganisasjonerDTO.builder() .interval(tuple.getT2()) .organisasjoner(tuple.getT1().entrySet().stream() @@ -150,9 +164,78 @@ public Flux getDollyTeamsStatus() { .sort(Comparator.comparing(DashboardDollyTeamsDTO::getInterval).reversed()); } + public Flux getFeilstatusSummert(int year, Month month) { + + var filter = "%4d-%02d".formatted(year, month.getValue()); + return buildFeilWhereFragment() + .map(feilFilter -> "select b.sist_oppdatert::date bestilling_dato, bp.* " + + "from bestilling b " + + "join bestilling_progress bp on bp.bestilling_id = b.id " + + "where to_char(b.sist_oppdatert, 'YYYY-MM') = :range " + + "and (" + feilFilter + ") order by bestilling_dato") + .flatMapMany(query -> entityTemplate.getDatabaseClient() + .sql(query) + .bind("range", filter) + .map((row, metadata) -> entityTemplate.getConverter() + .read(BestillingProgressDTO.class, row, metadata)) + .all()) + .groupBy(BestillingProgressDTO::getBestillingDato) + .concatMap(Flux::collectList) + .concatMap(this::tilFeilstatusSummert); + } + + public Flux getFeilstatusDetaljert(int year, Month month, int day) { + + var filter = "%4d-%02d-%02d".formatted(year, month.getValue(), day); + return buildFeilWhereFragment() + .map(feilFilter -> "select b.sist_oppdatert, bp.* " + + "from bestilling b " + + "join bestilling_progress bp on bp.bestilling_id = b.id " + + "where to_char(b.sist_oppdatert, 'YYYY-MM-DD') = :range " + + "and (" + feilFilter + ") order by b.sist_oppdatert") + .flatMapMany(query -> entityTemplate.getDatabaseClient() + .sql(query) + .bind("range", filter) + .map((row, metadata) -> entityTemplate.getConverter() + .read(BestillingProgressDTO.class, row, metadata)) + .all()) + .flatMap(this::tilFeilJson); + } + + public Flux getPerioderOversikt() { + + return bestillingRepository.findByAvailIntervals() + .groupBy(OversiktFragment::getMaaned) + .flatMap(Flux::collectList) + .map(fragmenter -> { + var aarManed = fragmenter.getFirst().getMaaned(); + var yearMonth = YearMonth.parse(aarManed); + return DashboardOversiktDTO.builder() + .aarManed(aarManed) + .aar(yearMonth.getYear()) + .maaned(yearMonth.getMonth()) + .totaltAntallPersoner(fragmenter.stream() + .map(OversiktFragment::getAntall) + .mapToInt(Long::intValue) + .sum()) + .nye(fragmenter.stream() + .filter(fragment -> "NYBESTILLING".equals(fragment.getGjenopprettstatus())) + .map(OversiktFragment::getAntall) + .mapToInt(Long::intValue) + .sum()) + .gjenopprettede(fragmenter.stream() + .filter(fragment -> "GJENOPPRETTING".equals(fragment.getGjenopprettstatus())) + .map(OversiktFragment::getAntall) + .mapToInt(Long::intValue) + .sum()) + .build(); + }) + .sort(Comparator.comparing(DashboardOversiktDTO::getAarManed).reversed()); + } + private static long sumByStatus(List fragments, - Function statusGetter, - String value) { + Function statusGetter, + Object value) { return fragments.stream() .filter(f -> value.equals(statusGetter.apply(f))) @@ -178,7 +261,9 @@ private static Map> groupFragmentsByOrganisasjon(List>(); fragments.forEach(fragment -> { var orgNummer = brukerToOrgnummer.get(fragment.getBrukerid()); - grouped.computeIfAbsent(orgNummer, _ -> new HashSet<>()).add(fragment.getBrukerid()); + if (Objects.nonNull(orgNummer)) { + grouped.computeIfAbsent(orgNummer, _ -> new HashSet<>()).add(fragment.getBrukerid()); + } }); return grouped; } @@ -195,9 +280,118 @@ private static DashboardOrganisasjonerDTO.Entry toOrganisasjonEntry(String orgNu } private static DashboardDollyTeamsDTO.Entry toDollyTeamEntry(DollyTeamFragment fragment, - Map oppslag) { + Map oppslag) { var info = fragment.getInformasjon().split("\\|"); return new DashboardDollyTeamsDTO.Entry(info[0], info[1], toIntExact(oppslag.get(info[2]))); } + + private Mono buildFeilWhereFragment() { + return bestillingProgressRepository.findStatusColumns() + .reduce(new StringJoiner(" or "), (joiner, column) -> + joiner.add("lower(bp." + column + ") like '%feil%'")) + .map(StringJoiner::toString) + .map(s -> s.isBlank() ? "false" : s); + } + + private Mono tilFeilstatusSummert(List bestillingProgressDTOS) { + + Map resultat = new HashMap<>(); + bestillingProgressDTOS.forEach(progress -> { + var kilde = (ObjectNode) jsonMapper.valueToTree(progress); + + kilde.propertyNames().forEach(navn -> { + var verdi = kilde.get(navn); + if (!IDENTITETSFELT.contains(navn)) { + if ("bestillingDato".equals(navn)) { + resultat.putIfAbsent(navn, verdi); + } else if (verdi.isString() && verdi.asString().toLowerCase().contains("feil")) { + resultat.merge(navn, 1, (existing, one) -> (Integer) existing + (Integer) one); + } + } + }); + }); + var summert = resultat.entrySet().stream() + .collect(Collectors.toMap(entry -> { + var key = entry.getKey(); + if (key.contains("Status")) { + return key.replace("Status", "Feil"); + } else if ("feil".equals(key)) { + return "andreFeil"; + } else { + return key; + } + }, + Map.Entry::getValue)); + return summert.isEmpty() ? Mono.empty() : Mono.just(jsonMapper.valueToTree(summert)); + } + + private Mono tilFeilJson(BestillingProgressDTO progress) { + + var kilde = (ObjectNode) jsonMapper.valueToTree(progress); + var resultat = jsonMapper.createObjectNode(); + + kilde.propertyNames().forEach(navn -> { + var verdi = kilde.get(navn); + if (IDENTITETSFELT.contains(navn)) { + resultat.set(navn, verdi); + } else if (verdi.isString() && verdi.asString().toLowerCase().contains("feil")) { + resultat.set(navn, tilJsonEllerTekst(verdi, navn)); + } + }); + return Mono.just(resultat); + } + + private JsonNode tilJsonEllerTekst(JsonNode json, String navn) { + + var verdi = json.asString().trim(); + if (verdi.startsWith("{") || verdi.startsWith("[")) { + try { + if (!"pdlOrdreStatus".equals(navn)) { + return jsonMapper.readTree(verdi); + } else { + var ordreRespons = jsonMapper.readValue(verdi, OrdreResponseDTO.class); + var feilRespons = getFeil(ordreRespons); + var responsTekst = jsonMapper.writeValueAsString(feilRespons); + return jsonMapper.readTree(responsTekst); + } + } catch (JacksonException e) { + log.debug("Statusfelt er ikke gyldig JSON – beholdes som tekst", e); + } + } + return json; + } + + private static OrdreResponseDTO getFeil(OrdreResponseDTO response) { + + return OrdreResponseDTO.builder() + .hovedperson(getError(response.getHovedperson())) + .relasjoner(response.getRelasjoner().stream() + .map(DashboardService::getError) + .toList()) + .build(); + } + + private static OrdreResponseDTO.PersonHendelserDTO getError(OrdreResponseDTO.PersonHendelserDTO hendelser) { + + return OrdreResponseDTO.PersonHendelserDTO.builder() + .ident(hendelser.getIdent()) + .ordrer(hendelser.getOrdrer().stream() + .filter(status -> status.getHendelser().stream() + .anyMatch(hendelse -> isNotBlank(hendelse.getError()))) + .map(status -> OrdreResponseDTO.PdlStatusDTO.builder() + .ident(status.getIdent()) + .infoElement(status.getInfoElement()) + .hendelser(status.getHendelser().stream() + .filter(hendelse -> isNotBlank(hendelse.getError())) + .map(hendelse -> OrdreResponseDTO.HendelseDTO.builder() + .id(hendelse.getId()) + .status(hendelse.getStatus()) + .error(hendelse.getError()) + .build()) + .toList()) + .build()) + .toList()) + .build(); + } } diff --git a/apps/dolly-backend/src/test/java/no/nav/dolly/service/DashboardServiceTest.java b/apps/dolly-backend/src/test/java/no/nav/dolly/service/DashboardServiceTest.java index e8d9749ddb6..4fed55d939b 100644 --- a/apps/dolly-backend/src/test/java/no/nav/dolly/service/DashboardServiceTest.java +++ b/apps/dolly-backend/src/test/java/no/nav/dolly/service/DashboardServiceTest.java @@ -14,6 +14,7 @@ import no.nav.dolly.domain.projection.DollyTeam2Fragment; import no.nav.dolly.domain.projection.DollyTeamFragment; import no.nav.dolly.domain.projection.OrganisasjonFragment; +import no.nav.dolly.domain.projection.OversiktFragment; import no.nav.dolly.domain.projection.TeamFragment; import no.nav.dolly.repository.BestillingRepository; import no.nav.dolly.repository.BrukerRepository; @@ -27,6 +28,7 @@ import reactor.test.StepVerifier; import java.time.LocalDate; +import java.time.Month; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -74,8 +76,8 @@ void shouldReturnEmptyWhenNoFragments() { @Test void shouldSumPersonerTotaltForSingleDate() { - var f1 = fragment(DATE_1, 5L, "NYBESTILLING", "OK", "OK"); - var f2 = fragment(DATE_1, 3L, "NYBESTILLING", "OK", "OK"); + var f1 = fragment(DATE_1, 5L, "NYBESTILLING"); + var f2 = fragment(DATE_1, 3L, "NYBESTILLING"); when(bestillingRepository.findBestillingerOrderBySistOppdatert()).thenReturn(Flux.just(f1, f2)); StepVerifier.create(dashboardService.getPersonerStatus()) @@ -88,8 +90,8 @@ void shouldSumPersonerTotaltForSingleDate() { @Test void shouldCountNyeByGjenopprettstatusNYBESTILLING() { - var nybestilling = fragment(DATE_1, 4L, "NYBESTILLING", "OK", "OK"); - var gjenoppretting = fragment(DATE_1, 2L, "GJENOPPRETTING", "OK", "OK"); + var nybestilling = fragment(DATE_1, 4L, "NYBESTILLING"); + var gjenoppretting = fragment(DATE_1, 2L, "GJENOPPRETTING"); when(bestillingRepository.findBestillingerOrderBySistOppdatert()).thenReturn(Flux.just(nybestilling, gjenoppretting)); StepVerifier.create(dashboardService.getPersonerStatus()) @@ -100,31 +102,10 @@ void shouldCountNyeByGjenopprettstatusNYBESTILLING() { .verifyComplete(); } - @Test - void shouldCountPdlFeilByPdlstatusFEIL() { - var feil = fragment(DATE_1, 3L, "NYBESTILLING", "FEIL", "OK"); - var ok = fragment(DATE_1, 2L, "NYBESTILLING", "OK", "OK"); - when(bestillingRepository.findBestillingerOrderBySistOppdatert()).thenReturn(Flux.just(feil, ok)); - - StepVerifier.create(dashboardService.getPersonerStatus()) - .assertNext(dto -> assertThat(dto.getPdlFeil()).isEqualTo(3L)) - .verifyComplete(); - } - - @Test - void shouldCountAndreFeilByAnnenstatusFEIL() { - var feil = fragment(DATE_1, 7L, "NYBESTILLING", "OK", "FEIL"); - when(bestillingRepository.findBestillingerOrderBySistOppdatert()).thenReturn(Flux.just(feil)); - - StepVerifier.create(dashboardService.getPersonerStatus()) - .assertNext(dto -> assertThat(dto.getAndreFeil()).isEqualTo(7L)) - .verifyComplete(); - } - @Test void shouldGroupFragmentsByDateIntoSeparateDtos() { - var d1 = fragment(DATE_1, 10L, "NYBESTILLING", "OK", "OK"); - var d2 = fragment(DATE_2, 5L, "NYBESTILLING", "OK", "OK"); + var d1 = fragment(DATE_1, 10L, "NYBESTILLING"); + var d2 = fragment(DATE_2, 5L, "NYBESTILLING"); when(bestillingRepository.findBestillingerOrderBySistOppdatert()).thenReturn(Flux.just(d1, d2)); StepVerifier.create(dashboardService.getPersonerStatus()) @@ -135,8 +116,8 @@ void shouldGroupFragmentsByDateIntoSeparateDtos() { @Test void shouldSortPersonerStatusByDateDescending() { - var d1 = fragment(DATE_1, 1L, "NYBESTILLING", "OK", "OK"); - var d2 = fragment(DATE_2, 1L, "NYBESTILLING", "OK", "OK"); + var d1 = fragment(DATE_1, 1L, "NYBESTILLING"); + var d2 = fragment(DATE_2, 1L, "NYBESTILLING"); when(bestillingRepository.findBestillingerOrderBySistOppdatert()).thenReturn(Flux.just(d1, d2)); StepVerifier.create(dashboardService.getPersonerStatus()) @@ -147,15 +128,13 @@ void shouldSortPersonerStatusByDateDescending() { @Test void shouldProduceZeroCountsWhenNoMatchingStatuses() { - var f = fragment(DATE_1, 5L, "UKJENT", "UKJENT", "UKJENT"); + var f = fragment(DATE_1, 5L, "UKJENT"); when(bestillingRepository.findBestillingerOrderBySistOppdatert()).thenReturn(Flux.just(f)); StepVerifier.create(dashboardService.getPersonerStatus()) .assertNext(dto -> { assertThat(dto.getNye()).isZero(); assertThat(dto.getGjenopprettede()).isZero(); - assertThat(dto.getPdlFeil()).isZero(); - assertThat(dto.getAndreFeil()).isZero(); }) .verifyComplete(); } @@ -425,6 +404,26 @@ void shouldSortOrganisasjonerByNavnAlphabetically() { .verifyComplete(); } + @Test + void shouldSkipOrganisasjonFragmentsWithNoBrukerserviceMatch() { + when(altinn3TilgangServiceConsumer.getOrganisasjoner()).thenReturn(Flux.empty()); + when(brukerServiceConsumer.getAlleBrukere()).thenReturn(Flux.just( + BrukerDTO.builder().id("kjent").organisasjonsnummer("123456789").build() + )); + when(bestillingRepository.findBestillingerForOrganisasjonerOrderBySistOppdatert()) + .thenReturn(Flux.just( + organisasjonFragment(INTERVAL_1, "kjent"), + organisasjonFragment(INTERVAL_1, "ukjent") + )); + + StepVerifier.create(dashboardService.getOrganisasjonerStatus()) + .assertNext(dto -> { + assertThat(dto.getOrganisasjoner()).hasSize(1); + assertThat(dto.getTotaltAntallOrganisasjoner()).isEqualTo(1); + }) + .verifyComplete(); + } + // ── getDollyTeamsStatus ────────────────────────────────────────────────── @Test @@ -528,18 +527,80 @@ void shouldSortDollyTeamsStatusByIntervalDescending() { assertThat(results.get(1).getInterval()).isEqualTo(INTERVAL_1); } + // ── getPerioderOversikt ────────────────────────────────────────────────── + + @Test + void shouldReturnEmptyPerioderWhenNoIntervals() { + when(bestillingRepository.findByAvailIntervals()).thenReturn(Flux.empty()); + + StepVerifier.create(dashboardService.getPerioderOversikt()) + .verifyComplete(); + } + + @Test + void shouldParseAarManedIntoAarAndMaaned() { + when(bestillingRepository.findByAvailIntervals()).thenReturn(Flux.just( + oversiktFragment("2024-03", 10L, "NYBESTILLING") + )); + + StepVerifier.create(dashboardService.getPerioderOversikt()) + .assertNext(dto -> { + assertThat(dto.getAarManed()).isEqualTo("2024-03"); + assertThat(dto.getAar()).isEqualTo(2024); + assertThat(dto.getMaaned()).isEqualTo(Month.MARCH); + }) + .verifyComplete(); + } + + @Test + void shouldSumTotaltAntallPersonerForSamePeriod() { + when(bestillingRepository.findByAvailIntervals()).thenReturn(Flux.just( + oversiktFragment("2024-01", 5L, "NYBESTILLING"), + oversiktFragment("2024-01", 3L, "GJENOPPRETTING") + )); + + StepVerifier.create(dashboardService.getPerioderOversikt()) + .assertNext(dto -> assertThat(dto.getTotaltAntallPersoner()).isEqualTo(8)) + .verifyComplete(); + } + + @Test + void shouldSumNyeAndGjenopprettedeByGjenopprettstatus() { + when(bestillingRepository.findByAvailIntervals()).thenReturn(Flux.just( + oversiktFragment("2024-01", 4L, "NYBESTILLING"), + oversiktFragment("2024-01", 2L, "GJENOPPRETTING"), + oversiktFragment("2024-01", 1L, "UKJENT") + )); + + StepVerifier.create(dashboardService.getPerioderOversikt()) + .assertNext(dto -> { + assertThat(dto.getNye()).isEqualTo(4); + assertThat(dto.getGjenopprettede()).isEqualTo(2); + }) + .verifyComplete(); + } + + @Test + void shouldSortPerioderOversiktByAarManedDescending() { + when(bestillingRepository.findByAvailIntervals()).thenReturn(Flux.just( + oversiktFragment("2024-01", 1L, "NYBESTILLING"), + oversiktFragment("2024-02", 1L, "NYBESTILLING") + )); + + StepVerifier.create(dashboardService.getPerioderOversikt()) + .assertNext(dto -> assertThat(dto.getAarManed()).isEqualTo("2024-02")) + .assertNext(dto -> assertThat(dto.getAarManed()).isEqualTo("2024-01")) + .verifyComplete(); + } + // ── helpers ────────────────────────────────────────────────────────────── private static BestillingerFragment fragment(LocalDate dato, Long personer, - String gjenopprettstatus, - String pdlstatus, - String annenstatus) { + String gjenopprettstatus) { return BestillingerFragment.builder() .dato(dato) .personer(personer) .gjenopprettstatus(gjenopprettstatus) - .pdlstatus(pdlstatus) - .annenstatus(annenstatus) .build(); } @@ -563,4 +624,12 @@ private static DollyTeamFragment dollyTeamFragment(String interval, String navn, .informasjon(navn + "|" + beskrivelse + "|" + brukerid) .build(); } + + private static OversiktFragment oversiktFragment(String maaned, Long antall, String gjenopprettstatus) { + return OversiktFragment.builder() + .maaned(maaned) + .antall(antall) + .gjenopprettstatus(gjenopprettstatus) + .build(); + } }