Skip to content

Commit

Permalink
Merge pull request #4596 from navikt/feature/krav-version
Browse files Browse the repository at this point in the history
Sjekk om innsendt krav stemmer før godkjennelse
  • Loading branch information
fredrikpe authored Nov 11, 2024
2 parents 60d4c19 + e1ed576 commit 18d0ee1
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArrangorflateService, ArrangorflateTilsagn } from "@mr/api-client";
import { ApiError, ArrangorflateService, ArrangorflateTilsagn } from "@mr/api-client";
import { Alert, Button, Checkbox, ErrorSummary, TextField, VStack } from "@navikt/ds-react";
import { ActionFunction, json, LoaderFunction, redirect } from "@remix-run/node";
import { Form, useActionData, useLoaderData } from "@remix-run/react";
Expand All @@ -11,6 +11,7 @@ import { loadRefusjonskrav } from "~/loaders/loadRefusjonskrav";
import { internalNavigation } from "../internal-navigation";
import { useOrgnrFromUrl } from "../utils";
import { getCurrentTab } from "../utils/currentTab";
import { isValidationError } from "@mr/frontend-common/utils/utils";

type BekreftRefusjonskravData = {
krav: Refusjonskrav;
Expand Down Expand Up @@ -45,37 +46,52 @@ export const action: ActionFunction = async ({ request }) => {
const orgnr = formdata.get("orgnr")?.toString();
const currentTab = getCurrentTab(request);

const errors: { [key: string]: string } = {};
const errors: { name: string; message: string }[] = [];

if (!bekreftelse) {
errors.bekreftelse = "Du må bekrefte at opplysningene er korrekte";
errors.push({ name: "bekreftelse", message: "Du må bekrefte at opplysningene er korrekte" });
}

if (!kontonummer) {
errors.kontonummer = "Du må fylle ut kontonummer";
errors.push({ name: "kontonummer", message: "Du må fylle ut kontonummer" });
}
if (!refusjonskravId) {
throw new Error("Mangler refusjonskravId");
}

if (!orgnr) {
throw new Error("Mangler orgnr");
}

if (Object.keys(errors).length > 0) {
if (errors.length > 0) {
return json({ errors });
}

await ArrangorflateService.godkjennRefusjonskrav({
id: refusjonskravId as string,
requestBody: {
kontonummer: kontonummer as string,
kid: kid as string,
},
});
return redirect(
`${internalNavigation(orgnr).kvittering(refusjonskravId)}?forside-tab=${currentTab}`,
);
const krav = await loadRefusjonskrav(refusjonskravId);

try {
await ArrangorflateService.godkjennRefusjonskrav({
id: refusjonskravId as string,
requestBody: {
belop: krav.beregning.belop,
deltakelser: krav.deltakere.map((d) => ({ deltakelseId: d.id, perioder: d.perioder })),
betalingsinformasjon: {
kontonummer: kontonummer as string,
kid: kid as string,
},
},
});

return redirect(
`${internalNavigation(orgnr).kvittering(refusjonskravId)}?forside-tab=${currentTab}`,
);
} catch (e) {
const apiError = e as ApiError;
if (apiError.status === 400 && isValidationError(apiError.body)) {
// Remix revaliderer loader data ved actions, så når denne feilmeldingen vises skal allerede kravet
// være oppdatert. Det kan hende vi i fremtiden vil vise _hva_ som har endret seg også, men det
// får vi ta senere.
return json({ errors: apiError.body.errors });
}
throw e;
}
};

export default function BekreftRefusjonskrav() {
Expand All @@ -93,7 +109,6 @@ export default function BekreftRefusjonskrav() {
/>
<VStack className="max-w-[50%]" gap="5">
<RefusjonskravDetaljer krav={krav} tilsagn={tilsagn} />

<Form method="post">
<Definisjon label="Kontonummer">
<TextField
Expand Down Expand Up @@ -127,15 +142,13 @@ export default function BekreftRefusjonskrav() {
</Checkbox>
<input type="hidden" name="refusjonskravId" value={krav.id} />
<input type="hidden" name="orgnr" value={orgnr} />
{data?.errors
? Object.keys(data?.errors)?.length > 0 && (
<ErrorSummary>
{Object.values(data?.errors).map((error, index) => {
return <ErrorSummary.Item key={index}>{error as any}</ErrorSummary.Item>;
})}
</ErrorSummary>
)
: null}
{data?.errors?.length > 0 && (
<ErrorSummary>
{data.errors.map((error: any) => {
return <ErrorSummary.Item key={error.name}>{error.message}</ErrorSummary.Item>;
})}
</ErrorSummary>
)}
{data?.error && <Alert variant="error">{data.error}</Alert>}
<Button type="submit">Bekreft og send refusjonskrav</Button>
</VStack>
Expand Down
10 changes: 9 additions & 1 deletion frontend/frontend-common/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TiltaksgjennomforingStatus, Tiltakskode, TiltakskodeArena } from "@mr/api-client";
import { TiltaksgjennomforingStatus, Tiltakskode, TiltakskodeArena, ValidationErrorResponse } from "@mr/api-client";
import { shallowEquals } from "./shallow-equals";

export function addOrRemove<T>(array: T[], item: T): T[] {
Expand Down Expand Up @@ -56,3 +56,11 @@ export function formaterKontoNummer(kontoNummer?: string): string {
? ""
: `${kontoNummer.substring(0, 4)} ${kontoNummer.substring(4, 6)} ${kontoNummer.substring(6, 11)}`;
}

export function isValidationError(body: unknown): body is ValidationErrorResponse {
return (
body !== null &&
typeof body === "object" &&
Object.prototype.hasOwnProperty.call(body, "errors")
);
}
9 changes: 1 addition & 8 deletions frontend/mr-admin-flate/src/api/effects.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { UseMutationResult } from "@tanstack/react-query";
import { useEffect } from "react";
import { ApiError, ValidationErrorResponse } from "@mr/api-client";
import { isValidationError } from "@mr/frontend-common/utils/utils";

export function useHandleApiUpsertResponse<Response, Request>(
mutation: UseMutationResult<Response, ApiError, Request>,
Expand All @@ -26,11 +27,3 @@ export function useHandleApiUpsertResponse<Response, Request>(
onValidationError,
]);
}

function isValidationError(body: unknown): body is ValidationErrorResponse {
return (
body !== null &&
typeof body === "object" &&
Object.prototype.hasOwnProperty.call(body, "errors")
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package no.nav.mulighetsrommet.api.okonomi.refusjon

import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import arrow.core.right
import arrow.core.toNonEmptySetOrNull
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
Expand All @@ -23,13 +26,18 @@ import no.nav.mulighetsrommet.api.domain.dto.ArrangorDto
import no.nav.mulighetsrommet.api.domain.dto.DeltakerDto
import no.nav.mulighetsrommet.api.okonomi.refusjon.db.RefusjonskravRepository
import no.nav.mulighetsrommet.api.okonomi.refusjon.model.DeltakelsePeriode
import no.nav.mulighetsrommet.api.okonomi.refusjon.model.DeltakelsePerioder
import no.nav.mulighetsrommet.api.okonomi.refusjon.model.RefusjonKravBeregning
import no.nav.mulighetsrommet.api.okonomi.refusjon.model.RefusjonKravBeregningAft
import no.nav.mulighetsrommet.api.okonomi.refusjon.model.RefusjonskravDto
import no.nav.mulighetsrommet.api.okonomi.refusjon.model.RefusjonskravStatus
import no.nav.mulighetsrommet.api.okonomi.tilsagn.TilsagnService
import no.nav.mulighetsrommet.api.okonomi.tilsagn.model.ArrangorflateTilsagn
import no.nav.mulighetsrommet.api.plugins.ArrangorflatePrincipal
import no.nav.mulighetsrommet.api.repositories.DeltakerRepository
import no.nav.mulighetsrommet.api.responses.BadRequest
import no.nav.mulighetsrommet.api.responses.ValidationError
import no.nav.mulighetsrommet.api.responses.respondWithStatusResponse
import no.nav.mulighetsrommet.api.services.ArrangorService
import no.nav.mulighetsrommet.domain.dto.Kid
import no.nav.mulighetsrommet.domain.dto.Kontonummer
Expand Down Expand Up @@ -131,21 +139,24 @@ fun Route.arrangorflateRoutes() {

post("/godkjenn-refusjon") {
val id = call.parameters.getOrFail<UUID>("id")
val request = call.receive<GodkjennRefusjonskravAft>()

val krav = refusjonskrav.get(id)
?: throw NotFoundException("Fant ikke refusjonskrav med id=$id")
requireTilgangHosArrangor(krav.arrangor.organisasjonsnummer)

val request = call.receive<SetRefusjonKravBetalingsinformasjonRequest>()

refusjonskrav.setGodkjentAvArrangor(id, LocalDateTime.now())
refusjonskrav.setBetalingsInformasjon(
id,
request.kontonummer,
request.kid,
)
val result = validerGodkjennRefusjonskrav(request, krav.beregning)
.mapLeft { BadRequest(errors = it) }
.map {
refusjonskrav.setGodkjentAvArrangor(id, LocalDateTime.now())
refusjonskrav.setBetalingsInformasjon(
id,
request.betalingsinformasjon.kontonummer,
request.betalingsinformasjon.kid,
)
}

call.respond(HttpStatusCode.OK)
call.respondWithStatusResponse(result)
}

get("/kvittering") {
Expand Down Expand Up @@ -209,6 +220,20 @@ fun Route.arrangorflateRoutes() {
}
}

fun validerGodkjennRefusjonskrav(
request: GodkjennRefusjonskravAft,
beregning: RefusjonKravBeregning,
): Either<List<ValidationError>, Unit> =
when (beregning) {
is RefusjonKravBeregningAft -> {
if (beregning.input.deltakelser != request.deltakelser || beregning.output.belop != request.belop) {
listOf(ValidationError.ofCustomLocation("info", "Informasjonen i kravet har endret seg. Vennligst se over på nytt.")).left()
} else {
Unit.right()
}
}
}

fun toRefusjonskravKompakt(krav: RefusjonskravDto) = RefusjonKravKompakt(
id = krav.id,
status = krav.status,
Expand Down Expand Up @@ -341,7 +366,6 @@ data class RefusjonKravKompakt(
val arrangor: RefusjonskravDto.Arrangor,
val beregning: Beregning,
) {

@Serializable
data class Beregning(
@Serializable(with = LocalDateTimeSerializer::class)
Expand Down Expand Up @@ -399,14 +423,22 @@ data class RefusjonKravDeltakelse(
)
}

@Serializable
data class SetRefusjonKravBetalingsinformasjonRequest(
val kontonummer: Kontonummer,
val kid: Kid?,
)

@Serializable
data class RefusjonKravKvitteringDto(
val refusjon: RefusjonKravAft,
val tilsagn: List<ArrangorflateTilsagn>,
)

// Kan bli gjort om til en sealed class for andre etterhvert hvis det trengs
@Serializable
data class GodkjennRefusjonskravAft(
val belop: Int,
val deltakelser: Set<DeltakelsePerioder>,
val betalingsinformasjon: Betalingsinformasjon,
) {
@Serializable
data class Betalingsinformasjon(
val kontonummer: Kontonummer,
val kid: Kid?,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ class RefusjonService(
private val deltakerRepository: DeltakerRepository,
private val refusjonskravRepository: RefusjonskravRepository,
) {

fun genererRefusjonskravForMonth(dayInMonth: LocalDate) {
val periodeStart = dayInMonth.with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay()
val periodeSlutt = periodeStart.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1)
Expand Down
29 changes: 28 additions & 1 deletion mulighetsrommet-api/src/main/resources/web/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1873,7 +1873,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Betalingsinformasjon"
$ref: "#/components/schemas/GodkjennRefusjonskravAft"
required: true
responses:
200:
Expand Down Expand Up @@ -5137,6 +5137,17 @@ components:
required:
- navn

RefusjonKravDeltakelsePerioder:
type: object
properties:
deltakelseId:
type: string
format: uuid
perioder:
type: array
items:
$ref: "#/components/schemas/RefusjonKravDeltakelsePeriode"

RefusjonKravDeltakelsePeriode:
type: object
properties:
Expand Down Expand Up @@ -5276,3 +5287,19 @@ components:
type: string
kid:
type: string

GodkjennRefusjonskravAft:
type: object
properties:
belop:
type: number
deltakelser:
type: array
items:
$ref: "#/components/schemas/RefusjonKravDeltakelsePerioder"
betalingsinformasjon:
$ref: "#/components/schemas/Betalingsinformasjon"
required:
- belop
- deltakelser
- betalingsinformasjon
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package no.nav.mulighetsrommet.api

import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.testing.*
import no.nav.mulighetsrommet.altinn.AltinnClient
import no.nav.mulighetsrommet.api.clients.brreg.BrregClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.ktor.client.engine.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import no.nav.mulighetsrommet.altinn.AltinnClient
import no.nav.mulighetsrommet.altinn.AltinnClient.AuthorizedParty
Expand Down Expand Up @@ -175,4 +177,29 @@ class ArrangorflateRoutesTest : FunSpec({
kravResponse.id shouldBe krav.id
}
}

test("Utdatert refusjonskrav gir 400 i godkjenn-refusjon") {
withTestApplication(appConfig()) {
val client = createClient {
install(ContentNegotiation) {
json()
}
}
val response = client.post("/api/v1/intern/arrangorflate/refusjonskrav/${krav.id}/godkjenn-refusjon") {
bearerAuth(oauth.issueToken(claims = mapOf("pid" to identMedTilgang.value)).serialize())
contentType(ContentType.Application.Json)
setBody(
GodkjennRefusjonskravAft(
belop = krav.beregning.output.belop + 200, // Feil belop
deltakelser = (krav.beregning as RefusjonKravBeregningAft).input.deltakelser,
betalingsinformasjon = GodkjennRefusjonskravAft.Betalingsinformasjon(
kontonummer = Kontonummer("12312312312"),
kid = null,
),
),
)
}
response.status shouldBe HttpStatusCode.BadRequest
}
}
})

0 comments on commit 18d0ee1

Please sign in to comment.