From 642d62daedd584b96d991ffac1a4381f21baf698 Mon Sep 17 00:00:00 2001 From: Chamath Jayasena Date: Tue, 25 Jun 2024 11:56:19 +0530 Subject: [PATCH 01/10] Add member match pre-built service Related issue: https://github.com/wso2-enterprise/open-healthcare/issues/1553 formatting add content for demo setup Add bulk data export client implementation. fix:https://github.com/wso2-enterprise/open-healthcare/issues/1554 adding unit tests minor fixes add single patient and group export support format code restructure codebase --- .gitignore | 1 + .../bulk-export-client-service/Ballerina.toml | 8 + .../Dependencies.toml | 413 ++++++++++++++++ .../inMemoryStorage.bal | 42 ++ .../bulk-export-client-service/records.bal | 50 ++ .../bulk-export-client-service/registry.bal | 40 ++ .../bulk-export-client-service/service.bal | 209 ++++++++ .../tests/resources/exportedData.json | 6 + .../tests/test_service.bal | 54 +++ .../tests/test_storage.bal | 66 +++ .../bulk-export-client-service/utils.bal | 271 +++++++++++ .../member-match-service/.gitignore | 3 + .../member-match-service/Ballerina.toml | 20 + .../member-match-service/Dependencies.toml | 449 ++++++++++++++++++ .../member-match-service/api_config.bal | 54 +++ .../member-match-service/custom_matcher.bal | 11 + .../member-match-service/service.bal | 95 ++++ .../member-match-service/service_utils.bal | 140 ++++++ .../cms-0057-f/member-match-service/types.bal | 35 ++ 19 files changed, 1967 insertions(+) create mode 100644 regulation/cms-0057-f/bulk-export-client-service/Ballerina.toml create mode 100644 regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml create mode 100644 regulation/cms-0057-f/bulk-export-client-service/inMemoryStorage.bal create mode 100644 regulation/cms-0057-f/bulk-export-client-service/records.bal create mode 100644 regulation/cms-0057-f/bulk-export-client-service/registry.bal create mode 100644 regulation/cms-0057-f/bulk-export-client-service/service.bal create mode 100644 regulation/cms-0057-f/bulk-export-client-service/tests/resources/exportedData.json create mode 100644 regulation/cms-0057-f/bulk-export-client-service/tests/test_service.bal create mode 100644 regulation/cms-0057-f/bulk-export-client-service/tests/test_storage.bal create mode 100644 regulation/cms-0057-f/bulk-export-client-service/utils.bal create mode 100644 regulation/cms-0057-f/member-match-service/.gitignore create mode 100644 regulation/cms-0057-f/member-match-service/Ballerina.toml create mode 100644 regulation/cms-0057-f/member-match-service/Dependencies.toml create mode 100644 regulation/cms-0057-f/member-match-service/api_config.bal create mode 100644 regulation/cms-0057-f/member-match-service/custom_matcher.bal create mode 100644 regulation/cms-0057-f/member-match-service/service.bal create mode 100644 regulation/cms-0057-f/member-match-service/service_utils.bal create mode 100644 regulation/cms-0057-f/member-match-service/types.bal diff --git a/.gitignore b/.gitignore index be2c352..71c87a5 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ Cloud.toml # Mac configuration file .DS_Store +files/ diff --git a/regulation/cms-0057-f/bulk-export-client-service/Ballerina.toml b/regulation/cms-0057-f/bulk-export-client-service/Ballerina.toml new file mode 100644 index 0000000..c1f2b81 --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "wso2" +name = "bulk_export_client_service" +version = "0.1.0" +distribution = "2201.8.6" + +[build-options] +observabilityIncluded = true diff --git a/regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml b/regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml new file mode 100644 index 0000000..129ccc7 --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml @@ -0,0 +1,413 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.8.6" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.8.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.6.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "ftp" +version = "2.10.1" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "task"} +] +modules = [ + {org = "ballerina", packageName = "ftp", moduleName = "ftp"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.10.15" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "http", moduleName = "http"}, + {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.6.1" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] +modules = [ + {org = "ballerina", packageName = "io", moduleName = "io"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "jwt" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.error" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerina", packageName = "log", moduleName = "log"} +] + +[[package]] +org = "ballerina" +name = "mime" +version = "2.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.2.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerina", packageName = "task", moduleName = "task"} +] + +[[package]] +org = "ballerina" +name = "test" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.error"} +] +modules = [ + {org = "ballerina", packageName = "test", moduleName = "test"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.4.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "time", moduleName = "time"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.4.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "uuid" +version = "1.7.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerina", packageName = "uuid", moduleName = "uuid"} +] + +[[package]] +org = "ballerinai" +name = "observe" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerinai", packageName = "observe", moduleName = "observe"} +] + +[[package]] +org = "ballerinax" +name = "health.base" +version = "1.0.3" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "uuid"} +] + +[[package]] +org = "ballerinax" +name = "health.fhir.r4" +version = "5.1.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"}, + {org = "ballerina", name = "uuid"}, + {org = "ballerinax", name = "health.base"} +] +modules = [ + {org = "ballerinax", packageName = "health.fhir.r4", moduleName = "health.fhir.r4"} +] + +[[package]] +org = "ballerinax" +name = "health.fhir.r4.international401" +version = "2.1.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "log"}, + {org = "ballerinax", name = "health.fhir.r4"} +] +modules = [ + {org = "ballerinax", packageName = "health.fhir.r4.international401", moduleName = "health.fhir.r4.international401"} +] + +[[package]] +org = "wso2" +name = "bulk_export_client_service" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "ftp"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "test"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "uuid"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "health.fhir.r4"}, + {org = "ballerinax", name = "health.fhir.r4.international401"} +] +modules = [ + {org = "wso2", packageName = "bulk_export_client_service", moduleName = "bulk_export_client_service"} +] + diff --git a/regulation/cms-0057-f/bulk-export-client-service/inMemoryStorage.bal b/regulation/cms-0057-f/bulk-export-client-service/inMemoryStorage.bal new file mode 100644 index 0000000..542588d --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/inMemoryStorage.bal @@ -0,0 +1,42 @@ +import ballerina/time; + +//This file represents the in-memory storage of the export tasks and polling events. + +final isolated map exportTasks = {}; + +isolated function addExportTasktoMemory(map taskMap, ExportTask exportTask) returns boolean { + // add the export task to the memory + exportTask.lastUpdated = time:utcNow(); + lock { + taskMap[exportTask.id] = exportTask; + } + return true; +} + +isolated function addPollingEventToMemory(map taskMap, PollingEvent pollingEvent) returns boolean { + // add the polling event to the memory + ExportTask exportTask = taskMap.get(pollingEvent.id); + exportTask.lastUpdated = time:utcNow(); + exportTask.lastStatus = pollingEvent.exportStatus ?: "In-progress"; + lock { + taskMap.get(pollingEvent.id).pollingEvents.push(pollingEvent); + } + return true; +} + +isolated function updateExportTaskStatusInMemory(map taskMap, string exportTaskId, string newStatus) returns boolean { + + ExportTask exportTask = taskMap.get(exportTaskId); + exportTask.lastUpdated = time:utcNow(); + exportTask.lastStatus = newStatus; + return true; +} + +isolated function getExportTaskFromMemory(string exportId) returns ExportTask { + // get the export task from the memory + ExportTask exportTask; + lock { + exportTask = exportTasks.get(exportId).clone(); + } + return exportTask; +} diff --git a/regulation/cms-0057-f/bulk-export-client-service/records.bal b/regulation/cms-0057-f/bulk-export-client-service/records.bal new file mode 100644 index 0000000..e2de8b0 --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/records.bal @@ -0,0 +1,50 @@ + +public type BulkExportServerConfig record {| + string baseUrl; + string tokenUrl; + string clientId; + string clientSecret; + string[] scopes; + string fileServerUrl; + string contextPath; + decimal defaultIntervalInSec; +|}; + +// have a generic config for source server and target server for FHIR cases +// check Ballerina FTP client for the FTP server config + +public type BulkExportClientConfig record {| + int port; + boolean authEnabled; + string targetDirectory; +|}; + +public type FtpServerConfig record {| + boolean enabled; + string host; + int port; + string username; + string password; + string directory; +|}; + +public type OutputFile record {| + string 'type; + string url; + int count; +|}; + +public type ExportSummary record {| + string transactionTime; + string request; + boolean requiresAccessToken; + OutputFile[] output; + string[] deleted; + string[] 'error; +|}; + +public type MatchedPatient record {| + string id; + string canonical?; + map identifiers?; +|}; diff --git a/regulation/cms-0057-f/bulk-export-client-service/registry.bal b/regulation/cms-0057-f/bulk-export-client-service/registry.bal new file mode 100644 index 0000000..68fc88a --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/registry.bal @@ -0,0 +1,40 @@ +import ballerina/time; + +# Use to keep track of each polling event. +# +# + id - Id of the associated export task +# + eventStatus - whether the polling event is success or failed +# + exportStatus - status recieved from the export server +# + progress - progress of the export task +public type PollingEvent record {| + + string id; + string eventStatus; + string exportStatus?; + string progress?; + +|}; + +# Use to keep track of ongoing/completed exports. +# +# + id - task ID (generated internally) +# + lastUpdated - timestamp of the last polling event +# + lastStatus - export status recieved from the last polling event +# + pollingEvents - array of polling events +public type ExportTask record {| + + string id; + time:Utc lastUpdated?; + string lastStatus; + PollingEvent[] pollingEvents; + +|}; + +type getExportTask function (string exportId) returns ExportTask; + +type getPollingEvents function (string exportId) returns [PollingEvent]; + +type addExportTask isolated function (map taskMap, ExportTask exportTask) returns boolean; + +type addPollingEvent isolated function (map taskMap, PollingEvent pollingEvent) returns boolean; + diff --git a/regulation/cms-0057-f/bulk-export-client-service/service.bal b/regulation/cms-0057-f/bulk-export-client-service/service.bal new file mode 100644 index 0000000..0fefff1 --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/service.bal @@ -0,0 +1,209 @@ +import ballerina/http; +import ballerina/log; +import ballerina/task; +import ballerina/uuid; +import ballerinax/health.fhir.r4.international401; + +configurable BulkExportServerConfig exportSeverConfig = ?; +configurable BulkExportClientConfig clientServiceConfig = ?; +configurable FtpServerConfig ftpServerConfig = ?; + +http:OAuth2ClientCredentialsGrantConfig config = { + tokenUrl: exportSeverConfig.tokenUrl, + clientId: exportSeverConfig.clientId, + clientSecret: exportSeverConfig.clientSecret, + scopes: exportSeverConfig.scopes +}; + +isolated http:Client statusClient = check new (exportSeverConfig.baseUrl); + +service /trigger on new http:Listener(clientServiceConfig.port) { + + function init() returns error? { + + if clientServiceConfig.authEnabled { + lock { + statusClient = check new (exportSeverConfig.baseUrl, auth = config.clone()); + } + } else { + lock { + statusClient = check new (exportSeverConfig.baseUrl); + } + } + + log:printInfo("Bulk export client Service is started...", port = clientServiceConfig.port); + } + isolated resource function get export(string? _outputFormat, string? _since, string? _type) returns json|error { + + // update config for status polling + // initialize the status polling + addExportTask addTaskFunction = addExportTasktoMemory; + string taskId = uuid:createType1AsString(); + boolean isSuccess = false; + http:Response|http:ClientError status; + + log:printInfo("Bulk exporting started. Sending Kick-off request."); + + do { + + lock { + ExportTask exportTask = {id: taskId, lastStatus: "in-progress", pollingEvents: []}; + isSuccess = addTaskFunction(exportTasks, exportTask); + } + string queryString = populateQueryString(_outputFormat, _since, _type); + // kick-off request to the bulk export server + lock { + status = statusClient->get(string `${exportSeverConfig.contextPath}/Patient/$export${queryString}`, { + Accept: "application/fhir+json", + Prefer: "respond-async" + }); + } + submitBackgroundJob(taskId, status); + + if isSuccess { + log:printInfo("Export task persisted.", exportId = taskId); + } else { + log:printError("Error occurred while adding the export task to the memory."); + } + + } on fail var e { + log:printError("Error occurred while scheduling the status polling task.", e); + } + + string message = string `Export task is successfully kicked-off. ExportId: ${taskId} + To check the status, use: /trigger/status?exportId=${taskId}`; + return createOpereationOutcome("information", "processing", message).toJson(); + + } + + // This function is responsible for exporting data in bulk. + // It is an isolated resource function that handles the HTTP POST request for exporting data. + // The exported data will be saved to the specified file path. + // + // @param payload - The payload containing the data to be exported. + // @param _type - The types of the resource to be exported. Accept multiple values(comma seperated). + // + // @return The response indicating the success or failure of the export operation. + isolated resource function post export( + @http:Payload MatchedPatient[] matchedPatients, + @http:Query string? _outputFormat, + @http:Query string? _since, + @http:Query string? _type) returns json|error { + + addExportTask addTaskFunction = addExportTasktoMemory; + string taskId = uuid:createType1AsString(); + boolean isSuccess = false; + http:Response|http:ClientError status; + + log:printInfo("Bulk exporting started. Sending Kick-off request."); + do { + + lock { + ExportTask exportTask = {id: taskId, lastStatus: "in-progress", pollingEvents: []}; + isSuccess = addTaskFunction(exportTasks, exportTask); + } + international401:Parameters parametersResource = populateParamsResource(matchedPatients, _outputFormat, _since, _type); + // kick-off request to the bulk export server + + lock { + status = statusClient->post(string `${exportSeverConfig.contextPath}/Patient/$export`, parametersResource.clone().toJson(), + { + Accept: "application/fhir+json", + Prefer: "respond-async", + ContentType: "application/json" + }); + } + submitBackgroundJob(taskId, status); + + if isSuccess { + log:printInfo("Export task persisted.", exportId = taskId); + } else { + log:printError("Error occurred while adding the export task to the memory."); + } + + } on fail var e { + log:printError("Error occurred while scheduling the status polling task.", e); + } + + string message = string `Export task is successfully kicked-off. ExportId: ${taskId} + To check the status, use: /trigger/status?exportId=${taskId}`; + return createOpereationOutcome("information", "processing", message).toJson(); + + } + + isolated resource function get export/group/[string group_id](string? _outputFormat, string? _since, string? _type) returns json|error { + + // update config for status polling + // initialize the status polling + addExportTask addTaskFunction = addExportTasktoMemory; + string taskId = uuid:createType1AsString(); + boolean isSuccess = false; + http:Response|http:ClientError status; + + log:printInfo("Bulk exporting started. Sending Kick-off request."); + + do { + + lock { + ExportTask exportTask = {id: taskId, lastStatus: "in-progress", pollingEvents: []}; + isSuccess = addTaskFunction(exportTasks, exportTask); + } + string queryString = populateQueryString(_outputFormat, _since, _type); + // kick-off request to the bulk export server + lock { + status = statusClient->get(string `${exportSeverConfig.contextPath}/Group/${group_id}/$export${queryString}`, { + Accept: "application/fhir+json", + Prefer: "respond-async" + }); + } + submitBackgroundJob(taskId, status); + + if isSuccess { + log:printInfo("Export task persisted.", exportId = taskId); + } else { + log:printError("Error occurred while adding the export task to the memory."); + } + + } on fail var e { + log:printError("Error occurred while scheduling the status polling task.", e); + } + + string message = string `Export task is successfully kicked-off. ExportId: ${taskId} + To check the status, use: /trigger/status?exportId=${taskId}`; + return createOpereationOutcome("information", "processing", message).toJson(); + + } + + isolated resource function get status(string exportId) returns json|error { + + return getExportTaskFromMemory(exportId).toJson(); + + } + + isolated resource function get download(string location) returns http:STATUS_ACCEPTED|http:STATUS_INTERNAL_SERVER_ERROR { + + error? saveFileResult = saveFileInFS(location, "exportedData.json"); + if saveFileResult is error { + + } + + return http:STATUS_ACCEPTED; + + } +} + +isolated function submitBackgroundJob(string taskId, http:Response|http:ClientError status) { + if status is http:Response { + log:printDebug(status.statusCode.toBalString()); + + // get the location of the status check + do { + string location = check status.getHeader("Content-location"); + task:JobId|() _ = check executeJob(new PollingTask(taskId, location), exportSeverConfig.defaultIntervalInSec); + log:printDebug("Polling location recieved: " + location); + } on fail var e { + log:printError("Error occurred while getting the location or scheduling the Job", e); + // if location is available, can retry the task + } + } +} diff --git a/regulation/cms-0057-f/bulk-export-client-service/tests/resources/exportedData.json b/regulation/cms-0057-f/bulk-export-client-service/tests/resources/exportedData.json new file mode 100644 index 0000000..a11c726 --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/tests/resources/exportedData.json @@ -0,0 +1,6 @@ +{"resourceType":"Patient","id":"736a19c8-eea5-32c5-67ad-1947661de21a","meta":{"profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"]},"text":{"status":"generated","div":"
Generated by Synthea.Version identifier: master-branch-latest\n . Person seed: -8305409756606876035 Population seed: 1710790370898
"},"extension":[{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2106-3","display":"White"}},{"url":"text","valueString":"White"}]},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2186-5","display":"Not Hispanic or Latino"}},{"url":"text","valueString":"Not Hispanic or Latino"}]},{"url":"http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName","valueString":"Nora0 Keeling57"},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex","valueCode":"M"},{"url":"http://hl7.org/fhir/StructureDefinition/patient-birthPlace","valueAddress":{"city":"Norfolk","state":"Massachusetts","country":"US"}},{"url":"http://synthetichealth.github.io/synthea/disability-adjusted-life-years","valueDecimal":0},{"url":"http://synthetichealth.github.io/synthea/quality-adjusted-life-years","valueDecimal":36}],"identifier":[{"system":"https://github.com/synthetichealth/synthea","value":"736a19c8-eea5-32c5-67ad-1947661de21a"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"MR","display":"Medical Record Number"}],"text":"Medical Record Number"},"system":"http://hospital.smarthealthit.org","value":"736a19c8-eea5-32c5-67ad-1947661de21a"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"SS","display":"Social Security Number"}],"text":"Social Security Number"},"system":"http://hl7.org/fhir/sid/us-ssn","value":"999-64-7808"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"DL","display":"Driver's license number"}],"text":"Driver's license number"},"system":"urn:oid:2.16.840.1.113883.4.3.25","value":"S99929608"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"PPN","display":"Passport Number"}],"text":"Passport Number"},"system":"http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber","value":"X86059466X"}],"name":[{"use":"official","family":"Prohaska837","given":["Cortez851","Lucien408"],"prefix":["Mr."]}],"telecom":[{"system":"phone","value":"555-839-9316","use":"home"}],"gender":"male","birthDate":"1987-03-27","address":[{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/geolocation","extension":[{"url":"latitude","valueDecimal":42.198643304235844},{"url":"longitude","valueDecimal":-71.30214672040648}]}],"line":["624 Rippin Plaza"],"city":"Medfield","state":"MA","postalCode":"02052","country":"US"}],"maritalStatus":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v3-MaritalStatus","code":"M","display":"Married"}],"text":"Married"},"multipleBirthBoolean":false,"communication":[{"language":{"coding":[{"system":"urn:ietf:bcp:47","code":"en-US","display":"English (United States)"}],"text":"English (United States)"}}]} +{"resourceType":"Patient","id":"26d06b50-7868-829d-cf71-9f9a68901a81","meta":{"profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"]},"text":{"status":"generated","div":"
Generated by Synthea.Version identifier: master-branch-latest\n . Person seed: -3967916214635518055 Population seed: 1710790370898
"},"extension":[{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2106-3","display":"White"}},{"url":"text","valueString":"White"}]},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2186-5","display":"Not Hispanic or Latino"}},{"url":"text","valueString":"Not Hispanic or Latino"}]},{"url":"http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName","valueString":"Karl184 Halvorson124"},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex","valueCode":"F"},{"url":"http://hl7.org/fhir/StructureDefinition/patient-birthPlace","valueAddress":{"city":"Salem","state":"Massachusetts","country":"US"}},{"url":"http://synthetichealth.github.io/synthea/disability-adjusted-life-years","valueDecimal":0},{"url":"http://synthetichealth.github.io/synthea/quality-adjusted-life-years","valueDecimal":36}],"identifier":[{"system":"https://github.com/synthetichealth/synthea","value":"26d06b50-7868-829d-cf71-9f9a68901a81"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"MR","display":"Medical Record Number"}],"text":"Medical Record Number"},"system":"http://hospital.smarthealthit.org","value":"26d06b50-7868-829d-cf71-9f9a68901a81"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"SS","display":"Social Security Number"}],"text":"Social Security Number"},"system":"http://hl7.org/fhir/sid/us-ssn","value":"999-16-4506"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"DL","display":"Driver's license number"}],"text":"Driver's license number"},"system":"urn:oid:2.16.840.1.113883.4.3.25","value":"S99955901"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"PPN","display":"Passport Number"}],"text":"Passport Number"},"system":"http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber","value":"X51983542X"}],"name":[{"use":"official","family":"Moen819","given":["Ellis535","Elizabet136"],"prefix":["Mrs."]},{"use":"maiden","family":"Cole117","given":["Ellis535","Elizabet136"],"prefix":["Mrs."]}],"telecom":[{"system":"phone","value":"555-111-8552","use":"home"}],"gender":"female","birthDate":"1987-05-18","address":[{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/geolocation","extension":[{"url":"latitude","valueDecimal":42.043689932856125},{"url":"longitude","valueDecimal":-71.2931989189053}]}],"line":["732 Mitchell Flat"],"city":"Plainville","state":"MA","postalCode":"00000","country":"US"}],"maritalStatus":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v3-MaritalStatus","code":"M","display":"Married"}],"text":"Married"},"multipleBirthBoolean":false,"communication":[{"language":{"coding":[{"system":"urn:ietf:bcp:47","code":"en-US","display":"English (United States)"}],"text":"English (United States)"}}]} +{"resourceType":"Patient","id":"33652156-cbf8-4f5c-5856-0714d8e90f5b","meta":{"profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"]},"text":{"status":"generated","div":"
Generated by Synthea.Version identifier: master-branch-latest\n . Person seed: 8850013754564693531 Population seed: 1710790370898
"},"extension":[{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2106-3","display":"White"}},{"url":"text","valueString":"White"}]},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2186-5","display":"Not Hispanic or Latino"}},{"url":"text","valueString":"Not Hispanic or Latino"}]},{"url":"http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName","valueString":"Florene115 Jakubowski832"},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex","valueCode":"F"},{"url":"http://hl7.org/fhir/StructureDefinition/patient-birthPlace","valueAddress":{"city":"Randolph","state":"Massachusetts","country":"US"}},{"url":"http://synthetichealth.github.io/synthea/disability-adjusted-life-years","valueDecimal":0.4714571099095429},{"url":"http://synthetichealth.github.io/synthea/quality-adjusted-life-years","valueDecimal":70.52854289009046}],"identifier":[{"system":"https://github.com/synthetichealth/synthea","value":"33652156-cbf8-4f5c-5856-0714d8e90f5b"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"MR","display":"Medical Record Number"}],"text":"Medical Record Number"},"system":"http://hospital.smarthealthit.org","value":"33652156-cbf8-4f5c-5856-0714d8e90f5b"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"SS","display":"Social Security Number"}],"text":"Social Security Number"},"system":"http://hl7.org/fhir/sid/us-ssn","value":"999-85-2647"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"DL","display":"Driver's license number"}],"text":"Driver's license number"},"system":"urn:oid:2.16.840.1.113883.4.3.25","value":"S99941854"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"PPN","display":"Passport Number"}],"text":"Passport Number"},"system":"http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber","value":"X21925935X"}],"name":[{"use":"official","family":"Jacobi462","given":["Eliana466"],"prefix":["Mrs."]},{"use":"maiden","family":"Lang846","given":["Eliana466"],"prefix":["Mrs."]}],"telecom":[{"system":"phone","value":"555-155-3255","use":"home"}],"gender":"female","birthDate":"1953-01-01","address":[{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/geolocation","extension":[{"url":"latitude","valueDecimal":42.75272209311218},{"url":"longitude","valueDecimal":-71.06759472083945}]}],"line":["546 Kuhic Trailer Apt 79"],"city":"Haverhill","state":"MA","postalCode":"01830","country":"US"}],"maritalStatus":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v3-MaritalStatus","code":"M","display":"Married"}],"text":"Married"},"multipleBirthBoolean":false,"communication":[{"language":{"coding":[{"system":"urn:ietf:bcp:47","code":"en-US","display":"English (United States)"}],"text":"English (United States)"}}]} +{"resourceType":"Patient","id":"d73bb536-cc5c-0005-2da0-ea3a98435f5f","meta":{"profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"]},"text":{"status":"generated","div":"
Generated by Synthea.Version identifier: master-branch-latest\n . Person seed: 5970031910285992489 Population seed: 1710790370898
"},"extension":[{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2106-3","display":"White"}},{"url":"text","valueString":"White"}]},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2186-5","display":"Not Hispanic or Latino"}},{"url":"text","valueString":"Not Hispanic or Latino"}]},{"url":"http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName","valueString":"Edward499 Keebler762"},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex","valueCode":"F"},{"url":"http://hl7.org/fhir/StructureDefinition/patient-birthPlace","valueAddress":{"city":"Chicopee","state":"Massachusetts","country":"US"}},{"url":"http://synthetichealth.github.io/synthea/disability-adjusted-life-years","valueDecimal":0},{"url":"http://synthetichealth.github.io/synthea/quality-adjusted-life-years","valueDecimal":7}],"identifier":[{"system":"https://github.com/synthetichealth/synthea","value":"d73bb536-cc5c-0005-2da0-ea3a98435f5f"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"MR","display":"Medical Record Number"}],"text":"Medical Record Number"},"system":"http://hospital.smarthealthit.org","value":"d73bb536-cc5c-0005-2da0-ea3a98435f5f"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"SS","display":"Social Security Number"}],"text":"Social Security Number"},"system":"http://hl7.org/fhir/sid/us-ssn","value":"999-37-1674"}],"name":[{"use":"official","family":"Kreiger457","given":["Rocio28","Millie392"]}],"telecom":[{"system":"phone","value":"555-555-9540","use":"home"}],"gender":"female","birthDate":"2016-04-24","address":[{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/geolocation","extension":[{"url":"latitude","valueDecimal":42.29177689581121},{"url":"longitude","valueDecimal":-73.21515487480134}]}],"line":["849 Raynor Ville Suite 32"],"city":"Lee","state":"MA","postalCode":"01238","country":"US"}],"maritalStatus":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v3-MaritalStatus","code":"S","display":"Never Married"}],"text":"Never Married"},"multipleBirthInteger":3,"communication":[{"language":{"coding":[{"system":"urn:ietf:bcp:47","code":"en-US","display":"English (United States)"}],"text":"English (United States)"}}]} +{"resourceType":"Patient","id":"0bce45a9-7256-afc7-3240-943df5b25f65","meta":{"profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"]},"text":{"status":"generated","div":"
Generated by Synthea.Version identifier: master-branch-latest\n . Person seed: -7731425587518397889 Population seed: 1710790370898
"},"extension":[{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2106-3","display":"White"}},{"url":"text","valueString":"White"}]},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2186-5","display":"Not Hispanic or Latino"}},{"url":"text","valueString":"Not Hispanic or Latino"}]},{"url":"http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName","valueString":"Yuko264 Casper496"},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex","valueCode":"M"},{"url":"http://hl7.org/fhir/StructureDefinition/patient-birthPlace","valueAddress":{"city":"Holden","state":"Massachusetts","country":"US"}},{"url":"http://synthetichealth.github.io/synthea/disability-adjusted-life-years","valueDecimal":0},{"url":"http://synthetichealth.github.io/synthea/quality-adjusted-life-years","valueDecimal":33}],"identifier":[{"system":"https://github.com/synthetichealth/synthea","value":"0bce45a9-7256-afc7-3240-943df5b25f65"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"MR","display":"Medical Record Number"}],"text":"Medical Record Number"},"system":"http://hospital.smarthealthit.org","value":"0bce45a9-7256-afc7-3240-943df5b25f65"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"SS","display":"Social Security Number"}],"text":"Social Security Number"},"system":"http://hl7.org/fhir/sid/us-ssn","value":"999-40-3043"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"DL","display":"Driver's license number"}],"text":"Driver's license number"},"system":"urn:oid:2.16.840.1.113883.4.3.25","value":"S99916030"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"PPN","display":"Passport Number"}],"text":"Passport Number"},"system":"http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber","value":"X705176X"}],"name":[{"use":"official","family":"Hegmann834","given":["Fred155","Darrin898"],"prefix":["Mr."]}],"telecom":[{"system":"phone","value":"555-482-7305","use":"home"}],"gender":"male","birthDate":"1990-04-07","address":[{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/geolocation","extension":[{"url":"latitude","valueDecimal":41.561298805547864},{"url":"longitude","valueDecimal":-70.93955416457635}]}],"line":["1080 Erdman View"],"city":"New Bedford","state":"MA","postalCode":"02747","country":"US"}],"maritalStatus":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v3-MaritalStatus","code":"M","display":"Married"}],"text":"Married"},"multipleBirthBoolean":false,"communication":[{"language":{"coding":[{"system":"urn:ietf:bcp:47","code":"en-US","display":"English (United States)"}],"text":"English (United States)"}}]} +{"resourceType":"Patient","id":"396a53c6-cb9a-7dbb-f770-563527ac5975","meta":{"profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"]},"text":{"status":"generated","div":"
Generated by Synthea.Version identifier: master-branch-latest\n . Person seed: 3929086393522110073 Population seed: 1710790370898
"},"extension":[{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2106-3","display":"White"}},{"url":"text","valueString":"White"}]},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity","extension":[{"url":"ombCategory","valueCoding":{"system":"urn:oid:2.16.840.1.113883.6.238","code":"2186-5","display":"Not Hispanic or Latino"}},{"url":"text","valueString":"Not Hispanic or Latino"}]},{"url":"http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName","valueString":"Krystin400 Mitchell808"},{"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex","valueCode":"F"},{"url":"http://hl7.org/fhir/StructureDefinition/patient-birthPlace","valueAddress":{"city":"Walpole","state":"Massachusetts","country":"US"}},{"url":"http://synthetichealth.github.io/synthea/disability-adjusted-life-years","valueDecimal":0},{"url":"http://synthetichealth.github.io/synthea/quality-adjusted-life-years","valueDecimal":33}],"identifier":[{"system":"https://github.com/synthetichealth/synthea","value":"396a53c6-cb9a-7dbb-f770-563527ac5975"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"MR","display":"Medical Record Number"}],"text":"Medical Record Number"},"system":"http://hospital.smarthealthit.org","value":"396a53c6-cb9a-7dbb-f770-563527ac5975"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"SS","display":"Social Security Number"}],"text":"Social Security Number"},"system":"http://hl7.org/fhir/sid/us-ssn","value":"999-39-5818"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"DL","display":"Driver's license number"}],"text":"Driver's license number"},"system":"urn:oid:2.16.840.1.113883.4.3.25","value":"S99919300"},{"type":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0203","code":"PPN","display":"Passport Number"}],"text":"Passport Number"},"system":"http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber","value":"X25457534X"}],"name":[{"use":"official","family":"Rutherford999","given":["Veola813","Barbra486"],"prefix":["Mrs."]},{"use":"maiden","family":"Abernathy524","given":["Veola813","Barbra486"],"prefix":["Mrs."]}],"telecom":[{"system":"phone","value":"555-222-3800","use":"home"}],"gender":"female","birthDate":"1990-05-30","address":[{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/geolocation","extension":[{"url":"latitude","valueDecimal":42.36109533926901},{"url":"longitude","valueDecimal":-73.30970954937412}]}],"line":["1066 Murazik Divide Unit 27"],"city":"Richmond","state":"MA","postalCode":"00000","country":"US"}],"maritalStatus":{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v3-MaritalStatus","code":"M","display":"Married"}],"text":"Married"},"multipleBirthBoolean":false,"communication":[{"language":{"coding":[{"system":"urn:ietf:bcp:47","code":"en-US","display":"English (United States)"}],"text":"English (United States)"}}]} \ No newline at end of file diff --git a/regulation/cms-0057-f/bulk-export-client-service/tests/test_service.bal b/regulation/cms-0057-f/bulk-export-client-service/tests/test_service.bal new file mode 100644 index 0000000..66d2a74 --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/tests/test_service.bal @@ -0,0 +1,54 @@ +import ballerina/http; +import ballerina/io; +import ballerina/test; +import ballerinax/health.fhir.r4; + +@test:Config { + enable: true +} +function testGetFileAsStream() returns error? { + // Mock HTTP Client + http:Client mockClient = test:mock(http:Client); + + // Test case 1: Successful response + http:Response mockResponse1 = new; + mockResponse1.statusCode = 200; + stream fileReadBlocksAsStream = check io:fileReadBlocksAsStream("tests/resources/exportedData.json", 1024); + mockResponse1.setByteStream(fileReadBlocksAsStream); + test:prepare(mockClient).when("get").withArguments("/").thenReturn(mockResponse1); + + stream|error? result1 = getFileAsStream("http://example.com", mockClient); + if (result1 is stream) { + if fileReadBlocksAsStream is stream { + if fileReadBlocksAsStream.toString() == result1.toString() { + test:assertTrue(true, "Expected a byte stream, but got an empty string"); + } + } + } else { + test:assertFail("Expected a byte stream, but got an error or null"); + } + + // todo: Test case 2: Error response +} + +@test:Config { + enable: true +} +function testCreateOperationOutcome() { + // Test case 1: Valid input + r4:OperationOutcome result1 = createOpereationOutcome("warning", "invalid", "Invalid input provided"); + test:assertEquals(result1.issue[0].severity, "warning"); + test:assertEquals(result1.issue[0].code, "invalid"); + test:assertEquals(result1.issue[0].diagnostics, "Invalid input provided"); + + r4:OperationOutcome result2 = createOpereationOutcome("invalid_severity", "invalid", "Test message"); + test:assertEquals(result2.issue[0].severity, "error"); + test:assertEquals(result2.issue[0].code, "exception"); + test:assertEquals(result2.issue[0].diagnostics, "Error occurred while creating the operation outcome. Error in severity type"); + + // Test case 3: Empty inputs + r4:OperationOutcome result3 = createOpereationOutcome("", "", ""); + test:assertEquals(result3.issue[0].severity, "error"); + test:assertEquals(result3.issue[0].code, "exception"); + test:assertEquals(result3.issue[0].diagnostics, "Error occurred while creating the operation outcome. Error in severity type"); +} diff --git a/regulation/cms-0057-f/bulk-export-client-service/tests/test_storage.bal b/regulation/cms-0057-f/bulk-export-client-service/tests/test_storage.bal new file mode 100644 index 0000000..c5fb97e --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/tests/test_storage.bal @@ -0,0 +1,66 @@ +import ballerina/test; +import ballerina/time; + +@test:Config { + enable: true +} +function testAddExportTaskToMemory() { + map testMap = {}; + ExportTask testTask = { + id: "test-id", + lastUpdated: time:utcNow(), + lastStatus: "Pending", + pollingEvents: [] + }; + + boolean result = addExportTasktoMemory(testMap, testTask); + test:assertTrue(result); + test:assertTrue(testMap.hasKey("test-id")); + test:assertEquals(testMap.get("test-id").id, "test-id"); + test:assertFalse(testMap.get("test-id").lastUpdated < testTask.lastUpdated); +} + +@test:Config { + enable: true +} +function testAddPollingEventToMemory() { + ExportTask testTask = { + id: "test-id", + lastUpdated: time:utcNow(), + lastStatus: "Pending", + pollingEvents: [] + }; + map testMap = {"test-id": testTask}; + + PollingEvent testEvent = { + id: "test-id", + exportStatus: "In-progress" + , + eventStatus: "" + }; + + boolean result = addPollingEventToMemory(testMap, testEvent); + test:assertTrue(result); + test:assertEquals(testMap.get("test-id").lastStatus, "In-progress"); + test:assertEquals(testMap.get("test-id").pollingEvents.length(), 1); + test:assertEquals(testMap.get("test-id").pollingEvents[0], testEvent); + test:assertFalse(testMap.get("test-id").lastUpdated < testTask.lastUpdated); +} + +@test:Config { + enable: true +} +function testUpdateExportTaskStatusInMemory() { + ExportTask testTask = { + id: "test-id", + lastUpdated: time:utcNow(), + lastStatus: "Pending", + pollingEvents: [] + }; + map testMap = {"test-id": testTask}; + + boolean result = updateExportTaskStatusInMemory(testMap, "test-id", "Completed"); + test:assertTrue(result); + test:assertEquals(testMap.get("test-id").lastStatus, "Completed"); + test:assertFalse(testMap.get("test-id").lastUpdated < testTask.lastUpdated); +} diff --git a/regulation/cms-0057-f/bulk-export-client-service/utils.bal b/regulation/cms-0057-f/bulk-export-client-service/utils.bal new file mode 100644 index 0000000..97f79ac --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/utils.bal @@ -0,0 +1,271 @@ +import ballerina/ftp; +import ballerina/http; +import ballerina/io; +import ballerina/log; +import ballerina/task; +import ballerinax/health.fhir.r4; +import ballerinax/health.fhir.r4.international401; + +public isolated function executeJob(PollingTask job, decimal interval) returns task:JobId|error? { + + // Implement the job execution logic here + task:JobId id = check task:scheduleJobRecurByFrequency(job, interval); + job.setId(id); + return id; +} + +public isolated function unscheduleJob(task:JobId id) returns error? { + + // Implement the job termination logic here + log:printDebug("Unscheduling the job.", Jobid = id); + task:Error? unscheduleJob = task:unscheduleJob(id); + if unscheduleJob is task:Error { + log:printError("Error occurred while unscheduling the job.", unscheduleJob); + } + return null; +} + +public isolated function getFileAsStream(string downloadLink, http:Client statusClientV2) returns stream|error? { + + http:Response|http:ClientError statusResponse = statusClientV2->get("/"); + if statusResponse is http:Response { + int status = statusResponse.statusCode; + if status == 200 { + return check statusResponse.getByteStream(); + } else { + log:printError("Error occurred while getting the status."); + } + } else { + log:printError("Error occurred while getting the status.", statusResponse); + } + return null; +} + +public isolated function saveFileInFS(string downloadLink, string fileName) returns error? { + + http:Client statusClientV2 = check new (downloadLink); + stream streamer = check getFileAsStream(downloadLink, statusClientV2) ?: new (); + + check io:fileWriteBlocksFromStream(fileName, streamer); + check streamer.close(); + log:printDebug(string `Successfully downloaded the file. File name: ${fileName}`); +} + +public isolated function sendFileFromFSToFTP(FtpServerConfig config, string sourcePath, string fileName) returns error? { + // Implement the FTP server logic here. + ftp:Client fileClient = check new ({ + host: config.host, + auth: { + credentials: { + username: config.username, + password: config.password + } + } + }); + stream fileStream + = check io:fileReadBlocksAsStream(sourcePath, 1024); + check fileClient->put(string `${config.directory}/${fileName}`, fileStream); + check fileStream.close(); +} + +public isolated function downloadFiles(json exportSummary, string exportId) returns error? { + + ExportSummary exportSummary1 = check exportSummary.cloneWithType(ExportSummary); + + foreach OutputFile item in exportSummary1.output { + log:printDebug("Downloading the file.", url = item.url); + error? downloadFileResult = saveFileInFS(item.url, string `${clientServiceConfig.targetDirectory}/${item.'type}-exported.ndjson`); + if downloadFileResult is error { + log:printError("Error occurred while downloading the file.", downloadFileResult); + } + if ftpServerConfig.enabled { + // download the file to the FTP server + // implement the FTP server logic + error? uploadFileResult = sendFileFromFSToFTP(ftpServerConfig, string `${clientServiceConfig.targetDirectory}/${item.'type}-exported.ndjson`, string `${item.'type}-exported.ndjson`); + if uploadFileResult is error { + log:printError("Error occurred while sending the file to ftp.", downloadFileResult); + + } + } + } + lock { + boolean _ = updateExportTaskStatusInMemory(taskMap = exportTasks, exportTaskId = exportId, newStatus = "Downloaded"); + } + log:printInfo("All files downloaded successfully."); + return null; +} + +public isolated function createOpereationOutcome(string severity, string code, string message) returns r4:OperationOutcome { + r4:OperationOutcomeIssueSeverity severityType; + do { + severityType = check severity.cloneWithType(r4:OperationOutcomeIssueSeverity); + } on fail var e { + log:printError("Error occurred while creating the operation outcome. Error in severity type", e); + r4:OperationOutcome operationOutcomeError = { + issue: [ + {severity: "error", code: "exception", diagnostics: "Error occurred while creating the operation outcome. Error in severity type"} + ] + }; + return operationOutcomeError; + + } + r4:OperationOutcome operationOutcome = { + issue: [ + {severity: severityType, code: code, diagnostics: message} + ] + }; + return operationOutcome; +} + +public isolated function createR4Parameters(map parameters) returns international401:Parameters { + international401:Parameters r4Parameters = {'parameter: []}; + international401:ParametersParameter[] paramsArr = []; + foreach string key in parameters.keys() { + international401:ParametersParameter parameterToAdd = { + name: key, + valueString: parameters.get(key) + }; + paramsArr.push(parameterToAdd); + } + r4Parameters.'parameter = paramsArr; + return r4Parameters; +} + +public isolated function populateParamsResource(MatchedPatient[] matchedPatients, string? _outputFormat, string? _since, string? _type) returns international401:Parameters { + + international401:Parameters r4Parameters = {'parameter: []}; + international401:ParametersParameter[] paramsArr = []; + + if matchedPatients != [] { + foreach MatchedPatient patient in matchedPatients { + string patientReference = string `Patient/${patient.id}`; + r4:Reference patientRef = {reference: patientReference}; + paramsArr.push({name: "patient", valueReference: patientRef}); + } + } + + if _outputFormat is string { + paramsArr.push({name: "_outputFormat", valueString: _outputFormat}); + + } + if _since is string { + paramsArr.push({name: "_since", valueInstant: _since}); + } + if _type is string { + paramsArr.push({name: "_type", valueString: _type}); + } + + r4Parameters.'parameter = paramsArr; + return r4Parameters; +} + +public isolated function populateQueryString(string? _outputFormat, string? _since, string? _type) returns string { + + string queryString = ""; + + if _outputFormat is string { + queryString = string `?_outputFormat=${_outputFormat}`; + } + if _since is string { + queryString = addQueryParam(queryString, "_since", _since); + } + if _type is string { + queryString = addQueryParam(queryString, "_type", _type); + } + return queryString; +} + +public isolated function addQueryParam(string queryString, string key, string value) returns string { + if queryString == "" { + return string `?${key}=${value}`; + } else { + return string `${queryString}&${key}=${value}`; + } +} + +public class PollingTask { + + *task:Job; + string exportId; + string lastStatus; + string location; + task:JobId jobId = {id: 0}; + + public function execute() { + do { + http:Client statusClientV2 = check new (self.location); + + log:printDebug("Polling the export task status.", exportId = self.exportId); + if self.lastStatus == "In-progress" { + // update the status + // update the export task + + http:Response|http:ClientError statusResponse; + statusResponse = statusClientV2->/; + addPollingEvent addPollingEventFuntion = addPollingEventToMemory; + if statusResponse is http:Response { + int status = statusResponse.statusCode; + if status == 200 { + // update the status + // extract payload + // unschedule the job + self.setLastStaus("Completed"); + lock { + boolean _ = updateExportTaskStatusInMemory(taskMap = exportTasks, exportTaskId = self.exportId, newStatus = "Export Completed. Downloading files."); + } + json payload = check statusResponse.getJsonPayload(); + log:printDebug("Export task completed.", exportId = self.exportId, payload = payload); + error? unscheduleJobResult = unscheduleJob(self.jobId); + if unscheduleJobResult is error { + log:printError("Error occurred while unscheduling the job.", unscheduleJobResult); + } + + // download the files + error? downloadFilesResult = downloadFiles(payload, self.exportId); + if downloadFilesResult is error { + log:printError("Error in downloading files", downloadFilesResult); + } + + } else if status == 202 { + // update the status + log:printDebug("Export task in-progress.", exportId = self.exportId); + string progress = check statusResponse.getHeader("X-Progress"); + PollingEvent pollingEvent = {id: self.exportId, eventStatus: "Success", exportStatus: progress}; + + lock { + // persisting event + boolean _ = addPollingEventFuntion(exportTasks, pollingEvent.clone()); + } + self.setLastStaus("In-progress"); + } + } else { + log:printError("Error occurred while getting the status.", statusResponse); + lock { + // statusResponse + PollingEvent pollingEvent = {id: self.exportId, eventStatus: "Failed"}; + boolean _ = addPollingEventFuntion(exportTasks, pollingEvent.cloneReadOnly()); + } + } + } else if self.lastStatus == "Completed" { + // This is a rare occurance; if the job is not unscheduled properly, it will keep polling the status. + log:printDebug("Export task completed.", exportId = self.exportId); + } + } on fail var e { + log:printError("Error occurred while polling the export task status.", e); + } + } + + isolated function init(string exportId, string location, string lastStatus = "In-progress") { + self.exportId = exportId; + self.lastStatus = lastStatus; + self.location = location; + } + + public function setLastStaus(string newStatus) { + self.lastStatus = newStatus; + } + + public isolated function setId(task:JobId jobId) { + self.jobId = jobId; + } +} diff --git a/regulation/cms-0057-f/member-match-service/.gitignore b/regulation/cms-0057-f/member-match-service/.gitignore new file mode 100644 index 0000000..7512ebe --- /dev/null +++ b/regulation/cms-0057-f/member-match-service/.gitignore @@ -0,0 +1,3 @@ +target +generated +Config.toml diff --git a/regulation/cms-0057-f/member-match-service/Ballerina.toml b/regulation/cms-0057-f/member-match-service/Ballerina.toml new file mode 100644 index 0000000..d36488a --- /dev/null +++ b/regulation/cms-0057-f/member-match-service/Ballerina.toml @@ -0,0 +1,20 @@ +[package] +org = "wso2" +name = "MemberMatchService" +version = "0.1.0" +distribution = "2201.8.6" + +[build-options] +observabilityIncluded = true + +[[dependency]] +org="ballerinax" +name="health.fhir.r4.davincihrex100" +version="1.0.0" +repository="local" + +[[dependency]] +org="ballerinax" +name="health.fhirr4" +version="1.3.3" +repository="local" diff --git a/regulation/cms-0057-f/member-match-service/Dependencies.toml b/regulation/cms-0057-f/member-match-service/Dependencies.toml new file mode 100644 index 0000000..ee01e90 --- /dev/null +++ b/regulation/cms-0057-f/member-match-service/Dependencies.toml @@ -0,0 +1,449 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.8.6" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.8.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.6.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.10.15" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "http", moduleName = "http"}, + {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.6.1" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "jwt" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] + +[[package]] +org = "ballerina" +name = "mime" +version = "2.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.2.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.4.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.4.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "uuid" +version = "1.7.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerinai" +name = "observe" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerinai", packageName = "observe", moduleName = "observe"} +] + +[[package]] +org = "ballerinax" +name = "health.base" +version = "1.0.3" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "uuid"} +] + +[[package]] +org = "ballerinax" +name = "health.clients.fhir" +version = "2.0.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "health.base"} +] +modules = [ + {org = "ballerinax", packageName = "health.clients.fhir", moduleName = "health.clients.fhir"} +] + +[[package]] +org = "ballerinax" +name = "health.fhir.r4" +version = "5.1.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"}, + {org = "ballerina", name = "uuid"}, + {org = "ballerinax", name = "health.base"} +] +modules = [ + {org = "ballerinax", packageName = "health.fhir.r4", moduleName = "health.fhir.r4"} +] + +[[package]] +org = "ballerinax" +name = "health.fhir.r4.davincihrex100" +version = "1.0.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "log"}, + {org = "ballerinax", name = "health.clients.fhir"}, + {org = "ballerinax", name = "health.fhir.r4"}, + {org = "ballerinax", name = "health.fhir.r4.uscore501"} +] +modules = [ + {org = "ballerinax", packageName = "health.fhir.r4.davincihrex100", moduleName = "health.fhir.r4.davincihrex100"} +] + +[[package]] +org = "ballerinax" +name = "health.fhir.r4.international401" +version = "2.1.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "log"}, + {org = "ballerinax", name = "health.fhir.r4"} +] + +[[package]] +org = "ballerinax" +name = "health.fhir.r4.parser" +version = "5.1.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "log"}, + {org = "ballerinax", name = "health.fhir.r4"}, + {org = "ballerinax", name = "health.fhir.r4.international401"} +] + +[[package]] +org = "ballerinax" +name = "health.fhir.r4.uscore501" +version = "1.3.4" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "log"}, + {org = "ballerinax", name = "health.fhir.r4"} +] +modules = [ + {org = "ballerinax", packageName = "health.fhir.r4.uscore501", moduleName = "health.fhir.r4.uscore501"} +] + +[[package]] +org = "ballerinax" +name = "health.fhir.r4.validator" +version = "4.2.2" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "log"}, + {org = "ballerinax", name = "health.fhir.r4"}, + {org = "ballerinax", name = "health.fhir.r4.international401"}, + {org = "ballerinax", name = "health.fhir.r4.parser"} +] +modules = [ + {org = "ballerinax", packageName = "health.fhir.r4.validator", moduleName = "health.fhir.r4.validator"} +] + +[[package]] +org = "ballerinax" +name = "health.fhirr4" +version = "1.3.3" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "log"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "health.fhir.r4"}, + {org = "ballerinax", name = "health.fhir.r4.international401"}, + {org = "ballerinax", name = "health.fhir.r4.parser"} +] +modules = [ + {org = "ballerinax", packageName = "health.fhirr4", moduleName = "health.fhirr4"} +] + +[[package]] +org = "wso2" +name = "MemberMatchService" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "health.clients.fhir"}, + {org = "ballerinax", name = "health.fhir.r4"}, + {org = "ballerinax", name = "health.fhir.r4.davincihrex100"}, + {org = "ballerinax", name = "health.fhir.r4.uscore501"}, + {org = "ballerinax", name = "health.fhir.r4.validator"}, + {org = "ballerinax", name = "health.fhirr4"} +] +modules = [ + {org = "wso2", packageName = "MemberMatchService", moduleName = "MemberMatchService"} +] + diff --git a/regulation/cms-0057-f/member-match-service/api_config.bal b/regulation/cms-0057-f/member-match-service/api_config.bal new file mode 100644 index 0000000..992689d --- /dev/null +++ b/regulation/cms-0057-f/member-match-service/api_config.bal @@ -0,0 +1,54 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). All Rights Reserved. +// This software is the property of WSO2 LLC. and its suppliers, if any. +// Dissemination of any information or reproduction of any material contained +// herein is strictly forbidden, unless permitted by WSO2 in accordance with +// the WSO2 Software License available at: https://wso2.com/licenses/eula/3.2 +// For specific language governing the permissions and limitations under +// this license, please see the license as well as any agreement you’ve +// entered into with WSO2 governing the purchase of this software and any +// associated services. +// +// +// AUTO-GENERATED FILE. +// +// This file is auto-generated by Ballerina. +// Developers are allowed to modify this file as per the requirement. +import ballerinax/health.fhir.r4; + +final r4:ResourceAPIConfig apiConfig = { + resourceType: "Patient", + profiles: [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" + ], + defaultProfile: (), + searchParameters: [], + operations: [ + { + name: "member-match", + active: true, + parameters: [ + { + name: "MemberPatient", + active: true, + min: 1 + }, + { + name: "Consent", + active: true, + min: 1 + }, + { + name: "CoverageToMatch", + active: true, + min: 1 + }, + { + name: "CoverageToLink", + active: true + } + ] + } + ], + serverConfig: (), + authzConfig: () +}; diff --git a/regulation/cms-0057-f/member-match-service/custom_matcher.bal b/regulation/cms-0057-f/member-match-service/custom_matcher.bal new file mode 100644 index 0000000..c67dfe4 --- /dev/null +++ b/regulation/cms-0057-f/member-match-service/custom_matcher.bal @@ -0,0 +1,11 @@ +import ballerinax/health.fhir.r4; +import ballerinax/health.fhir.r4.davincihrex100; + +public isolated class DemoFHIRMemberMatcher { + *davincihrex100:MemberMatcher; + + public isolated function matchMember(anydata memberMatchResources) returns davincihrex100:MemberIdentifier|r4:FHIRError { + // Hardcoded values for the sake of the example + return "patinetID"; + } +} \ No newline at end of file diff --git a/regulation/cms-0057-f/member-match-service/service.bal b/regulation/cms-0057-f/member-match-service/service.bal new file mode 100644 index 0000000..8ca1bc1 --- /dev/null +++ b/regulation/cms-0057-f/member-match-service/service.bal @@ -0,0 +1,95 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +import ballerinax/health.clients.fhir; +import ballerinax/health.fhir.r4; +import ballerinax/health.fhirr4; +import ballerinax/health.fhir.r4.davincihrex100; + +// FHIR repository configs +configurable string fhirRepositoryUrl = ?; +configurable AuthConfig? fhirRepositoryAuthConfig = (); + +// Consent service configs +configurable string consentServiceUrl = ?; +configurable AuthConfig? consentServiceAuthConfig = (); +configurable map? consentServiceRequestHeaders = (); + +// Coverage service configs +configurable string coverageServiceUrl = ""; +configurable AuthConfig? coverageServiceAuthConfig = (); +configurable map? coverageServiceRequestHeaders = (); + +final fhir:FHIRConnectorConfig fhirClientConfig = { + baseURL: fhirRepositoryUrl, + mimeType: fhir:FHIR_JSON, + authConfig: getClientAuthConfig(fhirRepositoryAuthConfig) +}; + +final davincihrex100:ExternalServiceConfig consentServiceConfig = { + url: consentServiceUrl, + requestHeaders: consentServiceRequestHeaders, + clientConfig: { + auth: getClientAuthConfig(consentServiceAuthConfig) + } +}; + +final davincihrex100:ExternalServiceConfig coverageServiceConfig = { + url: coverageServiceUrl, + requestHeaders: coverageServiceRequestHeaders, + clientConfig: { + auth: getClientAuthConfig(coverageServiceAuthConfig) + } +}; + +final davincihrex100:MatcherConfig matcherConfig = { + fhirClientConfig: fhirClientConfig, + consentServiceConfig: consentServiceConfig, + coverageServiceConfig: coverageServiceConfig +}; + +// FHIR member matcher instance +final davincihrex100:FhirMemberMatcher fhirMemberMatcher = check new (matcherConfig, ()); + +// final DemoFHIRMemberMatcher fhirMemberMatcher = check new (); + +service / on new fhirr4:Listener(9095, apiConfig) { + isolated resource function post fhir/r4/Patient/\$member\-match(r4:FHIRContext context, + davincihrex100:HRexMemberMatchRequestParameters parameters) + returns davincihrex100:HRexMemberMatchResponseParameters|r4:FHIRError { + // Validate and extract resources from the request parameters + davincihrex100:MemberMatchResources memberMatchResources = + check validateAndExtractMemberMatchResources(parameters); + + // Match member + davincihrex100:MemberIdentifier memberIdentifier = check fhirMemberMatcher.matchMember(memberMatchResources); + + // Member match response profile: + // https://hl7.org/fhir/us/davinci-hrex/StructureDefinition-hrex-parameters-member-match-out.html + return { + 'parameter: { + name: "MemberIdentifier", + valueIdentifier: { + 'type: { + coding: [ + { + system: "http://terminology.hl7.org/3.1.0/CodeSystem-v2-0203.html", + code: "MB" + } + ] + }, + value: memberIdentifier + } + } + }; + } +} diff --git a/regulation/cms-0057-f/member-match-service/service_utils.bal b/regulation/cms-0057-f/member-match-service/service_utils.bal new file mode 100644 index 0000000..bf60146 --- /dev/null +++ b/regulation/cms-0057-f/member-match-service/service_utils.bal @@ -0,0 +1,140 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +import ballerina/http; +import ballerinax/health.fhir.r4; +import ballerinax/health.fhir.r4.davincihrex100; +import ballerinax/health.fhir.r4.uscore501; +import ballerinax/health.fhir.r4.validator; + +# Input parameters of the member match operation. +enum MemberMatchParameter { + MEMBER_PATIENT = "MemberPatient", + CONSENT = "Consent", + COVERAGE_TO_MATCH = "CoverageToMatch", + COVERAGE_TO_LINK = "CoverageToLink" +}; + +# Map of `ParameterInfo` to hold information about member match parameters. +final map & readonly MEMBER_MATCH_PARAMETERS_INFO = { + [MEMBER_PATIENT] : {profile: "USCorePatientProfile", typeDesc: uscore501:USCorePatientProfile}, + [CONSENT] : {profile: "HRexConsent", typeDesc: davincihrex100:HRexConsent}, + [COVERAGE_TO_MATCH] : {profile: "HRexCoverage", typeDesc: davincihrex100:HRexCoverage}, + [COVERAGE_TO_LINK] : {profile: "HrexCoverage", typeDesc: davincihrex100:HRexCoverage} +}; + +# Validates and extracts the parameter resources from member match request parameters. +# +# + requestParams - The `HRexMemberMatchRequestParameters` containing the parameters +# + return - A `MemberMatchResources` record containing the extracted resources if validation is successful, +# or a `FHIRError` if there's an error in validating the resources +isolated function validateAndExtractMemberMatchResources(davincihrex100:HRexMemberMatchRequestParameters requestParams) + returns davincihrex100:MemberMatchResources|r4:FHIRError { + map processedResources = {}; + + foreach string param in MEMBER_MATCH_PARAMETERS_INFO.keys() { + anydata? 'resource = check validateAndExtractParamResource(requestParams, param, + MEMBER_MATCH_PARAMETERS_INFO.get(param)); + if param != COVERAGE_TO_LINK && 'resource == () { // CoverageToLink is optional + return createMissingMandatoryParamError(param); + } + processedResources[param] = 'resource; + } + + return { + memberPatient: processedResources[MEMBER_PATIENT], + consent: processedResources[CONSENT], + coverageToMatch: processedResources[COVERAGE_TO_MATCH], + coverageToLink: processedResources[COVERAGE_TO_LINK] + }; +} + +# Validates and extracts a specific parameter resource from the member match request parameters. +# +# + requestParams - The `HRexMemberMatchRequestParameters` containing the parameters +# + paramName - The name of the parameter to be validated and extracted +# + paramInfo - The `ParameterInfo` of the parameter +# + return - The validated and extracted parameter as `anydata` if successful, a `FHIRError` if validation fails, or +# `()` if the parameter is not present +isolated function validateAndExtractParamResource(davincihrex100:HRexMemberMatchRequestParameters requestParams, + string paramName, ParameterInfo paramInfo) returns anydata|r4:FHIRError? { + r4:Resource? paramResource = getParamResource(requestParams, paramName); + if paramResource == () { + return; + } + + anydata|error 'resource = paramResource.cloneWithType(paramInfo.typeDesc); + if 'resource is error { + return createInvalidParamTypeError(paramName, paramInfo.profile); + } + + // Validate the resource + r4:FHIRValidationError? validationRes = validator:validate('resource, paramInfo.typeDesc); + if validationRes is r4:FHIRValidationError { + return createInvalidParamTypeError(paramName, paramInfo.profile); + } + + return 'resource; +} + +# Retrieves a specific FHIR resource associated with a parameter from the member match request parameters. +# +# + requestParams - The `HRexMemberMatchRequestParameters` containing the parameters +# + 'parameter - The name of the parameter whose resource is to be retrieved +# + return - The FHIR `r4:Resource` associated with the specified parameter if found, or `()` if not found +isolated function getParamResource(davincihrex100:HRexMemberMatchRequestParameters requestParams, string 'parameter) + returns r4:Resource? { + foreach davincihrex100:HRexMemberMatchRequestParametersParameter param in requestParams.'parameter { + if param.'name == 'parameter { + return param?.'resource; + } + } + return; +} + +# Constructs an HTTP client authentication configuration from a given `AuthConfig`. +# +# + authConfig - An optional `AuthConfig` containing the OAuth2 authentication details +# + return - An `http:ClientAuthConfig` if `authConfig` is provided, otherwise `()` +isolated function getClientAuthConfig(AuthConfig? authConfig) returns http:ClientAuthConfig? { + if authConfig != () { + return { + tokenUrl: authConfig.tokenUrl, + clientId: authConfig.clientId, + clientSecret: authConfig.clientSecret + }; + } + return; +} + +# Creates a `FHIRError` indicating an invalid parameter type error. +# +# + paramName - The name of the parameter that failed validation +# + expectedType - The expected data type of the parameter +# + return - A `FHIRError` with details about the invalid parameter type +isolated function createInvalidParamTypeError(string paramName, string expectedType) returns r4:FHIRError { + string message = "Invalid parameter"; + string diagnostic = "Parameter \"" + paramName + "\" must be a valid \"" + expectedType + "\" type"; + return r4:createFHIRError(message, r4:ERROR, r4:INVALID_VALUE, diagnostic = diagnostic, + httpStatusCode = http:STATUS_BAD_REQUEST); +} + +# Creates a `FHIRError` for a missing mandatory parameter in FHIR operations. +# +# + paramName - The name of the missing mandatory parameter +# + return - A `FHIRError` with details about the missing mandatory parameter +isolated function createMissingMandatoryParamError(string paramName) returns r4:FHIRError { + string message = "Missing mandatory parameter"; + string diagnostic = "Mandatory parameter \"" + paramName + "\" is missing"; + return r4:createFHIRError(message, r4:ERROR, r4:INVALID_REQUIRED, diagnostic = diagnostic, + httpStatusCode = http:STATUS_BAD_REQUEST); +} diff --git a/regulation/cms-0057-f/member-match-service/types.bal b/regulation/cms-0057-f/member-match-service/types.bal new file mode 100644 index 0000000..ec4f03e --- /dev/null +++ b/regulation/cms-0057-f/member-match-service/types.bal @@ -0,0 +1,35 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +# Holds information for OAuth2 authentication. +# +# + tokenUrl - Token URL of the token endpoint +# + clientId - Client ID of the client authentication +# + clientSecret - Client secret of the client authentication +type AuthConfig record {| + string tokenUrl; + string clientId; + string clientSecret; +|}; + +# Holds member match parameter information. +# +# + profile - The parameter profile +# + typeDesc - The Ballerina type descriptor for the parameter +type ParameterInfo record {| + readonly string profile; + readonly typedesc typeDesc; +|}; From c8d080fd92b45eb5549c0419dcb689c274f01d22 Mon Sep 17 00:00:00 2001 From: isuruh15 Date: Tue, 24 Sep 2024 11:15:16 +0530 Subject: [PATCH 02/10] Add webapp and backend for demo restucture code --- .../bulk-export-client-service/records.bal | 9 +++++++ .../bulk-export-client-service/service.bal | 26 +++++++++---------- .../bulk-export-client-service/utils.bal | 6 ++--- .../member-match-service/service.bal | 3 +++ 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/regulation/cms-0057-f/bulk-export-client-service/records.bal b/regulation/cms-0057-f/bulk-export-client-service/records.bal index e2de8b0..570a4de 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/records.bal +++ b/regulation/cms-0057-f/bulk-export-client-service/records.bal @@ -10,6 +10,15 @@ public type BulkExportServerConfig record {| decimal defaultIntervalInSec; |}; +public type TargetServerConfig record {| + string 'type; + string host; + int port; + string username; + string password; + string directory; + |}; + // have a generic config for source server and target server for FHIR cases // check Ballerina FTP client for the FTP server config diff --git a/regulation/cms-0057-f/bulk-export-client-service/service.bal b/regulation/cms-0057-f/bulk-export-client-service/service.bal index 0fefff1..4cc578a 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/service.bal +++ b/regulation/cms-0057-f/bulk-export-client-service/service.bal @@ -4,18 +4,18 @@ import ballerina/task; import ballerina/uuid; import ballerinax/health.fhir.r4.international401; -configurable BulkExportServerConfig exportSeverConfig = ?; +configurable BulkExportServerConfig sourceServerConfig = ?; configurable BulkExportClientConfig clientServiceConfig = ?; -configurable FtpServerConfig ftpServerConfig = ?; +configurable TargetServerConfig targetServerConfig = ?; http:OAuth2ClientCredentialsGrantConfig config = { - tokenUrl: exportSeverConfig.tokenUrl, - clientId: exportSeverConfig.clientId, - clientSecret: exportSeverConfig.clientSecret, - scopes: exportSeverConfig.scopes + tokenUrl: sourceServerConfig.tokenUrl, + clientId: sourceServerConfig.clientId, + clientSecret: sourceServerConfig.clientSecret, + scopes: sourceServerConfig.scopes }; -isolated http:Client statusClient = check new (exportSeverConfig.baseUrl); +isolated http:Client statusClient = check new (sourceServerConfig.baseUrl); service /trigger on new http:Listener(clientServiceConfig.port) { @@ -23,11 +23,11 @@ service /trigger on new http:Listener(clientServiceConfig.port) { if clientServiceConfig.authEnabled { lock { - statusClient = check new (exportSeverConfig.baseUrl, auth = config.clone()); + statusClient = check new (sourceServerConfig.baseUrl, auth = config.clone()); } } else { lock { - statusClient = check new (exportSeverConfig.baseUrl); + statusClient = check new (sourceServerConfig.baseUrl); } } @@ -53,7 +53,7 @@ service /trigger on new http:Listener(clientServiceConfig.port) { string queryString = populateQueryString(_outputFormat, _since, _type); // kick-off request to the bulk export server lock { - status = statusClient->get(string `${exportSeverConfig.contextPath}/Patient/$export${queryString}`, { + status = statusClient->get(string `${sourceServerConfig.contextPath}/Patient/$export${queryString}`, { Accept: "application/fhir+json", Prefer: "respond-async" }); @@ -106,7 +106,7 @@ service /trigger on new http:Listener(clientServiceConfig.port) { // kick-off request to the bulk export server lock { - status = statusClient->post(string `${exportSeverConfig.contextPath}/Patient/$export`, parametersResource.clone().toJson(), + status = statusClient->post(string `${sourceServerConfig.contextPath}/Patient/$export`, parametersResource.clone().toJson(), { Accept: "application/fhir+json", Prefer: "respond-async", @@ -151,7 +151,7 @@ service /trigger on new http:Listener(clientServiceConfig.port) { string queryString = populateQueryString(_outputFormat, _since, _type); // kick-off request to the bulk export server lock { - status = statusClient->get(string `${exportSeverConfig.contextPath}/Group/${group_id}/$export${queryString}`, { + status = statusClient->get(string `${sourceServerConfig.contextPath}/Group/${group_id}/$export${queryString}`, { Accept: "application/fhir+json", Prefer: "respond-async" }); @@ -199,7 +199,7 @@ isolated function submitBackgroundJob(string taskId, http:Response|http:ClientEr // get the location of the status check do { string location = check status.getHeader("Content-location"); - task:JobId|() _ = check executeJob(new PollingTask(taskId, location), exportSeverConfig.defaultIntervalInSec); + task:JobId|() _ = check executeJob(new PollingTask(taskId, location), sourceServerConfig.defaultIntervalInSec); log:printDebug("Polling location recieved: " + location); } on fail var e { log:printError("Error occurred while getting the location or scheduling the Job", e); diff --git a/regulation/cms-0057-f/bulk-export-client-service/utils.bal b/regulation/cms-0057-f/bulk-export-client-service/utils.bal index 97f79ac..b3db943 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/utils.bal +++ b/regulation/cms-0057-f/bulk-export-client-service/utils.bal @@ -51,7 +51,7 @@ public isolated function saveFileInFS(string downloadLink, string fileName) retu log:printDebug(string `Successfully downloaded the file. File name: ${fileName}`); } -public isolated function sendFileFromFSToFTP(FtpServerConfig config, string sourcePath, string fileName) returns error? { +public isolated function sendFileFromFSToFTP(TargetServerConfig config, string sourcePath, string fileName) returns error? { // Implement the FTP server logic here. ftp:Client fileClient = check new ({ host: config.host, @@ -78,10 +78,10 @@ public isolated function downloadFiles(json exportSummary, string exportId) retu if downloadFileResult is error { log:printError("Error occurred while downloading the file.", downloadFileResult); } - if ftpServerConfig.enabled { + if targetServerConfig.'type == "ftp" { // download the file to the FTP server // implement the FTP server logic - error? uploadFileResult = sendFileFromFSToFTP(ftpServerConfig, string `${clientServiceConfig.targetDirectory}/${item.'type}-exported.ndjson`, string `${item.'type}-exported.ndjson`); + error? uploadFileResult = sendFileFromFSToFTP(targetServerConfig, string `${clientServiceConfig.targetDirectory}/${item.'type}-exported.ndjson`, string `${item.'type}-exported.ndjson`); if uploadFileResult is error { log:printError("Error occurred while sending the file to ftp.", downloadFileResult); diff --git a/regulation/cms-0057-f/member-match-service/service.bal b/regulation/cms-0057-f/member-match-service/service.bal index 8ca1bc1..054012c 100644 --- a/regulation/cms-0057-f/member-match-service/service.bal +++ b/regulation/cms-0057-f/member-match-service/service.bal @@ -60,6 +60,9 @@ final davincihrex100:MatcherConfig matcherConfig = { // FHIR member matcher instance final davincihrex100:FhirMemberMatcher fhirMemberMatcher = check new (matcherConfig, ()); +## uncomment the following line to use the demo FHIR member matcher. +## Note: This will bypass the default matching flow. This is only for testing purposes. + // final DemoFHIRMemberMatcher fhirMemberMatcher = check new (); service / on new fhirr4:Listener(9095, apiConfig) { From c46dc32aaed5fca70a745f4079731ec255fda151 Mon Sep 17 00:00:00 2001 From: isuruh15 Date: Fri, 27 Sep 2024 22:23:19 +0530 Subject: [PATCH 03/10] update dependancies --- regulation/cms-0057-f/member-match-service/Dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regulation/cms-0057-f/member-match-service/Dependencies.toml b/regulation/cms-0057-f/member-match-service/Dependencies.toml index ee01e90..69ad967 100644 --- a/regulation/cms-0057-f/member-match-service/Dependencies.toml +++ b/regulation/cms-0057-f/member-match-service/Dependencies.toml @@ -361,7 +361,7 @@ modules = [ [[package]] org = "ballerinax" name = "health.fhir.r4.international401" -version = "2.1.0" +version = "2.1.1" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "log"}, From 6a0403b2d1ee57978ddfd6d340f1e41b74d33408 Mon Sep 17 00:00:00 2001 From: isuruh15 Date: Tue, 1 Oct 2024 15:15:51 +0530 Subject: [PATCH 04/10] add endpoints --- .../.choreo/endpoints.yaml | 21 +++++++++++++++++++ .../member-match-service/Dependencies.toml | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 regulation/cms-0057-f/member-match-service/.choreo/endpoints.yaml diff --git a/regulation/cms-0057-f/member-match-service/.choreo/endpoints.yaml b/regulation/cms-0057-f/member-match-service/.choreo/endpoints.yaml new file mode 100644 index 0000000..08589bb --- /dev/null +++ b/regulation/cms-0057-f/member-match-service/.choreo/endpoints.yaml @@ -0,0 +1,21 @@ +# +required Version of the endpoint configuration YAML +version: 0.1 + +# +required List of endpoints to create +endpoints: + # +required Unique name for the endpoint. (This name will be used when generating the managed API) +- name: FHIR Member Matcher API + # +required Numeric port value that gets exposed via this endpoint + port: 9091 + # +required Type of the traffic this endpoint is accepting. Example: REST, GraphQL, etc. + # Allowed values: REST, GraphQL, GRPC, UDP, TCP + type: REST + # +optional Network level visibility of this endpoint. Defaults to Public + # Accepted values: Project|Organization|Public. + networkVisibility: Public + # +optional Context (base path) of the API that is exposed via this endpoint. + # This is mandatory if the endpoint type is set to REST or GraphQL. + context: / + # +optional Path to the schema definition file. Defaults to wild card route if not provided + # This is only applicable to REST endpoint types. + # The path should be relative to the docker context. diff --git a/regulation/cms-0057-f/member-match-service/Dependencies.toml b/regulation/cms-0057-f/member-match-service/Dependencies.toml index 69ad967..ce12ea0 100644 --- a/regulation/cms-0057-f/member-match-service/Dependencies.toml +++ b/regulation/cms-0057-f/member-match-service/Dependencies.toml @@ -61,7 +61,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.10.15" +version = "2.10.16" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, From a350b7815b73087abae9fb1fecb193fe9bf270a2 Mon Sep 17 00:00:00 2001 From: Isuru Samaranayake Date: Tue, 29 Oct 2024 11:16:28 +0530 Subject: [PATCH 05/10] Add deployment configs (#8) * add endpoints * update dependancies * add endpoints * hardcode port * remove context * add component configs * add file fetch operation * update openapi * add error log * added error payload * add info logs * add service level isolation * remove locked block - tmp * refactor --- .../.choreo/component-config.yaml | 24 ++ .../.choreo/endpoints.yaml | 21 ++ .../Dependencies.toml | 14 +- .../openapi/service_openapi.yaml | 225 ++++++++++++++++++ .../bulk-export-client-service/service.bal | 24 +- .../bulk-export-client-service/utils.bal | 5 +- .../member-match-service/Dependencies.toml | 2 +- 7 files changed, 308 insertions(+), 7 deletions(-) create mode 100644 regulation/cms-0057-f/bulk-export-client-service/.choreo/component-config.yaml create mode 100644 regulation/cms-0057-f/bulk-export-client-service/.choreo/endpoints.yaml create mode 100644 regulation/cms-0057-f/bulk-export-client-service/openapi/service_openapi.yaml diff --git a/regulation/cms-0057-f/bulk-export-client-service/.choreo/component-config.yaml b/regulation/cms-0057-f/bulk-export-client-service/.choreo/component-config.yaml new file mode 100644 index 0000000..a917795 --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/.choreo/component-config.yaml @@ -0,0 +1,24 @@ +apiVersion: core.choreo.dev/v1beta1 +kind: ComponentConfig +spec: + # +optional Incoming connection details for the component (AKA endpoints). + inbound: + # +required Unique name for the endpoint. (This name will be used when generating the managed API) + - name: Bulk Export Client Endpoint + # +required Numeric port value that gets exposed via the endpoint + port: 9099 + # +required Type of traffic that the endpoint is accepting. For example: REST, GraphQL, etc. + # Allowed values: REST, GraphQL, GRPC, TCP, UDP. + type: REST + # +optional Network level visibility of the endpoint. Defaults to Public + # Accepted values: Project|Organization|Public. + networkVisibility: Public + # +optional Context (base path) of the API that gets exposed via the endpoint. + # This is mandatory if the endpoint type is set to REST or GraphQL. + context: / + # +optional The path to the schema definition file. Defaults to wildcard route if not specified. + # This is only applicable to REST endpoint types. + # The path should be relative to the Docker context. + schemaFilePath: openapi/service_openapi.yaml + # +optional Outgoing connection details for the component. + diff --git a/regulation/cms-0057-f/bulk-export-client-service/.choreo/endpoints.yaml b/regulation/cms-0057-f/bulk-export-client-service/.choreo/endpoints.yaml new file mode 100644 index 0000000..2ab9af5 --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/.choreo/endpoints.yaml @@ -0,0 +1,21 @@ +# +required Version of the endpoint configuration YAML +version: 0.1 + +# +required List of endpoints to create +endpoints: + # +required Unique name for the endpoint. (This name will be used when generating the managed API) +- name: FHIR Bulk export client API + # +required Numeric port value that gets exposed via this endpoint + port: 9094 + # +required Type of the traffic this endpoint is accepting. Example: REST, GraphQL, etc. + # Allowed values: REST, GraphQL, GRPC, UDP, TCP + type: REST + # +optional Network level visibility of this endpoint. Defaults to Public + # Accepted values: Project|Organization|Public. + networkVisibility: Public + # +optional Context (base path) of the API that is exposed via this endpoint. + # This is mandatory if the endpoint type is set to REST or GraphQL. + context: / + # +optional Path to the schema definition file. Defaults to wild card route if not provided + # This is only applicable to REST endpoint types. + # The path should be relative to the docker context. diff --git a/regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml b/regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml index 129ccc7..ed06bb8 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml +++ b/regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml @@ -57,6 +57,9 @@ dependencies = [ {org = "ballerina", name = "os"}, {org = "ballerina", name = "time"} ] +modules = [ + {org = "ballerina", packageName = "file", moduleName = "file"} +] [[package]] org = "ballerina" @@ -75,7 +78,7 @@ modules = [ [[package]] org = "ballerina" name = "http" -version = "2.10.15" +version = "2.10.16" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, @@ -241,6 +244,9 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.int"} ] +modules = [ + {org = "ballerina", packageName = "mime", moduleName = "mime"} +] [[package]] org = "ballerina" @@ -359,7 +365,7 @@ dependencies = [ [[package]] org = "ballerinax" name = "health.fhir.r4" -version = "5.1.0" +version = "5.1.1" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "http"}, @@ -380,7 +386,7 @@ modules = [ [[package]] org = "ballerinax" name = "health.fhir.r4.international401" -version = "2.1.0" +version = "2.1.1" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "log"}, @@ -395,10 +401,12 @@ org = "wso2" name = "bulk_export_client_service" version = "0.1.0" dependencies = [ + {org = "ballerina", name = "file"}, {org = "ballerina", name = "ftp"}, {org = "ballerina", name = "http"}, {org = "ballerina", name = "io"}, {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, {org = "ballerina", name = "task"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, diff --git a/regulation/cms-0057-f/bulk-export-client-service/openapi/service_openapi.yaml b/regulation/cms-0057-f/bulk-export-client-service/openapi/service_openapi.yaml new file mode 100644 index 0000000..848ed05 --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/openapi/service_openapi.yaml @@ -0,0 +1,225 @@ +openapi: 3.0.1 +info: + title: Service Openapi Yaml + version: 0.1.0 +servers: +- url: "{server}:{port}/" + variables: + server: + default: http://localhost + port: + default: "9099" +paths: + /export: + get: + operationId: getExport + parameters: + - name: _outputFormat + in: query + schema: + type: string + nullable: true + - name: _since + in: query + schema: + type: string + nullable: true + - name: _type + in: query + schema: + type: string + nullable: true + responses: + "200": + description: Ok + content: + application/json: + schema: + type: object + "500": + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + post: + operationId: postExport + parameters: + - name: _outputFormat + in: query + content: + application/json: + schema: + type: object + additionalProperties: true + - name: _since + in: query + content: + application/json: + schema: + type: object + additionalProperties: true + - name: _type + in: query + content: + application/json: + schema: + type: object + additionalProperties: true + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MatchedPatient' + responses: + "201": + description: Created + content: + application/json: + schema: + type: object + "500": + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + /export/group/{group_id}: + get: + operationId: getExportGroupGroupId + parameters: + - name: group_id + in: path + required: true + schema: + type: string + - name: _outputFormat + in: query + schema: + type: string + nullable: true + - name: _since + in: query + schema: + type: string + nullable: true + - name: _type + in: query + schema: + type: string + nullable: true + responses: + "200": + description: Ok + content: + application/json: + schema: + type: object + "500": + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + /status: + get: + operationId: getStatus + parameters: + - name: exportId + in: query + required: true + schema: + type: string + responses: + "200": + description: Ok + content: + application/json: + schema: + type: object + "500": + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + /download: + get: + operationId: getDownload + parameters: + - name: location + in: query + required: true + schema: + type: string + responses: {} + /file/download: + get: + operationId: getFileDownload + parameters: + - name: exportId + in: query + required: true + schema: + type: string + - name: resourceType + in: query + required: true + schema: + type: string + responses: + default: + description: Any Response + content: + '*/*': + schema: + description: Any type of entity body + "202": + description: Accepted + "400": + description: BadRequest + "500": + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' +components: + schemas: + ErrorPayload: + type: object + properties: + reason: + type: string + description: Reason phrase + path: + type: string + description: Request path + method: + type: string + description: Method type of the request + message: + type: string + description: Error message + timestamp: + type: string + description: Timestamp of the error + status: + type: integer + description: Relevant HTTP status code + format: int32 + MatchedPatient: + required: + - id + type: object + properties: + id: + type: string + canonical: + type: string + identifiers: + type: object + additionalProperties: + type: string diff --git a/regulation/cms-0057-f/bulk-export-client-service/service.bal b/regulation/cms-0057-f/bulk-export-client-service/service.bal index 4cc578a..82998ed 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/service.bal +++ b/regulation/cms-0057-f/bulk-export-client-service/service.bal @@ -3,6 +3,8 @@ import ballerina/log; import ballerina/task; import ballerina/uuid; import ballerinax/health.fhir.r4.international401; +import ballerina/file; +import ballerina/mime; configurable BulkExportServerConfig sourceServerConfig = ?; configurable BulkExportClientConfig clientServiceConfig = ?; @@ -17,7 +19,7 @@ http:OAuth2ClientCredentialsGrantConfig config = { isolated http:Client statusClient = check new (sourceServerConfig.baseUrl); -service /trigger on new http:Listener(clientServiceConfig.port) { +isolated service / on new http:Listener(9099) { function init() returns error? { @@ -190,6 +192,24 @@ service /trigger on new http:Listener(clientServiceConfig.port) { return http:STATUS_ACCEPTED; } + + isolated resource function get file/download(http:Request req, string exportId, string resourceType) returns http:Response|error? { + + log:printInfo("Downloading file for member: " + exportId + " and resource type: " + resourceType); + string filePath = clientServiceConfig.targetDirectory + file:pathSeparator + exportId + file:pathSeparator + resourceType + "-exported.ndjson"; + + mime:Entity entity = new; + entity.setFileAsEntityBody(filePath); + + http:Response response = new; + response.setEntity(entity); + error? contentType = response.setContentType("gzip"); + if contentType is error { + log:printError("Error occurred while setting the content type: "); + } + return response; + + } } isolated function submitBackgroundJob(string taskId, http:Response|http:ClientError status) { @@ -205,5 +225,7 @@ isolated function submitBackgroundJob(string taskId, http:Response|http:ClientEr log:printError("Error occurred while getting the location or scheduling the Job", e); // if location is available, can retry the task } + }else { + log:printError("Error occurred while sending the kick-off request to the bulk export server.", status); } } diff --git a/regulation/cms-0057-f/bulk-export-client-service/utils.bal b/regulation/cms-0057-f/bulk-export-client-service/utils.bal index b3db943..b4da1f4 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/utils.bal +++ b/regulation/cms-0057-f/bulk-export-client-service/utils.bal @@ -5,6 +5,7 @@ import ballerina/log; import ballerina/task; import ballerinax/health.fhir.r4; import ballerinax/health.fhir.r4.international401; +import ballerina/file; public isolated function executeJob(PollingTask job, decimal interval) returns task:JobId|error? { @@ -74,14 +75,14 @@ public isolated function downloadFiles(json exportSummary, string exportId) retu foreach OutputFile item in exportSummary1.output { log:printDebug("Downloading the file.", url = item.url); - error? downloadFileResult = saveFileInFS(item.url, string `${clientServiceConfig.targetDirectory}/${item.'type}-exported.ndjson`); + error? downloadFileResult = saveFileInFS(item.url, string `${clientServiceConfig.targetDirectory}${file:pathSeparator}${exportId}${file:pathSeparator}${item.'type}-exported.ndjson`); if downloadFileResult is error { log:printError("Error occurred while downloading the file.", downloadFileResult); } if targetServerConfig.'type == "ftp" { // download the file to the FTP server // implement the FTP server logic - error? uploadFileResult = sendFileFromFSToFTP(targetServerConfig, string `${clientServiceConfig.targetDirectory}/${item.'type}-exported.ndjson`, string `${item.'type}-exported.ndjson`); + error? uploadFileResult = sendFileFromFSToFTP(targetServerConfig, string `${clientServiceConfig.targetDirectory}${file:pathSeparator}${item.'type}-exported.ndjson`, string `${item.'type}-exported.ndjson`); if uploadFileResult is error { log:printError("Error occurred while sending the file to ftp.", downloadFileResult); diff --git a/regulation/cms-0057-f/member-match-service/Dependencies.toml b/regulation/cms-0057-f/member-match-service/Dependencies.toml index ce12ea0..7b149b1 100644 --- a/regulation/cms-0057-f/member-match-service/Dependencies.toml +++ b/regulation/cms-0057-f/member-match-service/Dependencies.toml @@ -324,7 +324,7 @@ modules = [ [[package]] org = "ballerinax" name = "health.fhir.r4" -version = "5.1.0" +version = "5.1.1" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "http"}, From e1dcc80d905fd62ce680e830709083408814a104 Mon Sep 17 00:00:00 2001 From: isuruh15 Date: Tue, 29 Oct 2024 14:50:03 +0530 Subject: [PATCH 06/10] add readme --- .../inMemoryStorage.bal | 12 + .../bulk-export-client-service/readme.md | 87 +++++ .../bulk-export-client-service/records.bal | 67 +++- .../bulk-export-client-service/registry.bal | 14 + .../bulk-export-client-service/service.bal | 23 +- .../bulk-export-client-service/utils.bal | 74 +++- .../member-match-service/custom_matcher.bal | 2 +- .../cms-0057-f/member-match-service/readme.md | 336 ++++++++++++++++++ .../member-match-service/service.bal | 4 +- 9 files changed, 597 insertions(+), 22 deletions(-) create mode 100644 regulation/cms-0057-f/bulk-export-client-service/readme.md create mode 100644 regulation/cms-0057-f/member-match-service/readme.md diff --git a/regulation/cms-0057-f/bulk-export-client-service/inMemoryStorage.bal b/regulation/cms-0057-f/bulk-export-client-service/inMemoryStorage.bal index 542588d..677618d 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/inMemoryStorage.bal +++ b/regulation/cms-0057-f/bulk-export-client-service/inMemoryStorage.bal @@ -1,3 +1,15 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. import ballerina/time; //This file represents the in-memory storage of the export tasks and polling events. diff --git a/regulation/cms-0057-f/bulk-export-client-service/readme.md b/regulation/cms-0057-f/bulk-export-client-service/readme.md new file mode 100644 index 0000000..35ffb17 --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/readme.md @@ -0,0 +1,87 @@ +# Bulk Export Client Pre-built Service + +This Ballerina pre-built service is designed to interact with a FHIR server to consume the /Patient/$export operation. It provides endpoints to kick off the export process, check the status of the export, download the exported files, and optionally send the downloaded files to an FTP server. + +## Features +- Initiates `/Patient/$export` operation on a FHIR server. +- Retrieves the status of the export process. +- Downloads exported NDJSON files from the FHIR server. +- Supports uploading the downloaded files to an FTP server. + +## Endpoints + +### 1. Kick-off Export +Initiates the `/Patient/$export` operation on the FHIR server. + +**Endpoint**: +`POST /export` for single Patient exports +`GET /export` for all Patient exports + +**Request Body**: +```json +[ + {"id":"member-id1"}, + {"id":"member-id2"} +] + +**Response**: +- Returns an OperationOutcome resource with the exportId. + +### 2. Get Export Status +Checks the status of the ongoing export operation. + +**Endpoint**: +`GET /status` + +***Params:***
- exportId - exportId returned in kick-off response + + +**Response**: +- Provides the status instnaces of the export process (status of all polling events happened in the background). + +### 3. Download Exported Files +Downloads the exported NDJSON files once the export process is complete. + +**Endpoint**: +`GET /file/download` + +***Params:***
+- exportId - exportId returned in kick-off response +- resourceType - FHIR resource type + +**Response**: +- Downloads the NDJSON files containing the exported FHIR resources for given exportId and resourceType. + +## How to Run + - Clone the repository + - Add configurations. + - If you're trying this on Choreo, you can configure values upon in the deploy step. + - For other deployments, add a file named, `Config.toml` with following configs to the Ballerina project root. + + ``` toml + [clientServiceConfig] + port = 9099 + authEnabled = false + targetDirectory = "target_dir" + + [sourceServerConfig] + baseUrl = "server_base_url" + contextPath = "context_path" + tokenUrl = "" + fileServerUrl = "" + clientId = "bulk-export-client-id" + clientSecret = "bulk-export-client-secret" + scopes = ["scope1"] + defaultIntervalInSec = 2.0 + + [targetServerConfig] + type = "fhir" #or fhir + host = "host" + port = + username = "username" + password = "password" + directory = ">" + ``` + + - Run the project + ` bal run` diff --git a/regulation/cms-0057-f/bulk-export-client-service/records.bal b/regulation/cms-0057-f/bulk-export-client-service/records.bal index 570a4de..835bbbf 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/records.bal +++ b/regulation/cms-0057-f/bulk-export-client-service/records.bal @@ -1,4 +1,26 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +# Source Server config, this server should support $export operation. +# +# + baseUrl - Base URL of the server +# + tokenUrl - token endpoint URL +# + clientId - client ID +# + clientSecret - client secret +# + scopes - array of scopes, to access the export operation +# + fileServerUrl - if the server exports to different file server, this URL should be provided +# + contextPath - context path for the export operation, if any +# + defaultIntervalInSec - polling interval in seconds public type BulkExportServerConfig record {| string baseUrl; string tokenUrl; @@ -10,6 +32,14 @@ public type BulkExportServerConfig record {| decimal defaultIntervalInSec; |}; +# Server config for import FHIR resources. +# +# + 'type - FHIR or FTP +# + host - host name of the server +# + port - port number of the server +# + username - user name to access the server, for ftp +# + password - password to access the server, for ftp +# + directory - directory to save the exported files public type TargetServerConfig record {| string 'type; string host; @@ -17,32 +47,38 @@ public type TargetServerConfig record {| string username; string password; string directory; - |}; - -// have a generic config for source server and target server for FHIR cases -// check Ballerina FTP client for the FTP server config +|}; +# Configs for pre built service. +# +# + port - port number of the service +# + authEnabled - true if the bulk export server requires authentication +# + targetDirectory - temporary directory to save the exported files public type BulkExportClientConfig record {| int port; boolean authEnabled; string targetDirectory; |}; -public type FtpServerConfig record {| - boolean enabled; - string host; - int port; - string username; - string password; - string directory; -|}; - +# record to map exported resource metadata. +# +# + 'type - file type +# + url - downloadable location of the file +# + count - record count public type OutputFile record {| string 'type; string url; int count; |}; +# record to hold summary of exports. +# +# + transactionTime - time of the transaction +# + request - request description +# + requiresAccessToken - authentication required or not +# + output - output files +# + deleted - deleted files +# + 'error - error files public type ExportSummary record {| string transactionTime; string request; @@ -52,6 +88,11 @@ public type ExportSummary record {| string[] 'error; |}; +# Record to hold matched patients. +# +# + id - patient ID +# + canonical - canonical URL +# + identifiers - other identifiers public type MatchedPatient record {| string id; string canonical?; diff --git a/regulation/cms-0057-f/bulk-export-client-service/registry.bal b/regulation/cms-0057-f/bulk-export-client-service/registry.bal index 68fc88a..ba4de0f 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/registry.bal +++ b/regulation/cms-0057-f/bulk-export-client-service/registry.bal @@ -1,3 +1,15 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. import ballerina/time; # Use to keep track of each polling event. @@ -30,6 +42,8 @@ public type ExportTask record {| |}; +# Function types to interact with the storage impl. + type getExportTask function (string exportId) returns ExportTask; type getPollingEvents function (string exportId) returns [PollingEvent]; diff --git a/regulation/cms-0057-f/bulk-export-client-service/service.bal b/regulation/cms-0057-f/bulk-export-client-service/service.bal index 82998ed..58116b0 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/service.bal +++ b/regulation/cms-0057-f/bulk-export-client-service/service.bal @@ -1,10 +1,22 @@ +import ballerina/file; +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. import ballerina/http; import ballerina/log; +import ballerina/mime; import ballerina/task; import ballerina/uuid; import ballerinax/health.fhir.r4.international401; -import ballerina/file; -import ballerina/mime; configurable BulkExportServerConfig sourceServerConfig = ?; configurable BulkExportClientConfig clientServiceConfig = ?; @@ -19,7 +31,7 @@ http:OAuth2ClientCredentialsGrantConfig config = { isolated http:Client statusClient = check new (sourceServerConfig.baseUrl); -isolated service / on new http:Listener(9099) { +isolated service / on new http:Listener(clientServiceConfig.port) { function init() returns error? { @@ -186,7 +198,8 @@ isolated service / on new http:Listener(9099) { error? saveFileResult = saveFileInFS(location, "exportedData.json"); if saveFileResult is error { - + log:printError("Error occurred while saving the file in the file system."); + return http:STATUS_INTERNAL_SERVER_ERROR; } return http:STATUS_ACCEPTED; @@ -225,7 +238,7 @@ isolated function submitBackgroundJob(string taskId, http:Response|http:ClientEr log:printError("Error occurred while getting the location or scheduling the Job", e); // if location is available, can retry the task } - }else { + } else { log:printError("Error occurred while sending the kick-off request to the bulk export server.", status); } } diff --git a/regulation/cms-0057-f/bulk-export-client-service/utils.bal b/regulation/cms-0057-f/bulk-export-client-service/utils.bal index b4da1f4..87dc1d3 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/utils.bal +++ b/regulation/cms-0057-f/bulk-export-client-service/utils.bal @@ -1,3 +1,16 @@ +import ballerina/file; +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. import ballerina/ftp; import ballerina/http; import ballerina/io; @@ -5,8 +18,12 @@ import ballerina/log; import ballerina/task; import ballerinax/health.fhir.r4; import ballerinax/health.fhir.r4.international401; -import ballerina/file; +# Schedule Ballerina task . +# +# + job - Polling task to be executed +# + interval - interval to execute the job +# + return - assigned job id public isolated function executeJob(PollingTask job, decimal interval) returns task:JobId|error? { // Implement the job execution logic here @@ -15,6 +32,10 @@ public isolated function executeJob(PollingTask job, decimal interval) returns t return id; } +# Terminate periodic task. +# +# + id - job id to be terminated +# + return - error if failed to terminate the job public isolated function unscheduleJob(task:JobId id) returns error? { // Implement the job termination logic here @@ -26,6 +47,11 @@ public isolated function unscheduleJob(task:JobId id) returns error? { return null; } +# Get ndjson content as stream. +# +# + downloadLink - file location +# + statusClientV2 - http Client instance +# + return - byte array stream of content public isolated function getFileAsStream(string downloadLink, http:Client statusClientV2) returns stream|error? { http:Response|http:ClientError statusResponse = statusClientV2->get("/"); @@ -42,6 +68,11 @@ public isolated function getFileAsStream(string downloadLink, http:Client status return null; } +# Write file into file system. +# +# + downloadLink - file location +# + fileName - file name +# + return - error if failed public isolated function saveFileInFS(string downloadLink, string fileName) returns error? { http:Client statusClientV2 = check new (downloadLink); @@ -52,6 +83,12 @@ public isolated function saveFileInFS(string downloadLink, string fileName) retu log:printDebug(string `Successfully downloaded the file. File name: ${fileName}`); } +# Send file from file system to a file server via ftp. +# +# + config - server config +# + sourcePath - file path +# + fileName - file name +# + return - error if failed public isolated function sendFileFromFSToFTP(TargetServerConfig config, string sourcePath, string fileName) returns error? { // Implement the FTP server logic here. ftp:Client fileClient = check new ({ @@ -69,6 +106,11 @@ public isolated function sendFileFromFSToFTP(TargetServerConfig config, string s check fileStream.close(); } +# Util method to handle file download. +# +# + exportSummary - metadata of the export +# + exportId - assigned export id, (local reference) +# + return - error if failed public isolated function downloadFiles(json exportSummary, string exportId) returns error? { ExportSummary exportSummary1 = check exportSummary.cloneWithType(ExportSummary); @@ -96,6 +138,12 @@ public isolated function downloadFiles(json exportSummary, string exportId) retu return null; } +# Result has to deliver as OperationOutcome resources, this method populate OpOutcome with relavant info. +# +# + severity - severity of the outcome +# + code - code of the outcome +# + message - text description of the outcome +# + return - FHIR:R4 OperationOutcome resource public isolated function createOpereationOutcome(string severity, string code, string message) returns r4:OperationOutcome { r4:OperationOutcomeIssueSeverity severityType; do { @@ -118,6 +166,10 @@ public isolated function createOpereationOutcome(string severity, string code, s return operationOutcome; } +# Create R4:Parameters resource with given info. +# +# + parameters - parameter description +# + return - R4:Parameters resource public isolated function createR4Parameters(map parameters) returns international401:Parameters { international401:Parameters r4Parameters = {'parameter: []}; international401:ParametersParameter[] paramsArr = []; @@ -132,6 +184,13 @@ public isolated function createR4Parameters(map parameters) returns inte return r4Parameters; } +# Create R4:Parameters resource to query member-match operation.. +# +# + matchedPatients - parameter description +# + _outputFormat - parameter description +# + _since - parameter description +# + _type - parameter description +# + return - return value description public isolated function populateParamsResource(MatchedPatient[] matchedPatients, string? _outputFormat, string? _since, string? _type) returns international401:Parameters { international401:Parameters r4Parameters = {'parameter: []}; @@ -160,6 +219,12 @@ public isolated function populateParamsResource(MatchedPatient[] matchedPatients return r4Parameters; } +# Populate query string for export operation. +# +# + _outputFormat - value of _outputFormat +# + _since - value of _since +# + _type - value of _type +# + return - complete query string public isolated function populateQueryString(string? _outputFormat, string? _since, string? _type) returns string { string queryString = ""; @@ -176,6 +241,12 @@ public isolated function populateQueryString(string? _outputFormat, string? _sin return queryString; } +# Util function to append param to query string. +# +# + queryString - current string +# + key - new param key +# + value - new param value +# + return - updated string public isolated function addQueryParam(string queryString, string key, string value) returns string { if queryString == "" { return string `?${key}=${value}`; @@ -184,6 +255,7 @@ public isolated function addQueryParam(string queryString, string key, string va } } +# This class holds information related to the Ballerina task that used to poll the status endpoint. public class PollingTask { *task:Job; diff --git a/regulation/cms-0057-f/member-match-service/custom_matcher.bal b/regulation/cms-0057-f/member-match-service/custom_matcher.bal index c67dfe4..ee82380 100644 --- a/regulation/cms-0057-f/member-match-service/custom_matcher.bal +++ b/regulation/cms-0057-f/member-match-service/custom_matcher.bal @@ -8,4 +8,4 @@ public isolated class DemoFHIRMemberMatcher { // Hardcoded values for the sake of the example return "patinetID"; } -} \ No newline at end of file +} diff --git a/regulation/cms-0057-f/member-match-service/readme.md b/regulation/cms-0057-f/member-match-service/readme.md new file mode 100644 index 0000000..e342b8d --- /dev/null +++ b/regulation/cms-0057-f/member-match-service/readme.md @@ -0,0 +1,336 @@ +# Ballerina Pre-built Service for $member-match Operation + +This pre-built service implements the FHIR PDex `$member-match` operation, enabling member matching functionality on top of an existing FHIR server and FHIR consent service. It can be integrated to facilitate seamless data exchange between payers as part of the DaVinci Payer Data Exchange (PDex) workflow. + +## Features +- Implements the FHIR PDex `$member-match` operation for payer-to-payer data exchange. +- Supports integration with existing FHIR servers and consent services. +- Customizable member matching logic via an extension point. +- Ensures compliance with FHIR PDex and US Core profiles for accurate member identification. + +## Endpoints + +### 1. Member Match +Matches members based on patient demographic data across different payers. + +**Endpoint**: +`POST /fhir/r4/Patient/$member-match` + +**Request Body**: +FHIR-compliant demographic data of the patient for matching purposes. + +**Response**: +- Returns the FHIR Patient resource if a match is found, or an appropriate error message if no match is found. + + +## How it Works +1. The service receives a `$member-match` request from a new payer, which includes patient demographic information. +2. The service validates the request and checks for matching patient records in the existing FHIR server. +3. Consent is verified (if applicable) by interacting with the integrated FHIR consent service. +4. Upon a successful match, the FHIR Patient resource is returned to the requesting payer. + +## Customization +The service comes with a default member matching logic, but you can implement your own by leveraging the built-in extension point. Simply develop a custom matcher by implementing `*davincihrex100:MemberMatcher` and update the matcher instantiation with the newly developed matcher. + +``` +final DemoFHIRMemberMatcher fhirMemberMatcher = check new (); + +davincihrex100:MemberIdentifier memberIdentifier = check fhirMemberMatcher.matchMember(memberMatchResources); +``` + +## Prerequisites +- An operational FHIR server compliant with US Core profiles. +- A FHIR consent service (optional) to validate patient consent before exchanging data. + +## How to Run +1. Clone the repository. +2. Configure the FHIR server and consent service endpoints in the environment variables. +3. Deploy the service alongside your FHIR server. +4. Use the provided API endpoints to enable member matching for payer-to-payer exchanges. + +## Example Request + +```json +POST /Patient/$member-match +{ + "resourceType": "Parameters", + "parameter": [ + { + "resource": { + "resourceType": "Patient", + "extension": [ + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2106-3", + "display": "White" + } + }, + { + "url": "text", + "valueString": "Mixed" + } + ] + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2135-2", + "display": "Hispanic or Latino" + } + }, + { + "url": "text", + "valueString": "Hispanic or Latino" + } + ] + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", + "valueCode": "F" + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-genderIdentity", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor", + "code": "ASKU", + "display": "asked but unknown" + } + ], + "text": "asked but unknown" + } + } + ], + "gender": "female", + "telecom": [ + { + "system": "phone", + "use": "home", + "value": "555-555-5555" + }, + { + "system": "email", + "value": "amy.shaw@example.com" + } + ], + "id": "patient-1", + "identifier": [ + { + "system": "http://hospital.smarthealthit.org", + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR", + "display": "Medical Record Number" + } + ], + "text": "Medical Record Number" + }, + "value": "1032702" + } + ], + "address": [ + { + "country": "US", + "period": { + "start": "2020-07-22" + }, + "city": "Mounds", + "line": [ + "183 Mountain View St" + ], + "postalCode": "74048", + "state": "OK" + } + ], + "birthDate": "1987-02-20", + "meta": { + "versionId": "1", + "lastUpdated": "2021-06-01T00:00:00Z", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" + ] + }, + "name": [ + { + "given": [ + "Cortez", + "V." + ], + "period": { + "start": "2020-07-22" + }, + "family": "Prohaska", + "suffix": [ + "PharmD" + ] + } + ], + "implicitRules": "https://example.com/base" + }, + "name": "MemberPatient" + }, + { + "resource": { + "resourceType": "Coverage", + "payor": [ + { + "identifier": { + "system": "http://hl7.org/fhir/sid/us-npi", + "value": "9876543210" + }, + "display": "Old Health Plan" + } + ], + "id": "coverage-1", + "class": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/coverage-class", + "code": "group" + } + ] + }, + "value": "CB135" + } + ], + "period": { + "start": "2011-05-23", + "end": "2012-05-23" + }, + "beneficiary": { + "reference": "Patient/736a19c8-eea5-32c5-67ad-1947661de21a" + }, + "meta": { + "versionId": "1", + "lastUpdated": "2021-06-01T00:00:00Z" + }, + "implicitRules": "https://example.com/base", + "status": "entered-in-error" + }, + "name": "CoverageToMatch" + }, + { + "resource": { + "resourceType": "Coverage", + "payor": [ + { + "identifier": { + "system": "http://hl7.org/fhir/sid/us-npi", + "value": "0123456789" + }, + "display": "New Health Plan" + } + ], + "id": "cAA87654", + "period": { + "start": "2011-05-23", + "end": "2012-05-23" + }, + "beneficiary": { + "reference": "Patient/patient-1" + }, + "meta": { + "versionId": "1", + "lastUpdated": "2021-06-01T00:00:00Z" + }, + "implicitRules": "https://example.com/base", + "status": "active" + }, + "name": "CoverageToLink" + }, + { + "resource": { + "resourceType": "Consent", + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "patient-privacy" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IDSCL" + } + ] + } + ], + "patient": { + "reference": "Patient/patient-1" + }, + "performer": [ + { + "reference": "http://example.org/Patient/example" + } + ], + "sourceReference": { + "reference": "http://example.org/DocumentReference/someconsent" + }, + "policy": [ + { + "uri": "http://hl7.org/fhir/us/davinci-hrex/StructureDefinition-hrex-consent.html#regular" + } + ], + "provision": { + "type": "permit", + "period": { + "start": "2022-01-01", + "end": "2022-01-31" + }, + "actor": [ + { + "role": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/provenance-participant-type", + "code": "performer" + } + ] + }, + "reference": { + "identifier": { + "system": "http://hl7.org/fhir/sid/us-npi", + "value": "9876543210" + }, + "display": "Old Health Plan" + } + } + ], + "action": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentaction", + "code": "disclose" + } + ] + } + ] + } + }, + "name": "Consent" + } + ], + "id": "member-match-in" +} +``` diff --git a/regulation/cms-0057-f/member-match-service/service.bal b/regulation/cms-0057-f/member-match-service/service.bal index 054012c..1261977 100644 --- a/regulation/cms-0057-f/member-match-service/service.bal +++ b/regulation/cms-0057-f/member-match-service/service.bal @@ -60,8 +60,8 @@ final davincihrex100:MatcherConfig matcherConfig = { // FHIR member matcher instance final davincihrex100:FhirMemberMatcher fhirMemberMatcher = check new (matcherConfig, ()); -## uncomment the following line to use the demo FHIR member matcher. -## Note: This will bypass the default matching flow. This is only for testing purposes. +# # uncomment the following line to use the demo FHIR member matcher. +# # Note: This will bypass the default matching flow. This is only for testing purposes. // final DemoFHIRMemberMatcher fhirMemberMatcher = check new (); From 3e8119b9f163c38caa63573f03b589c054caf73d Mon Sep 17 00:00:00 2001 From: isuruh15 Date: Tue, 11 Feb 2025 08:55:46 +0530 Subject: [PATCH 07/10] update Ballerina version --- .../bulk-export-client-service/Ballerina.toml | 2 +- .../Dependencies.toml | 39 ++++++++------- .../member-match-service/Ballerina.toml | 4 +- .../member-match-service/Dependencies.toml | 48 ++++++++++--------- 4 files changed, 49 insertions(+), 44 deletions(-) diff --git a/regulation/cms-0057-f/bulk-export-client-service/Ballerina.toml b/regulation/cms-0057-f/bulk-export-client-service/Ballerina.toml index c1f2b81..eec775b 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/Ballerina.toml +++ b/regulation/cms-0057-f/bulk-export-client-service/Ballerina.toml @@ -2,7 +2,7 @@ org = "wso2" name = "bulk_export_client_service" version = "0.1.0" -distribution = "2201.8.6" +distribution = "2201.10.2" [build-options] observabilityIncluded = true diff --git a/regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml b/regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml index ed06bb8..0b5dda6 100644 --- a/regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml +++ b/regulation/cms-0057-f/bulk-export-client-service/Dependencies.toml @@ -5,12 +5,12 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.8.6" +distribution-version = "2201.10.2" [[package]] org = "ballerina" name = "auth" -version = "2.10.0" +version = "2.12.0" dependencies = [ {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "jballerina.java"}, @@ -41,7 +41,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.6.3" +version = "2.7.2" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "time"} @@ -50,7 +50,7 @@ dependencies = [ [[package]] org = "ballerina" name = "file" -version = "1.9.0" +version = "1.10.0" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, @@ -64,7 +64,7 @@ modules = [ [[package]] org = "ballerina" name = "ftp" -version = "2.10.1" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, @@ -78,7 +78,7 @@ modules = [ [[package]] org = "ballerina" name = "http" -version = "2.10.16" +version = "2.12.4" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, @@ -110,7 +110,7 @@ modules = [ [[package]] org = "ballerina" name = "io" -version = "1.6.1" +version = "1.6.3" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.value"} @@ -127,10 +127,11 @@ version = "0.0.0" [[package]] org = "ballerina" name = "jwt" -version = "2.10.0" +version = "2.13.0" dependencies = [ {org = "ballerina", name = "cache"}, {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.int"}, {org = "ballerina", name = "lang.string"}, @@ -224,7 +225,7 @@ dependencies = [ [[package]] org = "ballerina" name = "log" -version = "2.9.0" +version = "2.10.0" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, @@ -238,11 +239,12 @@ modules = [ [[package]] org = "ballerina" name = "mime" -version = "2.9.0" +version = "2.10.1" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.int"} + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "log"} ] modules = [ {org = "ballerina", packageName = "mime", moduleName = "mime"} @@ -251,7 +253,7 @@ modules = [ [[package]] org = "ballerina" name = "oauth2" -version = "2.10.0" +version = "2.12.0" dependencies = [ {org = "ballerina", name = "cache"}, {org = "ballerina", name = "crypto"}, @@ -264,7 +266,7 @@ dependencies = [ [[package]] org = "ballerina" name = "observe" -version = "1.2.3" +version = "1.3.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -297,6 +299,7 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ @@ -306,7 +309,7 @@ modules = [ [[package]] org = "ballerina" name = "time" -version = "2.4.0" +version = "2.5.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -325,7 +328,7 @@ dependencies = [ [[package]] org = "ballerina" name = "uuid" -version = "1.7.0" +version = "1.8.0" dependencies = [ {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "jballerina.java"}, @@ -351,7 +354,7 @@ modules = [ [[package]] org = "ballerinax" name = "health.base" -version = "1.0.3" +version = "1.1.0" dependencies = [ {org = "ballerina", name = "http"}, {org = "ballerina", name = "io"}, @@ -365,7 +368,7 @@ dependencies = [ [[package]] org = "ballerinax" name = "health.fhir.r4" -version = "5.1.1" +version = "5.2.1" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "http"}, @@ -386,7 +389,7 @@ modules = [ [[package]] org = "ballerinax" name = "health.fhir.r4.international401" -version = "2.1.1" +version = "2.2.0" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "log"}, diff --git a/regulation/cms-0057-f/member-match-service/Ballerina.toml b/regulation/cms-0057-f/member-match-service/Ballerina.toml index d36488a..2053397 100644 --- a/regulation/cms-0057-f/member-match-service/Ballerina.toml +++ b/regulation/cms-0057-f/member-match-service/Ballerina.toml @@ -2,7 +2,7 @@ org = "wso2" name = "MemberMatchService" version = "0.1.0" -distribution = "2201.8.6" +distribution = "2201.10.2" [build-options] observabilityIncluded = true @@ -11,10 +11,8 @@ observabilityIncluded = true org="ballerinax" name="health.fhir.r4.davincihrex100" version="1.0.0" -repository="local" [[dependency]] org="ballerinax" name="health.fhirr4" version="1.3.3" -repository="local" diff --git a/regulation/cms-0057-f/member-match-service/Dependencies.toml b/regulation/cms-0057-f/member-match-service/Dependencies.toml index 7b149b1..3d8c08d 100644 --- a/regulation/cms-0057-f/member-match-service/Dependencies.toml +++ b/regulation/cms-0057-f/member-match-service/Dependencies.toml @@ -5,12 +5,12 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.8.6" +distribution-version = "2201.10.2" [[package]] org = "ballerina" name = "auth" -version = "2.10.0" +version = "2.12.0" dependencies = [ {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "jballerina.java"}, @@ -41,7 +41,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.6.3" +version = "2.7.2" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "time"} @@ -50,7 +50,7 @@ dependencies = [ [[package]] org = "ballerina" name = "file" -version = "1.9.0" +version = "1.10.0" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, @@ -61,7 +61,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.10.16" +version = "2.12.4" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, @@ -93,7 +93,7 @@ modules = [ [[package]] org = "ballerina" name = "io" -version = "1.6.1" +version = "1.6.3" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.value"} @@ -107,10 +107,11 @@ version = "0.0.0" [[package]] org = "ballerina" name = "jwt" -version = "2.10.0" +version = "2.13.0" dependencies = [ {org = "ballerina", name = "cache"}, {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.int"}, {org = "ballerina", name = "lang.string"}, @@ -195,7 +196,7 @@ dependencies = [ [[package]] org = "ballerina" name = "log" -version = "2.9.0" +version = "2.10.0" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, @@ -206,17 +207,18 @@ dependencies = [ [[package]] org = "ballerina" name = "mime" -version = "2.9.0" +version = "2.10.1" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.int"} + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "log"} ] [[package]] org = "ballerina" name = "oauth2" -version = "2.10.0" +version = "2.12.0" dependencies = [ {org = "ballerina", name = "cache"}, {org = "ballerina", name = "crypto"}, @@ -229,7 +231,7 @@ dependencies = [ [[package]] org = "ballerina" name = "observe" -version = "1.2.3" +version = "1.3.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -255,7 +257,7 @@ dependencies = [ [[package]] org = "ballerina" name = "time" -version = "2.4.0" +version = "2.5.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -271,7 +273,7 @@ dependencies = [ [[package]] org = "ballerina" name = "uuid" -version = "1.7.0" +version = "1.8.0" dependencies = [ {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "jballerina.java"}, @@ -294,7 +296,7 @@ modules = [ [[package]] org = "ballerinax" name = "health.base" -version = "1.0.3" +version = "1.1.0" dependencies = [ {org = "ballerina", name = "http"}, {org = "ballerina", name = "io"}, @@ -308,7 +310,7 @@ dependencies = [ [[package]] org = "ballerinax" name = "health.clients.fhir" -version = "2.0.0" +version = "2.1.0" dependencies = [ {org = "ballerina", name = "http"}, {org = "ballerina", name = "io"}, @@ -324,7 +326,7 @@ modules = [ [[package]] org = "ballerinax" name = "health.fhir.r4" -version = "5.1.1" +version = "5.2.1" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "http"}, @@ -361,7 +363,7 @@ modules = [ [[package]] org = "ballerinax" name = "health.fhir.r4.international401" -version = "2.1.1" +version = "2.2.0" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "log"}, @@ -371,9 +373,11 @@ dependencies = [ [[package]] org = "ballerinax" name = "health.fhir.r4.parser" -version = "5.1.0" +version = "5.2.1" dependencies = [ + {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "http"}, + {org = "ballerina", name = "lang.regexp"}, {org = "ballerina", name = "log"}, {org = "ballerinax", name = "health.fhir.r4"}, {org = "ballerinax", name = "health.fhir.r4.international401"} @@ -382,7 +386,7 @@ dependencies = [ [[package]] org = "ballerinax" name = "health.fhir.r4.uscore501" -version = "1.3.4" +version = "1.4.0" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "log"}, @@ -395,7 +399,7 @@ modules = [ [[package]] org = "ballerinax" name = "health.fhir.r4.validator" -version = "4.2.2" +version = "4.3.0" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "http"}, @@ -412,7 +416,7 @@ modules = [ [[package]] org = "ballerinax" name = "health.fhirr4" -version = "1.3.3" +version = "1.4.0" dependencies = [ {org = "ballerina", name = "http"}, {org = "ballerina", name = "io"}, From a6ffdf3af1ca1938e174e84a986581f785166e9b Mon Sep 17 00:00:00 2001 From: isuruh15 Date: Tue, 11 Feb 2025 09:16:07 +0530 Subject: [PATCH 08/10] added to ci build --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12a9381..8770fae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ env: EHR_PATTERN: ehr-connectivity/*/* MISC_PATTERN: miscellaneous/*/* TRANSFORMATION_PATTERN: transformation/*/* + REGULATION_PATTERN: regulation/*/*/* BALLERINA_VERSION: 2201.10.2 jobs: @@ -53,7 +54,7 @@ jobs: # Remove duplicates from the CHANGED_FILES_ARRAY declare -A UNIQUE_PATHS_MAP for file in "${CHANGED_FILES_ARRAY[@]}"; do - if [[ $file == $CONFORMANCE_PATTERN ]] || [[ $file == $EHR_PATTERN ]] || [[ $file == $MISC_PATTERN ]] || [[ $file == $TRANSFORMATION_PATTERN ]]; then + if [[ $file == $CONFORMANCE_PATTERN ]] || [[ $file == $EHR_PATTERN ]] || [[ $file == $MISC_PATTERN ]] || [[ $file == $TRANSFORMATION_PATTERN ]] || [[ $file == $REGULATION_PATTERN ]]; then # Condition for 2 level projects EXTRACTED_PATH=$(echo "$file" | cut -d '/' -f 1-2) echo "${EXTRACTED_PATH}" From 6481d0f724b9b998505f0c55e6548e677bc77864 Mon Sep 17 00:00:00 2001 From: isuruh15 Date: Tue, 11 Feb 2025 09:26:43 +0530 Subject: [PATCH 09/10] updated path of ci build --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8770fae..488aff7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,11 +54,16 @@ jobs: # Remove duplicates from the CHANGED_FILES_ARRAY declare -A UNIQUE_PATHS_MAP for file in "${CHANGED_FILES_ARRAY[@]}"; do - if [[ $file == $CONFORMANCE_PATTERN ]] || [[ $file == $EHR_PATTERN ]] || [[ $file == $MISC_PATTERN ]] || [[ $file == $TRANSFORMATION_PATTERN ]] || [[ $file == $REGULATION_PATTERN ]]; then + if [[ $file == $CONFORMANCE_PATTERN ]] || [[ $file == $EHR_PATTERN ]] || [[ $file == $MISC_PATTERN ]] || [[ $file == $TRANSFORMATION_PATTERN ]] ; then # Condition for 2 level projects EXTRACTED_PATH=$(echo "$file" | cut -d '/' -f 1-2) echo "${EXTRACTED_PATH}" fi + if [[ $file == $REGULATION_PATTERN ]]; then + # Condition for 3 level projects + EXTRACTED_PATH=$(echo "$file" | cut -d '/' -f 1-3) + echo "${EXTRACTED_PATH}" + fi if [[ $EXTRACTED_PATH ]]; then UNIQUE_PATHS_MAP[$EXTRACTED_PATH]=1 From 2b3d53b875b75ad6e35fe629287c0c22cdf9e46f Mon Sep 17 00:00:00 2001 From: isuruh15 Date: Tue, 11 Feb 2025 09:35:13 +0530 Subject: [PATCH 10/10] fix test failures --- .../tests/Config.toml | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 regulation/cms-0057-f/bulk-export-client-service/tests/Config.toml diff --git a/regulation/cms-0057-f/bulk-export-client-service/tests/Config.toml b/regulation/cms-0057-f/bulk-export-client-service/tests/Config.toml new file mode 100644 index 0000000..609392e --- /dev/null +++ b/regulation/cms-0057-f/bulk-export-client-service/tests/Config.toml @@ -0,0 +1,27 @@ + +[sourceServerConfig] +baseUrl = "https://bulk-data.smarthealthit.org" +contextPath = "/eyJlcnIiOiIiLCJwYWdlIjoxMDAwMCwiZHVyIjoxMCwidGx0IjoxNSwibSI6MSwic3R1Ijo0LCJkZWwiOjAsInNlY3VyZSI6MH0/fhir/Patient" +tokenUrl = "https://bulk-data.smarthealthit.org/auth/token" +fileServerUrl = "https://bulk-data.smarthealthit.org" +clientId = "bulk-export-client" +clientSecret = "bulk-export-client-secret" +scopes = ["admin"] +defaultIntervalInSec = 5.0 + +[clientServiceConfig] +port = 9099 +authEnabled = false +targetDirectory = "files" + +[targetServerConfig] +type = "fhir" +host = "localhost" +port = 21 +username = "admin" +password = "admin" +directory = "/target/files" + + + +