1
0
mirror of https://github.com/pcvolkmer/etl-processor.git synced 2025-04-19 09:16:51 +00:00

feat: use issue severity to create status (#90)

This commit is contained in:
Paul-Christian Volkmer 2025-04-03 17:06:03 +02:00 committed by GitHub
parent 98b971d7db
commit befeef3153
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 293 additions and 70 deletions

View File

@ -264,6 +264,27 @@ ein Consent-Widerspruch erfolgte.
Dieses Vorgehen empfiehlt sich, wenn Sie gespeicherte Records nachgelagert für andere Auswertungen verwenden möchten.
### Antworten und Statusauswertung
Anfragen and bwHC-Backend aus Versionen bis 0.9.x wurden wie folgt behandelt:
| HTTP-Response | Status |
|----------------|-----------|
| `HTTP 200` | `SUCCESS` |
| `HTTP 201` | `WARNING` |
| `HTTP 400-...` | `ERROR` |
Dies konnte dazu führen, dass zwar mit einem `HTTP 201` geantwortet wurde, aber dennoch in der Issue-Liste die
Severity `error` aufgetaucht ist.
Ab Version 0.10 wird die Issue-Liste der Antwort verwendet und die darion enthaltene höchste Severity-Stufe als Ergebnis verwendet.
| Höchste Severity | Status |
|------------------|-----------|
| `info` | `SUCCESS` |
| `warning` | `WARNING` |
| `error`, `fatal` | `ERROR` |
## Docker-Images
Diese Anwendung ist auch als Docker-Image verfügbar: https://github.com/pcvolkmer/etl-processor/pkgs/container/etl-processor

View File

@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.config
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
import dev.dnpm.etl.processor.monitoring.ReportService
import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService
import dev.dnpm.etl.processor.output.MtbFileSender
import dev.dnpm.etl.processor.output.RestBwhcMtbFileSender
@ -53,15 +54,16 @@ class AppRestConfiguration {
fun restMtbFileSender(
restTemplate: RestTemplate,
restTargetProperties: RestTargetProperties,
retryTemplate: RetryTemplate
retryTemplate: RetryTemplate,
reportService: ReportService,
): MtbFileSender {
if (restTargetProperties.isBwhc) {
logger.info("Selected 'RestBwhcMtbFileSender'")
return RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
return RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
}
logger.info("Selected 'RestDipMtbFileSender'")
return RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
return RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
}
@Bean

View File

@ -25,6 +25,8 @@ import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.ObjectMapper
import dev.dnpm.etl.processor.monitoring.ReportService.Issue
import dev.dnpm.etl.processor.monitoring.ReportService.Severity
class ReportService(
private val objectMapper: ObjectMapper
@ -63,4 +65,13 @@ class ReportService(
WARNING("warning"),
INFO("info")
}
}
fun List<Issue>.asRequestStatus(): RequestStatus {
val severity = this.minOfOrNull { it.severity }
return when (severity) {
Severity.FATAL, Severity.ERROR -> RequestStatus.ERROR
Severity.WARNING -> RequestStatus.WARNING
else -> RequestStatus.SUCCESS
}
}

View File

@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.output
import dev.dnpm.etl.processor.PatientPseudonym
import dev.dnpm.etl.processor.config.RestTargetProperties
import dev.dnpm.etl.processor.monitoring.ReportService
import org.springframework.retry.support.RetryTemplate
import org.springframework.web.client.RestTemplate
import org.springframework.web.util.UriComponentsBuilder
@ -28,8 +29,9 @@ import org.springframework.web.util.UriComponentsBuilder
class RestBwhcMtbFileSender(
restTemplate: RestTemplate,
private val restTargetProperties: RestTargetProperties,
retryTemplate: RetryTemplate
) : RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate) {
retryTemplate: RetryTemplate,
reportService: ReportService,
) : RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) {
override fun sendUrl(): String {
return UriComponentsBuilder

View File

@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.output
import dev.dnpm.etl.processor.PatientPseudonym
import dev.dnpm.etl.processor.config.RestTargetProperties
import dev.dnpm.etl.processor.monitoring.ReportService
import org.springframework.retry.support.RetryTemplate
import org.springframework.web.client.RestTemplate
import org.springframework.web.util.UriComponentsBuilder
@ -28,8 +29,9 @@ import org.springframework.web.util.UriComponentsBuilder
class RestDipMtbFileSender(
restTemplate: RestTemplate,
private val restTargetProperties: RestTargetProperties,
retryTemplate: RetryTemplate
) : RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate) {
retryTemplate: RetryTemplate,
reportService: ReportService
) : RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) {
override fun sendUrl(): String {
return UriComponentsBuilder

View File

@ -22,6 +22,8 @@ package dev.dnpm.etl.processor.output
import dev.dnpm.etl.processor.config.RestTargetProperties
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.PatientPseudonym
import dev.dnpm.etl.processor.monitoring.ReportService
import dev.dnpm.etl.processor.monitoring.asRequestStatus
import org.slf4j.LoggerFactory
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
@ -34,7 +36,8 @@ import org.springframework.web.client.RestTemplate
abstract class RestMtbFileSender(
private val restTemplate: RestTemplate,
private val restTargetProperties: RestTargetProperties,
private val retryTemplate: RetryTemplate
private val retryTemplate: RetryTemplate,
private val reportService: ReportService
) : MtbFileSender {
private val logger = LoggerFactory.getLogger(RestMtbFileSender::class.java)
@ -56,19 +59,19 @@ abstract class RestMtbFileSender(
if (!response.statusCode.is2xxSuccessful) {
logger.warn("Error sending to remote system: {}", response.body)
return@execute MtbFileSender.Response(
response.statusCode.asRequestStatus(),
reportService.deserialize(response.body).asRequestStatus(),
"Status-Code: ${response.statusCode.value()}"
)
}
logger.debug("Sent file via RestMtbFileSender")
return@execute MtbFileSender.Response(response.statusCode.asRequestStatus(), response.body.orEmpty())
return@execute MtbFileSender.Response(reportService.deserialize(response.body).asRequestStatus(), response.body.orEmpty())
}
} catch (e: IllegalArgumentException) {
logger.error("Not a valid URI to export to: '{}'", restTargetProperties.uri!!)
} catch (e: RestClientResponseException) {
logger.info(restTargetProperties.uri!!.toString())
logger.error("Request data not accepted by remote system", e)
return MtbFileSender.Response(e.statusCode.asRequestStatus(), e.responseBodyAsString)
return MtbFileSender.Response(reportService.deserialize(e.responseBodyAsString).asRequestStatus(), e.responseBodyAsString)
}
return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung")
}

View File

@ -19,14 +19,18 @@
package dev.dnpm.etl.processor.output
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import de.ukw.ccc.bwhc.dto.*
import dev.dnpm.etl.processor.PatientPseudonym
import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.config.RestTargetProperties
import dev.dnpm.etl.processor.monitoring.ReportService
import dev.dnpm.etl.processor.monitoring.RequestStatus
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
@ -45,6 +49,8 @@ class RestBwhcMtbFileSenderTest {
private lateinit var restMtbFileSender: RestMtbFileSender
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
@BeforeEach
fun setup() {
val restTemplate = RestTemplate()
@ -53,7 +59,8 @@ class RestBwhcMtbFileSenderTest {
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
this.restMtbFileSender = RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
this.restMtbFileSender =
RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
}
@ParameterizedTest
@ -94,7 +101,8 @@ class RestBwhcMtbFileSenderTest {
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
this.restMtbFileSender = RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
this.restMtbFileSender =
RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
val expectedCount = when (requestWithResponse.httpStatus) {
// OK - No Retry
@ -123,7 +131,8 @@ class RestBwhcMtbFileSenderTest {
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
this.restMtbFileSender = RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
this.restMtbFileSender =
RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
val expectedCount = when (requestWithResponse.httpStatus) {
// OK - No Retry
@ -154,24 +163,6 @@ class RestBwhcMtbFileSenderTest {
val TEST_REQUEST_ID = RequestId("TestId")
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
private val warningBody = """
{
"patient_id": "PID",
"issues": [
{ "severity": "warning", "message": "Something is not right" }
]
}
""".trimIndent()
private val errorBody = """
{
"patient_id": "PID",
"issues": [
{ "severity": "error", "message": "Something is very bad" }
]
}
""".trimIndent()
val mtbFile: MtbFile = MtbFile.builder()
.withPatient(
Patient.builder()
@ -205,21 +196,34 @@ class RestBwhcMtbFileSenderTest {
@JvmStatic
fun mtbFileRequestWithResponseSource(): Set<RequestWithResponse> {
return setOf(
RequestWithResponse(HttpStatus.OK, "{}", MtbFileSender.Response(RequestStatus.SUCCESS, "{}")),
RequestWithResponse(
HttpStatus.OK,
responseBodyWithMaxSeverity(ReportService.Severity.INFO),
MtbFileSender.Response(
RequestStatus.SUCCESS,
responseBodyWithMaxSeverity(ReportService.Severity.INFO)
)
),
RequestWithResponse(
HttpStatus.CREATED,
warningBody,
MtbFileSender.Response(RequestStatus.WARNING, warningBody)
responseBodyWithMaxSeverity(ReportService.Severity.WARNING),
MtbFileSender.Response(
RequestStatus.WARNING,
responseBodyWithMaxSeverity(ReportService.Severity.WARNING)
)
),
RequestWithResponse(
HttpStatus.BAD_REQUEST,
ERROR_RESPONSE_BODY,
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
responseBodyWithMaxSeverity(ReportService.Severity.ERROR),
MtbFileSender.Response(RequestStatus.ERROR, responseBodyWithMaxSeverity(ReportService.Severity.ERROR))
),
RequestWithResponse(
HttpStatus.UNPROCESSABLE_ENTITY,
errorBody,
MtbFileSender.Response(RequestStatus.ERROR, errorBody)
responseBodyWithMaxSeverity(ReportService.Severity.FATAL),
MtbFileSender.Response(
RequestStatus.ERROR,
responseBodyWithMaxSeverity(ReportService.Severity.FATAL)
)
),
// Some more errors not mentioned in documentation
RequestWithResponse(
@ -256,6 +260,52 @@ class RestBwhcMtbFileSenderTest {
)
)
}
fun responseBodyWithMaxSeverity(severity: ReportService.Severity): String {
return when (severity) {
ReportService.Severity.INFO -> """
{
"patient": "PID",
"issues": [
{ "severity": "info", "message": "Info Message" }
]
}
"""
ReportService.Severity.WARNING -> """
{
"patient": "PID",
"issues": [
{ "severity": "info", "message": "Info Message" },
{ "severity": "warning", "message": "Warning Message" }
]
}
"""
ReportService.Severity.ERROR -> """
{
"patient": "PID",
"issues": [
{ "severity": "info", "message": "Info Message" },
{ "severity": "warning", "message": "Warning Message" },
{ "severity": "error", "message": "Error Message" }
]
}
"""
ReportService.Severity.FATAL -> """
{
"patient": "PID",
"issues": [
{ "severity": "info", "message": "Info Message" },
{ "severity": "warning", "message": "Warning Message" },
{ "severity": "error", "message": "Error Message" },
{ "severity": "fatal", "message": "Fatal Message" }
]
}
"""
}
}
}

View File

@ -19,13 +19,17 @@
package dev.dnpm.etl.processor.output
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import de.ukw.ccc.bwhc.dto.*
import dev.dnpm.etl.processor.PatientPseudonym
import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.config.AppConfiguration
import dev.dnpm.etl.processor.config.RestTargetProperties
import dev.dnpm.etl.processor.monitoring.ReportService
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.output.RestBwhcMtbFileSenderTest.Companion
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.params.ParameterizedTest
@ -48,6 +52,8 @@ class RestDipMtbFileSenderTest {
private lateinit var restMtbFileSender: RestMtbFileSender
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
@BeforeEach
fun setup() {
val restTemplate = RestTemplate()
@ -56,7 +62,7 @@ class RestDipMtbFileSenderTest {
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
}
@ParameterizedTest
@ -98,11 +104,14 @@ class RestDipMtbFileSenderTest {
retryTemplate.setBackOffPolicy(NoBackOffPolicy())
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
this.restMtbFileSender =
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
val expectedCount = when (requestWithResponse.httpStatus) {
// OK - No Retry
HttpStatus.OK, HttpStatus.CREATED, HttpStatus.UNPROCESSABLE_ENTITY, HttpStatus.BAD_REQUEST -> ExpectedCount.max(1)
HttpStatus.OK, HttpStatus.CREATED, HttpStatus.UNPROCESSABLE_ENTITY, HttpStatus.BAD_REQUEST -> ExpectedCount.max(
1
)
// Request failed - Retry max 3 times
else -> ExpectedCount.max(3)
}
@ -128,11 +137,14 @@ class RestDipMtbFileSenderTest {
retryTemplate.setBackOffPolicy(NoBackOffPolicy())
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
this.restMtbFileSender =
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
val expectedCount = when (requestWithResponse.httpStatus) {
// OK - No Retry
HttpStatus.OK, HttpStatus.CREATED, HttpStatus.UNPROCESSABLE_ENTITY, HttpStatus.BAD_REQUEST -> ExpectedCount.max(1)
HttpStatus.OK, HttpStatus.CREATED, HttpStatus.UNPROCESSABLE_ENTITY, HttpStatus.BAD_REQUEST -> ExpectedCount.max(
1
)
// Request failed - Retry max 3 times
else -> ExpectedCount.max(3)
}
@ -159,24 +171,6 @@ class RestDipMtbFileSenderTest {
val TEST_REQUEST_ID = RequestId("TestId")
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
private val warningBody = """
{
"patient_id": "PID",
"issues": [
{ "severity": "warning", "message": "Something is not right" }
]
}
""".trimIndent()
private val errorBody = """
{
"patient_id": "PID",
"issues": [
{ "severity": "error", "message": "Something is very bad" }
]
}
""".trimIndent()
val mtbFile: MtbFile = MtbFile.builder()
.withPatient(
Patient.builder()
@ -210,21 +204,28 @@ class RestDipMtbFileSenderTest {
@JvmStatic
fun mtbFileRequestWithResponseSource(): Set<RequestWithResponse> {
return setOf(
RequestWithResponse(HttpStatus.OK, "{}", MtbFileSender.Response(RequestStatus.SUCCESS, "{}")),
RequestWithResponse(
HttpStatus.OK,
responseBodyWithMaxSeverity(ReportService.Severity.INFO),
MtbFileSender.Response(
RequestStatus.SUCCESS,
responseBodyWithMaxSeverity(ReportService.Severity.INFO)
)
),
RequestWithResponse(
HttpStatus.CREATED,
warningBody,
MtbFileSender.Response(RequestStatus.WARNING, warningBody)
responseBodyWithMaxSeverity(ReportService.Severity.WARNING),
MtbFileSender.Response(RequestStatus.WARNING, responseBodyWithMaxSeverity(ReportService.Severity.WARNING))
),
RequestWithResponse(
HttpStatus.BAD_REQUEST,
ERROR_RESPONSE_BODY,
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
responseBodyWithMaxSeverity(ReportService.Severity.ERROR),
MtbFileSender.Response(RequestStatus.ERROR, responseBodyWithMaxSeverity(ReportService.Severity.ERROR))
),
RequestWithResponse(
HttpStatus.UNPROCESSABLE_ENTITY,
errorBody,
MtbFileSender.Response(RequestStatus.ERROR, errorBody)
responseBodyWithMaxSeverity(ReportService.Severity.ERROR),
MtbFileSender.Response(RequestStatus.ERROR, responseBodyWithMaxSeverity(ReportService.Severity.ERROR))
),
// Some more errors not mentioned in documentation
RequestWithResponse(
@ -261,6 +262,52 @@ class RestDipMtbFileSenderTest {
)
)
}
fun responseBodyWithMaxSeverity(severity: ReportService.Severity): String {
return when (severity) {
ReportService.Severity.INFO -> """
{
"patient": "PID",
"issues": [
{ "severity": "info", "message": "Info Message" }
]
}
"""
ReportService.Severity.WARNING -> """
{
"patient": "PID",
"issues": [
{ "severity": "info", "message": "Info Message" },
{ "severity": "warning", "message": "Warning Message" }
]
}
"""
ReportService.Severity.ERROR -> """
{
"patient": "PID",
"issues": [
{ "severity": "info", "message": "Info Message" },
{ "severity": "warning", "message": "Warning Message" },
{ "severity": "error", "message": "Error Message" }
]
}
"""
ReportService.Severity.FATAL -> """
{
"patient": "PID",
"issues": [
{ "severity": "info", "message": "Info Message" },
{ "severity": "warning", "message": "Warning Message" },
{ "severity": "error", "message": "Error Message" },
{ "severity": "fatal", "message": "Fatal Message" }
]
}
"""
}
}
}

View File

@ -1,7 +1,7 @@
/*
* This file is part of ETL-Processor
*
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
@ -22,9 +22,14 @@ package dev.dnpm.etl.processor.services
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import dev.dnpm.etl.processor.monitoring.ReportService
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.asRequestStatus
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
class ReportServiceTest {
@ -60,6 +65,15 @@ class ReportServiceTest {
assertThat(actual[2].message).isEqualTo("Warning Message")
assertThat(actual[3].severity).isEqualTo(ReportService.Severity.INFO)
assertThat(actual[3].message).isEqualTo("Info Message")
assertThat(actual.asRequestStatus()).isEqualTo(RequestStatus.ERROR)
}
@ParameterizedTest
@MethodSource("testData")
fun shouldParseDataQualityReport(json: String, requestStatus: RequestStatus) {
val actual = this.reportService.deserialize(json)
assertThat(actual.asRequestStatus()).isEqualTo(requestStatus)
}
@Test
@ -73,4 +87,75 @@ class ReportServiceTest {
assertThat(actual[0].message).isEqualTo("Not parsable data quality report '$invalidResponse'")
}
companion object {
@JvmStatic
fun testData(): Set<Arguments> {
return setOf(
Arguments.of(
"""
{
"patient": "4711",
"issues": [
{ "severity": "info", "message": "Info Message" },
{ "severity": "warning", "message": "Warning Message" },
{ "severity": "error", "message": "Error Message" },
{ "severity": "fatal", "message": "Fatal Message" }
]
}
""".trimIndent(),
RequestStatus.ERROR
),
Arguments.of(
"""
{
"patient": "4711",
"issues": [
{ "severity": "info", "message": "Info Message" },
{ "severity": "warning", "message": "Warning Message" },
{ "severity": "error", "message": "Error Message" }
]
}
""".trimIndent(),
RequestStatus.ERROR
),
Arguments.of(
"""
{
"patient": "4711",
"issues": [
{ "severity": "error", "message": "Error Message" }
{ "severity": "info", "message": "Info Message" }
]
}
""".trimIndent(),
RequestStatus.ERROR
),
Arguments.of(
"""
{
"patient": "4711",
"issues": [
{ "severity": "info", "message": "Info Message" },
{ "severity": "warning", "message": "Warning Message" }
]
}
""".trimIndent(),
RequestStatus.WARNING
),
Arguments.of(
"""
{
"patient": "4711",
"issues": [
{ "severity": "info", "message": "Info Message" }
]
}
""".trimIndent(),
RequestStatus.SUCCESS
)
)
}
}
}