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:
parent
a6c0257495
commit
808cb103f7
|
@ -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>
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
}
|
||||||
|
|
3794
backend/pkg/web/handler/testdata/ccda_to_fhir_converted_C-CDA_R2-1_CCD.xml.json
vendored
Normal file
3794
backend/pkg/web/handler/testdata/ccda_to_fhir_converted_C-CDA_R2-1_CCD.xml.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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){
|
||||||
|
try {
|
||||||
let convertedFile = await this.platformApi.convertCcdaToFhir(processingFile).toPromise()
|
let convertedFile = await this.platformApi.convertCcdaToFhir(processingFile).toPromise()
|
||||||
processingFile = convertedFile
|
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 = []
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 |
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue