using metriport fhir converter (#486)

* using metriport fhir converter

fixes: https://github.com/fastenhealth/fasten-onprem/issues/472
fixes: https://github.com/fastenhealth/fasten-onprem/issues/291

* working metriport converter. Forked to implement a fix.
This commit is contained in:
Jason Kulatunga 2024-06-27 13:34:13 -07:00 committed by GitHub
parent a6c0257495
commit 808cb103f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 3923 additions and 40 deletions

View File

@ -171,5 +171,4 @@ I'd also like to thank the following Corporate Sponsors:
<a href="https://depot.dev/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/depot.png" height="100px" /></a> <a href="https://depot.dev/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/depot.png" height="100px" /></a>
<a style="padding-left:5px" href="https://www.macminivault.com/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/macminivault.png" height="100px" /></a> <a style="padding-left:5px" href="https://www.macminivault.com/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/macminivault.png" height="100px" /></a>
<a style="padding-left:5px" href="https://www.health-samurai.io/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/health-samurai-logo.png" height="100px" /></a>
<a style="padding-left:5px" href="https://www.browserstack.com/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/browserstack.png" height="100px" /></a> <a style="padding-left:5px" href="https://www.browserstack.com/"><img src="https://raw.githubusercontent.com/fastenhealth/docs/main/img/sponsors/browserstack.png" height="100px" /></a>

View File

@ -152,3 +152,49 @@ func (suite *SourceHandlerTestSuite) TestCreateManualSourceHandler() {
}, summary.ResourceTypeCounts[3]) }, summary.ResourceTypeCounts[3])
} }
// bug: https://github.com/fastenhealth/fasten-onprem/pull/486
func (suite *SourceHandlerTestSuite) TestCreateManualSourceHandler_ShouldExtractPatientIdFromConvertedCCDA() {
//setup
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Set(pkg.ContextKeyTypeLogger, logrus.WithField("test", suite.T().Name()))
ctx.Set(pkg.ContextKeyTypeDatabase, suite.AppRepository)
ctx.Set(pkg.ContextKeyTypeConfig, suite.AppConfig)
ctx.Set(pkg.ContextKeyTypeEventBusServer, suite.AppEventBus)
ctx.Set(pkg.ContextKeyTypeAuthUsername, "test_username")
//test
req, err := CreateManualSourceHttpRequestFromFile("testdata/ccda_to_fhir_converted_C-CDA_R2-1_CCD.xml.json")
require.NoError(suite.T(), err)
ctx.Request = req
CreateManualSource(ctx)
//assert
require.Equal(suite.T(), http.StatusOK, w.Code)
type ResponseWrapper struct {
Data struct {
UpdatedResources []string `json:"UpdatedResources"`
TotalResources int `json:"TotalResources"`
} `json:"data"`
Success bool `json:"success"`
Source models.SourceCredential `json:"source"`
}
var respWrapper ResponseWrapper
err = json.Unmarshal(w.Body.Bytes(), &respWrapper)
require.NoError(suite.T(), err)
require.Equal(suite.T(), true, respWrapper.Success)
require.Equal(suite.T(), "manual", string(respWrapper.Source.PlatformType))
require.Equal(suite.T(), 65, respWrapper.Data.TotalResources)
summary, err := suite.AppRepository.GetSourceSummary(ctx, respWrapper.Source.ID.String())
require.NoError(suite.T(), err)
require.Equal(suite.T(), map[string]interface{}{
"count": int64(1),
"resource_type": "Consent",
"source_id": respWrapper.Source.ID.String(),
}, summary.ResourceTypeCounts[3])
}

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@ import {SourceState} from '../../models/fasten/source-state';
import {PatientAccessBrand} from '../../models/patient-access-brands'; import {PatientAccessBrand} from '../../models/patient-access-brands';
import {environment} from '../../../environments/environment'; import {environment} from '../../../environments/environment';
import {BackgroundJobSyncData} from '../../models/fasten/background-job'; import {BackgroundJobSyncData} from '../../models/fasten/background-job';
import {extractErrorFromResponse, replaceErrors} from '../../../lib/utils/error_extract';
@Component({ @Component({
selector: 'app-medical-sources-connected', selector: 'app-medical-sources-connected',
@ -217,7 +218,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
const toastNotification = new ToastNotification() const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Error toastNotification.type = ToastType.Error
toastNotification.message = `An error occurred while finalizing external data source and starting sync: '${this.extractErrorFromResponse(err)}'` toastNotification.message = `An error occurred while finalizing external data source and starting sync: '${extractErrorFromResponse(err)}'`
toastNotification.autohide = false toastNotification.autohide = false
toastNotification.link = { toastNotification.link = {
text: "View Details", text: "View Details",
@ -233,7 +234,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
const toastNotification = new ToastNotification() const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Error toastNotification.type = ToastType.Error
toastNotification.message = `An error occurred while initializing external data source connection: '${this.extractErrorFromResponse(err)}'` toastNotification.message = `An error occurred while initializing external data source connection: '${extractErrorFromResponse(err)}'`
toastNotification.autohide = false toastNotification.autohide = false
this.toastService.show(toastNotification) this.toastService.show(toastNotification)
console.error(err) console.error(err)
@ -253,7 +254,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
} }
errData.error_data = { errData.error_data = {
summary: toastNotification.message, summary: toastNotification.message,
error: JSON.stringify(err, this.replaceErrors), error: JSON.stringify(err, replaceErrors),
stack: err.stack stack: err.stack
} }
@ -262,31 +263,31 @@ export class MedicalSourcesConnectedComponent implements OnInit {
}) })
} }
//https://stackoverflow.com/a/18391400/1157633 // //https://stackoverflow.com/a/18391400/1157633
extractErrorFromResponse(errResp: any): string { // extractErrorFromResponse(errResp: any): string {
let errMsg = "" // let errMsg = ""
if(errResp.name == "HttpErrorResponse" && errResp.error && errResp.error?.error){ // if(errResp.name == "HttpErrorResponse" && errResp.error && errResp.error?.error){
errMsg = errResp.error.error // errMsg = errResp.error.error
} else { // } else {
errMsg = JSON.stringify(errResp, this.replaceErrors) // errMsg = JSON.stringify(errResp, replaceErrors)
} // }
return errMsg // return errMsg
} // }
//stringify error objects // //stringify error objects
replaceErrors(key, value) { // replaceErrors(key, value) {
if (value instanceof Error) { // if (value instanceof Error) {
var error = {}; // var error = {};
//
Object.getOwnPropertyNames(value).forEach(function (propName) { // Object.getOwnPropertyNames(value).forEach(function (propName) {
error[propName] = value[propName]; // error[propName] = value[propName];
}); // });
//
return error; // return error;
} // }
//
return value; // return value;
} // }
/** /**
* https://github.com/smart-on-fhir/client-js/blob/8f64b770dbcd0abd30646e239cd446dfa4d831f6/src/lib.ts#L311 * https://github.com/smart-on-fhir/client-js/blob/8f64b770dbcd0abd30646e239cd446dfa4d831f6/src/lib.ts#L311
@ -382,7 +383,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
const toastNotification = new ToastNotification() const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Error toastNotification.type = ToastType.Error
toastNotification.message = `An error occurred while updating source (${source.display}): ${this.extractErrorFromResponse(err)}` toastNotification.message = `An error occurred while updating source (${source.display}): ${extractErrorFromResponse(err)}`
this.toastService.show(toastNotification) this.toastService.show(toastNotification)
console.log(err) console.log(err)
@ -425,7 +426,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
const toastNotification = new ToastNotification() const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Error toastNotification.type = ToastType.Error
toastNotification.message = `An error occurred while deleting source (${sourceDisplayName}): ${this.extractErrorFromResponse(err)}` toastNotification.message = `An error occurred while deleting source (${sourceDisplayName}): ${extractErrorFromResponse(err)}`
this.toastService.show(toastNotification) this.toastService.show(toastNotification)
console.log(err) console.log(err)
}) })

View File

@ -39,6 +39,10 @@
<ngx-dropzone-label>{{ f.name }} ({{ f.type }})</ngx-dropzone-label> <ngx-dropzone-label>{{ f.name }} ({{ f.type }})</ngx-dropzone-label>
</ngx-dropzone-preview> </ngx-dropzone-preview>
</ngx-dropzone> </ngx-dropzone>
<div *ngIf="uploadErrorMsg" class="alert alert-danger mt-3" role="alert">
<strong>Error:</strong> {{uploadErrorMsg}}
</div>
</div><!-- col --> </div><!-- col -->
</div><!-- row --> </div><!-- row -->
@ -155,15 +159,15 @@
<div class="modal-body"> <div class="modal-body">
<h6>Fasten does not natively support <a href="https://en.wikipedia.org/wiki/Consolidated_Clinical_Document_Architecture" externalLink >CCDA</a> Health Records</h6> <h6>Fasten does not natively support <a href="https://en.wikipedia.org/wiki/Consolidated_Clinical_Document_Architecture" externalLink >CCDA</a> Health Records</h6>
<p>However we can convert it automatically using software generously donated by the team at <a href="https://www.health-samurai.io/" externalLink>Health Samurai</a><br/><br/> <p>However we can convert it automatically using <a href="https://github.com/metriport/metriport/tree/master/packages/fhir-converter" externalLink>open-source software</a> generously provided the team at <a href="https://www.metriport.com/" externalLink>Metriport</a><br/><br/>
This converter is hosted by <a href="https://www.fastenhealth.com/" externalLink>Fasten Health, Inc.</a> and is subject to our <a href="https://policy.fastenhealth.com/privacy_policy.html" externalLink>Privacy Policy</a>. This converter is hosted by <a href="https://www.fastenhealth.com/" externalLink>Fasten Health, Inc.</a> and is subject to our <a href="https://policy.fastenhealth.com/privacy_policy.html" externalLink>Privacy Policy</a>.
Your data will <strong>NEVER</strong> be shared with any third parties. Your data will <strong>NEVER</strong> be shared with any third parties.
</p> </p>
<div class="d-flex h-100"> <div class="d-flex h-100">
<div class="mx-auto my-auto"> <div class="mx-auto my-auto">
<a href="https://www.health-samurai.io/" externalLink> <a href="https://www.metriport.com/" externalLink>
<img style="height:50px" src="assets/images/health-samurai-logo.png"> <img style="height:50px" src="assets/images/metriport-logo.png">
</a> </a>
</div> </div>
</div> </div>

View File

@ -19,6 +19,7 @@ import * as _ from 'lodash';
import {PatientAccessBrand} from '../../models/patient-access-brands'; import {PatientAccessBrand} from '../../models/patient-access-brands';
import {PlatformService} from '../../services/platform.service'; import {PlatformService} from '../../services/platform.service';
import {FormRequestHealthSystemComponent} from '../../components/form-request-health-system/form-request-health-system.component'; import {FormRequestHealthSystemComponent} from '../../components/form-request-health-system/form-request-health-system.component';
import {extractErrorFromResponse} from '../../../lib/utils/error_extract';
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120) export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
@ -39,7 +40,7 @@ export class MedicalSourcesComponent implements OnInit {
environment_name = environment.environment_name environment_name = environment.environment_name
uploadedFile: File[] = [] uploadedFile: File[] = []
uploadErrorMsg: string = ""
availableLighthouseBrandList: SourceListItem[] = [] availableLighthouseBrandList: SourceListItem[] = []
searchTermUpdate = new BehaviorSubject<string>(""); searchTermUpdate = new BehaviorSubject<string>("");
@ -313,7 +314,7 @@ export class MedicalSourcesComponent implements OnInit {
* @param event * @param event
*/ */
public async uploadSourceBundleHandler(event) { public async uploadSourceBundleHandler(event) {
this.uploadErrorMsg = ""
let processingFile = event.addedFiles[0] as File let processingFile = event.addedFiles[0] as File
this.uploadedFile = [processingFile] this.uploadedFile = [processingFile]
@ -321,8 +322,16 @@ export class MedicalSourcesComponent implements OnInit {
let shouldConvert = await this.showCcdaWarningModal() let shouldConvert = await this.showCcdaWarningModal()
if(shouldConvert){ if(shouldConvert){
let convertedFile = await this.platformApi.convertCcdaToFhir(processingFile).toPromise() try {
processingFile = convertedFile let convertedFile = await this.platformApi.convertCcdaToFhir(processingFile).toPromise()
processingFile = convertedFile
} catch(err){
console.error(err)
this.uploadErrorMsg = "Error converting file: " + (extractErrorFromResponse(err) || "Unknown Error")
this.uploadedFile = []
return
}
} else { } else {
this.uploadedFile = [] this.uploadedFile = []
return return
@ -334,7 +343,10 @@ export class MedicalSourcesComponent implements OnInit {
this.fastenApi.createManualSource(processingFile).subscribe( this.fastenApi.createManualSource(processingFile).subscribe(
(respData) => { (respData) => {
}, },
(err) => {console.log(err)}, (err) => {
console.log(err)
this.uploadErrorMsg = "Error uploading file: " + (extractErrorFromResponse(err)|| "Unknown Error")
},
() => { () => {
this.uploadedFile = [] this.uploadedFile = []
} }

View File

@ -9,6 +9,7 @@ import {LighthouseSourceSearch} from '../models/lighthouse/lighthouse-source-sea
import {environment} from '../../environments/environment'; import {environment} from '../../environments/environment';
import {ResponseWrapper} from '../models/response-wrapper'; import {ResponseWrapper} from '../models/response-wrapper';
import {map} from 'rxjs/operators'; import {map} from 'rxjs/operators';
import {uuidV4} from '../../lib/utils/uuid';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -22,8 +23,8 @@ export class PlatformService {
throw new Error("Invalid CCDA file") throw new Error("Invalid CCDA file")
} }
const endpointUrl = new URL(`https://api.platform.fastenhealth.com/v1/app/ccda-to-fhir`); const endpointUrl = new URL(`https://api.platform.fastenhealth.com/v1/app/convert/ccda/to/fhir?patientId=${uuidV4()}`);
return this._httpClient.post<string>(endpointUrl.toString(), ccdaFile, { headers: {'Content-Type': 'application/cda+xml'} }) return this._httpClient.post<string>(endpointUrl.toString(), ccdaFile, { headers: {'Content-Type': 'application/xml', 'Accept':'application/json'} })
.pipe( .pipe(
map((responseJson: string) => { map((responseJson: string) => {
return new File([JSON.stringify(responseJson)], ccdaFile.name + ".converted.json", {type: "application/json", lastModified: ccdaFile.lastModified || Date.now()}) return new File([JSON.stringify(responseJson)], ccdaFile.name + ".converted.json", {type: "application/json", lastModified: ccdaFile.lastModified || Date.now()})

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,26 @@
//https://stackoverflow.com/a/18391400/1157633
export function extractErrorFromResponse(errResp: any): string {
let errMsg = ""
if(errResp.name == "HttpErrorResponse" && errResp.error && errResp.error?.error){
errMsg = errResp.error.error
} else {
errMsg = JSON.stringify(errResp, replaceErrors)
}
return errMsg
}
//stringify error objects
export function replaceErrors(key, value) {
if (value instanceof Error) {
var error = {};
Object.getOwnPropertyNames(value).forEach(function (propName) {
error[propName] = value[propName];
});
return error;
}
return value;
}