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 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>

View File

@ -152,3 +152,49 @@ func (suite *SourceHandlerTestSuite) TestCreateManualSourceHandler() {
}, 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 {environment} from '../../../environments/environment';
import {BackgroundJobSyncData} from '../../models/fasten/background-job';
import {extractErrorFromResponse, replaceErrors} from '../../../lib/utils/error_extract';
@Component({
selector: 'app-medical-sources-connected',
@ -217,7 +218,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
const toastNotification = new ToastNotification()
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.link = {
text: "View Details",
@ -233,7 +234,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
const toastNotification = new ToastNotification()
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
this.toastService.show(toastNotification)
console.error(err)
@ -253,7 +254,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
}
errData.error_data = {
summary: toastNotification.message,
error: JSON.stringify(err, this.replaceErrors),
error: JSON.stringify(err, replaceErrors),
stack: err.stack
}
@ -262,31 +263,31 @@ export class MedicalSourcesConnectedComponent implements OnInit {
})
}
//https://stackoverflow.com/a/18391400/1157633
extractErrorFromResponse(errResp: any): string {
let errMsg = ""
if(errResp.name == "HttpErrorResponse" && errResp.error && errResp.error?.error){
errMsg = errResp.error.error
} else {
errMsg = JSON.stringify(errResp, this.replaceErrors)
}
return errMsg
}
// //https://stackoverflow.com/a/18391400/1157633
// 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
replaceErrors(key, value) {
if (value instanceof Error) {
var error = {};
Object.getOwnPropertyNames(value).forEach(function (propName) {
error[propName] = value[propName];
});
return error;
}
return value;
}
// //stringify error objects
// replaceErrors(key, value) {
// if (value instanceof Error) {
// var error = {};
//
// Object.getOwnPropertyNames(value).forEach(function (propName) {
// error[propName] = value[propName];
// });
//
// return error;
// }
//
// return value;
// }
/**
* 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()
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)
console.log(err)
@ -425,7 +426,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
const toastNotification = new ToastNotification()
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)
console.log(err)
})

View File

@ -39,6 +39,10 @@
<ngx-dropzone-label>{{ f.name }} ({{ f.type }})</ngx-dropzone-label>
</ngx-dropzone-preview>
</ngx-dropzone>
<div *ngIf="uploadErrorMsg" class="alert alert-danger mt-3" role="alert">
<strong>Error:</strong> {{uploadErrorMsg}}
</div>
</div><!-- col -->
</div><!-- row -->
@ -155,15 +159,15 @@
<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>
<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>.
Your data will <strong>NEVER</strong> be shared with any third parties.
</p>
<div class="d-flex h-100">
<div class="mx-auto my-auto">
<a href="https://www.health-samurai.io/" externalLink>
<img style="height:50px" src="assets/images/health-samurai-logo.png">
<a href="https://www.metriport.com/" externalLink>
<img style="height:50px" src="assets/images/metriport-logo.png">
</a>
</div>
</div>

View File

@ -19,6 +19,7 @@ import * as _ from 'lodash';
import {PatientAccessBrand} from '../../models/patient-access-brands';
import {PlatformService} from '../../services/platform.service';
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)
@ -39,7 +40,7 @@ export class MedicalSourcesComponent implements OnInit {
environment_name = environment.environment_name
uploadedFile: File[] = []
uploadErrorMsg: string = ""
availableLighthouseBrandList: SourceListItem[] = []
searchTermUpdate = new BehaviorSubject<string>("");
@ -313,7 +314,7 @@ export class MedicalSourcesComponent implements OnInit {
* @param event
*/
public async uploadSourceBundleHandler(event) {
this.uploadErrorMsg = ""
let processingFile = event.addedFiles[0] as File
this.uploadedFile = [processingFile]
@ -321,8 +322,16 @@ export class MedicalSourcesComponent implements OnInit {
let shouldConvert = await this.showCcdaWarningModal()
if(shouldConvert){
let convertedFile = await this.platformApi.convertCcdaToFhir(processingFile).toPromise()
processingFile = convertedFile
try {
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 {
this.uploadedFile = []
return
@ -334,7 +343,10 @@ export class MedicalSourcesComponent implements OnInit {
this.fastenApi.createManualSource(processingFile).subscribe(
(respData) => {
},
(err) => {console.log(err)},
(err) => {
console.log(err)
this.uploadErrorMsg = "Error uploading file: " + (extractErrorFromResponse(err)|| "Unknown Error")
},
() => {
this.uploadedFile = []
}

View File

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