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 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>
|
||||
|
|
|
@ -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])
|
||||
|
||||
}
|
||||
|
|
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 {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)
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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){
|
||||
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 = []
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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