Skip to content

Commit 1d0d9f7

Browse files
committed
chore: improved error handling
1 parent 6d839f3 commit 1d0d9f7

File tree

9 files changed

+136
-91
lines changed

9 files changed

+136
-91
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import { logger } from '@navikt/next-logger'
22

33
import { ensureValidFhirAuth } from '@fhir/auth/verify'
4-
import {
5-
FhirDocumentReferenceResponse,
6-
FhirDocumentReferenceResponseSchema,
7-
} from '@fhir/fhir-data/schema/documentReference'
4+
import { FhirDocumentReferenceBaseSchema } from '@fhir/fhir-data/schema/documentReference'
85
import { sykInnApiService } from '@services/syk-inn-api/SykInnApiService'
96
import { serverFhirResources } from '@fhir/fhir-data/fhir-data-server'
107

11-
type DocRefResponseResult = FhirDocumentReferenceResponse | [{ errors: { message: string } }]
8+
import { WriteToEhrResult } from '../../../../../../data-fetcher/data-service'
129

13-
export async function POST(request: Request): Promise<Response | DocRefResponseResult> {
10+
type WriteToEhrResponse = WriteToEhrResult | { errors: [{ message: string }] }
11+
12+
export async function POST(request: Request): Promise<Response> {
1413
const authStatus = await ensureValidFhirAuth()
1514
if (authStatus !== 'ok') {
1615
return authStatus
@@ -33,52 +32,47 @@ export async function POST(request: Request): Promise<Response | DocRefResponseR
3332
logger.error(`Failed to retrieve sykmelding pdf ${sykmeldingPdfArrayBuffer.errorType}`)
3433
return new Response('Failed to retrieve sykmelding pdf', { status: 500 })
3534
}
36-
3735
const sykmeldingBase64 = Buffer.from(sykmeldingPdfArrayBuffer).toString('base64')
3836

39-
// check if document reference already exists
4037
const existingDocumentReference = await serverFhirResources.getDocumentReference(sykmeldingId)
38+
if ('outcome' in existingDocumentReference) {
39+
return Response.json(existingDocumentReference satisfies WriteToEhrResult)
40+
}
4141

42-
const verifiedExistingDocRef = FhirDocumentReferenceResponseSchema.safeParse(existingDocumentReference)
43-
if (!verifiedExistingDocRef.success) {
44-
logger.error(`Invalid existing DocumentReference: ${JSON.stringify(verifiedExistingDocRef.error, null, 2)}`)
45-
42+
if ('errorType' in existingDocumentReference && existingDocumentReference.errorType === 'INVALID_FHIR_RESPONSE') {
43+
logger.error(new Error(existingDocumentReference.errorType, { cause: existingDocumentReference.zodErrors }))
4644
return Response.json(
47-
[{ errors: { message: 'Invalid existing DocumentReference' } }] satisfies DocRefResponseResult,
45+
{ errors: [{ message: existingDocumentReference.errorType }] } satisfies WriteToEhrResponse,
4846
{
4947
status: 400,
5048
},
5149
)
5250
}
5351

54-
if (existingDocumentReference) {
55-
return Response.json(verifiedExistingDocRef.data satisfies DocRefResponseResult)
56-
}
57-
58-
// create document reference as it didn't exist
59-
const createdDocRef = await serverFhirResources.createDocumentReference(sykmeldingBase64, 'Sykmelding for Foo Bar') // TODO title
52+
// Doesn't already exist, and hasn't returned invalid FHIR response, we can try and create it
53+
const createdDocRef = await serverFhirResources.createDocumentReference(
54+
sykmeldingBase64,
55+
'Sykmelding for Foo Bar',
56+
sykmeldingId,
57+
) // TODO title
6058
if ('errorType' in createdDocRef) {
61-
logger.error(`Failed to create DocumentReference: ${JSON.stringify(createdDocRef, null, 2)}`)
62-
63-
return Response.json(
64-
[{ errors: { message: 'Failed to create DocumentReference' } }] satisfies DocRefResponseResult,
65-
{
66-
status: 500,
67-
},
68-
)
59+
logger.error(new Error(createdDocRef.errorType, { cause: createdDocRef.zodErrors }))
60+
return Response.json({ errors: [{ message: createdDocRef.errorType }] } satisfies WriteToEhrResponse, {
61+
status: 500,
62+
})
6963
}
7064

71-
const verifiedCreatedDocRef = FhirDocumentReferenceResponseSchema.safeParse(createdDocRef)
65+
const verifiedCreatedDocRef = FhirDocumentReferenceBaseSchema.safeParse(createdDocRef)
7266
if (!verifiedCreatedDocRef.success) {
7367
logger.error(`Invalid created DocumentReference: ${JSON.stringify(verifiedCreatedDocRef.error, null, 2)}`)
7468

75-
return Response.json(
76-
[{ errors: { message: 'Invalid created DocumentReference' } }] satisfies DocRefResponseResult,
77-
{
78-
status: 400,
79-
},
80-
)
69+
return Response.json({ errors: [verifiedCreatedDocRef.error] } satisfies WriteToEhrResponse, {
70+
status: 400,
71+
})
8172
}
8273

83-
return Response.json(verifiedCreatedDocRef.data satisfies DocRefResponseResult)
74+
return Response.json({
75+
outcome: 'NEWLY_CREATED',
76+
documentReference: verifiedCreatedDocRef.data,
77+
} satisfies WriteToEhrResult)
8478
}

src/app/api/mocks/fhir/(resources)/DocumentReference/[id]/route.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import { logger as pinoLogger } from '@navikt/next-logger'
22

3-
import { cleanPath } from '../../utils'
4-
53
const logger = pinoLogger.child({}, { msgPrefix: '[FHIR-MOCK] ' })
64

7-
export async function GET(req: Request): Promise<Response> {
8-
const url = new URL(req.url)
9-
const fhirPath = cleanPath(url.pathname)
10-
logger.info(`Incoming request: ${req.method} - ${fhirPath}`)
5+
const shouldReturn = false
6+
7+
type RouteParams = {
8+
params: Promise<{ id: string }>
9+
}
10+
11+
export async function GET(_: Request, { params }: RouteParams): Promise<Response> {
12+
const docRefId = (await params).id
13+
14+
logger.info(`Incoming request: GET DocumentReference/${docRefId}`)
1115

1216
// verifyAuthed(req)
17+
if (!shouldReturn) {
18+
logger.info("Pretending document reference doesn't exist")
19+
return new Response('Not found', { status: 404 })
20+
}
1321

1422
return mockedDocumentReference()
1523
}

src/app/api/mocks/fhir/(resources)/DocumentReference/route.ts

-4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ export async function POST(req: Request): Promise<Response> {
1212

1313
verifyAuthed(req)
1414

15-
if (req.method !== 'POST') {
16-
return new Response('DocumentReference without a specific id parameter only supports POST')
17-
}
18-
1915
return mockedDocumentReference()
2016
}
2117

src/components/existing-sykmelding-kvittering/ExistingSykmeldingKvittering.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function WritebackStatus({ sykmeldingId }: WritebackStatusProps): ReactElement {
9898
)
9999
}
100100

101-
if (!data || !data.id) {
101+
if (!data || !data.documentReference.id) {
102102
return (
103103
<div className="mt-4">
104104
<Alert variant="warning">Data er ikke tilgjengelig ennå. Vennligst prøv igjen senere.</Alert>
@@ -114,7 +114,7 @@ function WritebackStatus({ sykmeldingId }: WritebackStatusProps): ReactElement {
114114
return (
115115
<div className="my-4">
116116
<Heading size="small" level="3">
117-
Sykmelding er skrevet til EPJ-systemet og lagret på DokumentReferanse: ${data.id}
117+
Sykmelding er lagret i EPJ-systemet på DokumentReferanse: ${data.documentReference.id}
118118
</Heading>
119119
</div>
120120
)

src/data-fetcher/data-service.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { SubmitSykmeldingFormValues } from '@services/syk-inn-api/SykInnApiSchema'
2+
import { FhirDocumentReferenceBase } from '@fhir/fhir-data/schema/documentReference'
23

34
export type NotAvailable = typeof NotAvailable
45
export const NotAvailable = {
@@ -36,7 +37,7 @@ export type DataService = {
3637
}
3738
mutation: {
3839
sendSykmelding: (sykmelding: SubmitSykmeldingFormValues) => Promise<NySykmelding>
39-
writeToEhr: (sykmeldingId: string) => Promise<DocumentReferenceResponse>
40+
writeToEhr: (sykmeldingId: string) => Promise<WriteToEhrResult>
4041
}
4142
}
4243

@@ -126,14 +127,11 @@ export type ExistingSykmelding = {
126127
}
127128
}
128129

129-
export type DocumentReferenceResponse = {
130-
resourceType: string
131-
id: string
132-
meta: {
133-
versionId: string
134-
lastUpdated: string
135-
}
130+
export type WriteToEhrResult = {
131+
outcome: 'NEWLY_CREATED' | 'ALREADY_EXISTS'
132+
documentReference: FhirDocumentReferenceBase
136133
}
134+
137135
/**
138136
* Type guard to check if a resource (i.e. a data service function) is available or not. Used to conditionally render
139137
* the form based on whether the data is configured to be available.

src/fhir/fhir-data/fhir-data-server.ts

+69-29
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
11
import { decodeJwt } from 'jose'
22
import { logger } from '@navikt/next-logger'
3+
import { ZodIssue } from 'zod'
34

45
import { getSession } from '@fhir/auth/session'
5-
import { FhirDocumentReference, FhirDocumentReferenceResponseSchema } from '@fhir/fhir-data/schema/documentReference'
6+
import { FhirDocumentReference, FhirDocumentReferenceBaseSchema } from '@fhir/fhir-data/schema/documentReference'
67

78
import { FhirPractitionerSchema } from '../fhir-data/schema/practitioner'
89
import { getHpr } from '../fhir-data/schema/mappers/practitioner'
910
import { getName } from '../fhir-data/schema/mappers/patient'
10-
import { BehandlerInfo, DocumentReferenceResponse } from '../../data-fetcher/data-service'
11+
import { BehandlerInfo, WriteToEhrResult } from '../../data-fetcher/data-service'
1112

1213
/**
1314
* These FHIR resources are only available in the server runtime. They are not proxied through the backend.
1415
* They will use the session store and fetch resources directly from the FHIR server.
1516
*/
1617
export const serverFhirResources = {
17-
createDocumentReference: async (pdf: string, title: string): Promise<DocumentReferenceResponse> => {
18+
createDocumentReference: async (
19+
pdf: string,
20+
title: string,
21+
sykmeldingId: string,
22+
): Promise<
23+
| WriteToEhrResult
24+
| { errorType: 'INVALID_FHIR_RESPONSE'; zodErrors: ZodIssue[] }
25+
| { errorType: 'INVALID_PRACTITIONER_ID'; zodErrors: ZodIssue[] }
26+
| { errorType: 'INACTIVE_USER_SESSION'; zodErrors: ZodIssue[] }
27+
> => {
1828
const currentSession = await getSession()
1929
if (currentSession == null) {
20-
throw new Error('Active session is required')
30+
return { errorType: 'INACTIVE_USER_SESSION', zodErrors: [] }
2131
}
2232

2333
const decodedIdToken = decodeJwt(currentSession.idToken)
@@ -31,14 +41,21 @@ export const serverFhirResources = {
3141
const encounterId = currentSession.encounter
3242

3343
if (typeof practitionerId !== 'string') {
34-
throw new Error('practitionerId is not string')
44+
return { errorType: 'INVALID_PRACTITIONER_ID', zodErrors: [] }
3545
}
36-
const documentReference = prepareDocRefWithB64Data(practitionerId, patientId, encounterId, pdf, title)
46+
const documentReferencePayload = prepareDocRefWithB64Data({
47+
title,
48+
pdf,
49+
sykmeldingId,
50+
encounterId,
51+
patientId,
52+
practitionerId,
53+
})
3754

3855
const resourcePath = `${currentSession.issuer}/DocumentReference/`
3956
const documentReferenceResponse = await fetch(resourcePath, {
4057
method: 'POST',
41-
body: JSON.stringify(documentReference),
58+
body: JSON.stringify(documentReferencePayload),
4259
headers: {
4360
Authorization: `Bearer ${currentSession.accessToken}`,
4461
ContentType: 'application/fhir+json',
@@ -55,24 +72,30 @@ export const serverFhirResources = {
5572
logger.error(`Request to create DocumentReference failed with json: ${JSON.stringify(json)}`)
5673
}
5774

58-
throw new Error('Unable to create DocumentReference')
75+
return { errorType: 'INVALID_FHIR_RESPONSE', zodErrors: [] }
5976
}
6077

6178
const docRefResult = await documentReferenceResponse.json()
62-
const parsedDocRefResult = FhirDocumentReferenceResponseSchema.safeParse(docRefResult)
79+
const parsedDocRefResult = FhirDocumentReferenceBaseSchema.safeParse(docRefResult)
6380
if (!parsedDocRefResult.success) {
64-
throw new Error('DocumentReference was not a valid FhirDocumentReference', {
65-
cause: parsedDocRefResult.error,
66-
})
81+
return { errorType: 'INVALID_FHIR_RESPONSE', zodErrors: [] }
6782
}
6883

69-
return parsedDocRefResult.data
84+
return { outcome: 'NEWLY_CREATED', documentReference: parsedDocRefResult.data }
7085
},
7186

72-
getDocumentReference: async (sykmeldingId: string): Promise<DocumentReferenceResponse> => {
87+
getDocumentReference: async (
88+
sykmeldingId: string,
89+
): Promise<
90+
| WriteToEhrResult
91+
| { errorType: 'DOCUMENT_NOT_FOUND' }
92+
| { errorType: 'INVALID_FHIR_RESPONSE'; zodErrors: ZodIssue[] }
93+
| { errorType: 'INACTIVE_USER_SESSION'; zodErrors: ZodIssue[] }
94+
| { errorType: 'FAILED_TO_GET_DOCUMENT_REFERENCE'; zodErrors: ZodIssue[] }
95+
> => {
7396
const currentSession = await getSession()
7497
if (currentSession == null) {
75-
throw new Error('Active session is required')
98+
return { errorType: 'INACTIVE_USER_SESSION', zodErrors: [] }
7699
}
77100
const resourcePath = `${currentSession.issuer}/DocumentReference/${sykmeldingId}`
78101
logger.info(`Resource path: ${resourcePath}`)
@@ -84,6 +107,11 @@ export const serverFhirResources = {
84107
},
85108
})
86109

110+
if (documentReferenceResponse.status === 404) {
111+
logger.info(`DocumentReference/${sykmeldingId} was not found on FHIR server`)
112+
return { errorType: 'DOCUMENT_NOT_FOUND' }
113+
}
114+
87115
if (!documentReferenceResponse.ok) {
88116
logger.error('Request to get DocumentReference failed', documentReferenceResponse)
89117
if (documentReferenceResponse.headers.get('Content-Type')?.includes('text/plain')) {
@@ -93,19 +121,20 @@ export const serverFhirResources = {
93121
const json = await documentReferenceResponse.json()
94122
logger.error(`Request to get DocumentReference failed with json: ${JSON.stringify(json)}`)
95123
}
96-
97-
throw new Error('Unable to get DocumentReference')
124+
return { errorType: 'FAILED_TO_GET_DOCUMENT_REFERENCE', zodErrors: [] }
98125
}
99126

100-
const safeParsedDocumentReferenceResponse = FhirDocumentReferenceResponseSchema.safeParse(
127+
const safeParsedDocumentReferenceResponse = FhirDocumentReferenceBaseSchema.safeParse(
101128
await documentReferenceResponse.json(),
102129
)
103130
if (!safeParsedDocumentReferenceResponse.success) {
104-
throw new Error('DocumentReference was not a valid FhirDocumentReference', {
105-
cause: safeParsedDocumentReferenceResponse.error,
106-
})
131+
return {
132+
errorType: 'INVALID_FHIR_RESPONSE',
133+
zodErrors: safeParsedDocumentReferenceResponse.error.errors,
134+
}
107135
}
108-
return safeParsedDocumentReferenceResponse.data
136+
137+
return { outcome: 'ALREADY_EXISTS', documentReference: safeParsedDocumentReferenceResponse.data }
109138
},
110139

111140
getBehandlerInfo: async (): Promise<BehandlerInfo> => {
@@ -165,15 +194,26 @@ export const serverFhirResources = {
165194
},
166195
}
167196

168-
function prepareDocRefWithB64Data(
169-
patientId: string,
170-
practitionerId: string,
171-
encounterId: string,
172-
pdf: string,
173-
title: string,
174-
): FhirDocumentReference {
197+
type PrepareDocRefOpts = {
198+
patientId: string
199+
practitionerId: string
200+
encounterId: string
201+
pdf: string
202+
title: string
203+
sykmeldingId: string
204+
}
205+
206+
function prepareDocRefWithB64Data({
207+
patientId,
208+
practitionerId,
209+
encounterId,
210+
pdf,
211+
title,
212+
sykmeldingId,
213+
}: PrepareDocRefOpts): Omit<FhirDocumentReference, 'meta'> {
175214
return {
176215
resourceType: 'DocumentReference',
216+
id: sykmeldingId,
177217
status: 'current',
178218
type: {
179219
coding: [

0 commit comments

Comments
 (0)