Skip to content

Commit

Permalink
Merge FHIR Bug Fixes To Release 6.1.0 (#5160)
Browse files Browse the repository at this point in the history
* Fixes #5095 Patient,Goals,Immunization (#5097)

Not sure what happened but it looks like provider_uuid got stomped on in
CarePlan and Immunization.  Adding that in to our uuid mappings fixes
the problem with the resources returning 500.

For Patient it looks like the patient/* context was broken in the
Patient resource endpoint when we refactored the parameter name for
documentation purposes.  I've updated the $id parameter in the patient
route to be $uuid.

* Openemr fhir single patient api #5122 (#5129)

* Fix SurgeryService query.

FHIR searchAll was relying on a username column that doesn't exist.  Not
sure if some code got stomped on or how this ever passed our inferno
test suite.

* Fix Provenance queries.

Again not sure if code got stomped on but DiagnosticReports and
DocumentReference and Immunization Provenance were not working with
single patient api.

* FHIR document how medication works inside request

Added comments that explain how medication is embedded via the code
property inside of MedicationRequest.

* Fixes #5125,#5123 FHIR ids,Provenance

Reworked the FHIR ids for Provenance,CarePlan, and Goals.  FHIR R4.0.1
changed the logical id allowed fields (or at least inferno started
validating them) so we had to adjust how we handle these resources.  I
use a different surrogate key string which is valid in 4.0.1.  I also
handle the prior key data so we can still read and return the old format
so people's data doesn't break.

I also fix the issue where clinical notes,procedures and immunization weren't
returning provenance resources

* Fix style issues.

* Fix Care Plan unit tests.

We had too many fixture interdependencies now that the careplan also
joins on the forms table. Since I was testing the
surrogate key logic I stopped the getOne test and just focused on the
surrogateKey.

* Fixes #5137 Vitals save data absent value (#5138)

Make it so we can save a 0 value for columns in vitals to signify the
data is missing.

* Fixes #5127 mapped uuids not saving in registry (#5139)

Made it so the uuids for mapped resources such as vitals are being
populated in the uuid registry.  Users will need to run the
sql_upgrade.php for any existing installation in order for the vitals to
get their registry populated.
  • Loading branch information
adunsulag committed Apr 16, 2022
1 parent 9a3090f commit b11a160
Show file tree
Hide file tree
Showing 18 changed files with 240 additions and 51 deletions.
4 changes: 2 additions & 2 deletions _rest_routes.inc.php
Expand Up @@ -10636,10 +10636,10 @@
"GET /fhir/Patient/:uuid" => function ($uuid, HttpRestRequest $request) {
if ($request->isPatientRequest()) {
// only allow access to data of binded patient
if (empty($id) || ($id != $request->getPatientUUIDString())) {
if (empty($uuid) || ($uuid != $request->getPatientUUIDString())) {
throw new AccessDeniedException("patients", "demo", "patient id invalid");
}
$id = $request->getPatientUUIDString();
$uuid = $request->getPatientUUIDString();
} else {
RestConfig::authorization_check("patients", "demo");
}
Expand Down
4 changes: 2 additions & 2 deletions interface/forms/vitals/templates/vitals/vitals_textbox.tpl
Expand Up @@ -9,10 +9,10 @@
<td class='currentvalues p-2'>
{if isset($vitalsStringFormat) }
<input type="text" class="form-control" size='5' name='{$input|attr}' id='{$input|attr}_input'
value="{if $vitals->$vitalsValue() != 0}{$vitals->$vitalsValue()|string_format:$vitalsStringFormat|attr}{/if}"/>
value="{if is_numeric($vitals->$vitalsValue()) }{$vitals->$vitalsValue()|string_format:$vitalsStringFormat|attr}{/if}"/>
{else}
<input type="text" class="form-control" size='5' name='{$input|attr}' id='{$input|attr}_input'
value="{if $vitals->$vitalsValue() != 0}{$vitals->$vitalsValue()|attr}{/if}"/>
value="{if is_numeric($vitals->$vitalsValue())}{$vitals->$vitalsValue()|attr}{/if}"/>
{/if}

</td>
Expand Down
Expand Up @@ -24,7 +24,7 @@
<td class='currentvalues p-2'>
{/if}
<input type="text" class="form-control" size='5' name='{$input|attr}' id='{$input|attr}_input'
value="{if $vitals->$vitalsValue() != 0}{$vitals->$vitalsValue()|attr}{/if}"
value="{if is_numeric($vitals->$vitalsValue()) }{$vitals->$vitalsValue()|attr}{/if}"
onChange="convUnit('usa', {$unit|attr_js}, '{$input|attr}_input')" title='{$vitalsValueUSAHelpTitle|default:''|xlt}'/>
</td>
<td class="editonly">
Expand Down Expand Up @@ -61,7 +61,7 @@
{/if}
<!-- Note we intentionally use vitalsValue not vitalValuesMetric because of how data is stored internally -->
<input type="text" class="form-control" size='5' id='{$input|attr}_input_metric'
value="{if $vitals->$vitalsValue() != 0}{$vitals->$vitalsValueMetric()|attr}{/if}"
value="{if is_numeric($vitals->$vitalsValue()) }{$vitals->$vitalsValueMetric()|attr}{/if}"
onChange="convUnit('metric', {$unit|attr_js}, '{$input|attr}_input')"/>
</td>
<td class="editonly">
Expand Down
28 changes: 14 additions & 14 deletions src/Common/Forms/FormVitals.php
Expand Up @@ -226,7 +226,7 @@ public function get_weight_metric()
}
public function set_weight($w)
{
if (!empty($w) && is_numeric($w)) {
if (is_numeric($w)) {
$this->weight = $w;
}
}
Expand All @@ -251,7 +251,7 @@ public function get_height_metric()
}
public function set_height($h)
{
if (!empty($h) && is_numeric($h)) {
if (is_numeric($h)) {
$this->height = $h;
}
}
Expand All @@ -265,7 +265,7 @@ public function get_temperature_metric()
}
public function set_temperature($t)
{
if (!empty($t) && is_numeric($t)) {
if (is_numeric($t)) {
$this->temperature = $t;
}
}
Expand All @@ -286,7 +286,7 @@ public function get_pulse()
}
public function set_pulse($p)
{
if (!empty($p) && is_numeric($p)) {
if (is_numeric($p)) {
$this->pulse = $p;
}
}
Expand All @@ -296,7 +296,7 @@ public function get_respiration()
}
public function set_respiration($r)
{
if (!empty($r) && is_numeric($r)) {
if (is_numeric($r)) {
$this->respiration = $r;
}
}
Expand All @@ -320,7 +320,7 @@ public function get_BMI_short()
}
public function set_BMI($bmi)
{
if (!empty($bmi) && is_numeric($bmi)) {
if (is_numeric($bmi)) {
$this->BMI = $bmi;
}
}
Expand All @@ -342,7 +342,7 @@ public function get_waist_circ_metric()
}
public function set_waist_circ($w)
{
if (!empty($w) && is_numeric($w)) {
if (is_numeric($w)) {
$this->waist_circ = $w;
}
}
Expand All @@ -356,7 +356,7 @@ public function get_head_circ_metric()
}
public function set_head_circ($h)
{
if (!empty($h) && is_numeric($h)) {
if (is_numeric($h)) {
$this->head_circ = $h;
}
}
Expand All @@ -366,7 +366,7 @@ public function get_oxygen_saturation()
}
public function set_oxygen_saturation($o)
{
if (!empty($o) && is_numeric($o)) {
if (is_numeric($o)) {
$this->oxygen_saturation = $o;
}
}
Expand All @@ -377,7 +377,7 @@ public function get_oxygen_flow_rate()
}
public function set_oxygen_flow_rate($o)
{
if (!empty($o) && is_numeric($o)) {
if (is_numeric($o)) {
$this->oxygen_flow_rate = $o;
} else {
$this->oxygen_flow_rate = 0.00;
Expand All @@ -391,7 +391,7 @@ public function get_inhaled_oxygen_concentration()

public function set_inhaled_oxygen_concentration($value)
{
if (!empty($value) && is_numeric($value)) {
if (is_numeric($value)) {
$this->inhaled_oxygen_concentration = $value;
} else {
$this->inhaled_oxygen_concentration = 0.00;
Expand All @@ -404,7 +404,7 @@ public function get_ped_weight_height()
}
public function set_ped_weight_height($o)
{
if (!empty($o) && is_numeric($o)) {
if (is_numeric($o)) {
$this->ped_weight_height = $o;
} else {
$this->ped_weight_height = 0.00;
Expand All @@ -417,7 +417,7 @@ public function get_ped_bmi()
}
public function set_ped_bmi($o)
{
if (!empty($o) && is_numeric($o)) {
if (is_numeric($o)) {
$this->ped_bmi = $o;
} else {
$this->ped_bmi = 0.00;
Expand All @@ -430,7 +430,7 @@ public function get_ped_head_circ()
}
public function set_ped_head_circ($o)
{
if (!empty($o) && is_numeric($o)) {
if (is_numeric($o)) {
$this->ped_head_circ = $o;
} else {
$this->ped_head_circ = 0.00;
Expand Down
2 changes: 2 additions & 0 deletions src/Common/Uuid/UuidMapping.php
Expand Up @@ -91,6 +91,8 @@ public static function createMappingRecordForResourcePaths($targetUuid, $resourc
sqlStatementNoLog($insertStatement, $bindValues, true);
$index++;
}
// now insert the mapped uuids into the registry
$uuidRegistry->insertUuidsIntoRegistry($uuids);
}
return $uuids;
}
Expand Down
22 changes: 22 additions & 0 deletions src/Common/Uuid/UuidRegistry.php
Expand Up @@ -134,6 +134,11 @@ public static function populateAllMissingUuids($log = true)
self::appendPopulateLog('uuid_mapping', $mappedCounter, $logEntryComment);
}

// To rectify a bug where mapped uuids were created but nothing in the UUID register for vital observations we
// will populate the UUIDRegistry
$mappedRegistryUuidCounter = self::createMissingMappedUuids();
self::appendPopulateLog('uuid_registry', $mappedRegistryUuidCounter, $logEntryComment);

if (!empty($logEntryComment)) {
$logEntryComment = rtrim($logEntryComment, ', ');
}
Expand All @@ -151,6 +156,23 @@ public static function populateAllMissingUuids($log = true)
}
}

/**
* Creates registry entries for missing uuids in uuid_mapping that are not in uuid_registry. Returns the count of
* the records that were created.
* @return int
*/
private static function createMissingMappedUuids()
{
$createdRows = 0;
$sql = "INSERT INTO `uuid_registry`(`uuid`,`table_name`,`table_id`,`mapped`) "
. " SELECT `uuid_mapping`.`uuid`,'uuid_mapping','id',1 FROM `uuid_mapping` LEFT JOIN `uuid_registry` registry2 ON `uuid_mapping`.`uuid` = registry2.uuid WHERE registry2.uuid IS NULL";
$result = sqlStatementNoLog($sql, []);
if ($result !== false) {
$createdRows = generic_sql_affected_rows();
}
return $createdRows;
}

/**
* Returns the uuid registry record for a given uuid.
* @param string|binary $uuid The uuid to search
Expand Down
44 changes: 38 additions & 6 deletions src/Services/CarePlanService.php
Expand Up @@ -30,7 +30,17 @@

class CarePlanService extends BaseService
{
const SURROGATE_KEY_SEPARATOR = "_";
// Note: FHIR 4.0.1 id columns put a constraint on ids such that:
// Ids can be up to 64 characters long, and contain any combination of upper and lowercase ASCII letters,
// numerals, "-" and ".". Logical ids are opaque to the resource server and should NOT be changed once they've
// been issued by the resource server
// Up to OpenEMR 6.1.0 patch 0 we used underscores as our separator
const SURROGATE_KEY_SEPARATOR_V1 = "_";
// use the abbreviation SK for Surrogate key and hyphens. Since Logical ids are opaque we can do this as long as
// our UUID NEVER generates a two digit hyphenated id which none of the standards currently do.
// the best approach would be to completely overhaul Careplan but for historical reasons we aren't doing that right now.
const SURROGATE_KEY_SEPARATOR_V2 = "-SK-";
const V2_TIMESTAMP = 1649476800; // strtotime("2022-04-09");
private const PATIENT_TABLE = "patient_data";
private const ENCOUNTER_TABLE = "form_encounter";
private const CARE_PLAN_TABLE = "form_care_plan";
Expand All @@ -52,7 +62,7 @@ class CarePlanService extends BaseService

function getUuidFields(): array
{
return ['puuid', 'euuid'];
return ['puuid', 'euuid', 'provider_uuid'];
}

public function __construct($carePlanType = self::TYPE_PLAN_OF_CARE)
Expand Down Expand Up @@ -134,7 +144,9 @@ public function search($search, $isAndCondition = true)
,patients.pid
,encounters.euuid
,encounters.eid
,fcp.form_id
,fcp_forms.form_id
,fcp_forms.creation_date
,UNIX_TIMESTAMP(fcp_forms.creation_date) AS 'creation_timestamp'
,fcp.code
,fcp.codetext
,fcp.description
Expand All @@ -147,7 +159,7 @@ public function search($search, $isAndCondition = true)
FROM
(
select
id AS form_id
id
,code
,codetext
,description
Expand All @@ -171,6 +183,16 @@ public function search($search, $isAndCondition = true)
FROM
form_encounter
) encounters ON fcp.encounter = encounters.eid
-- we need the form date information
JOIN (
SELECT
id
,form_id
,encounter AS form_encounter
,date AS creation_date
FROM forms
WHERE form_name = 'Care Plan Form'
) fcp_forms ON fcp.id = fcp_forms.form_id AND encounters.eid = fcp_forms.form_encounter
LEFT JOIN (
select
pid
Expand Down Expand Up @@ -270,9 +292,15 @@ private function populateSurrogateSearchFieldsForUUID(TokenSearchField $fieldUUI
*/
public function getSurrogateKeyForRecord(array $record)
{
// note that logical ids are allowed to be 64 characters. Our UUID is 32 characters so as long as
// the form_id + separator never exceeds 64 characters we are good here.
$form_id = $record['form_id'] ?? '';
$encounter = $record['euuid'] ?? '';
return $encounter . self::SURROGATE_KEY_SEPARATOR . $form_id;
$separator = self::SURROGATE_KEY_SEPARATOR_V2;
if (intval($record['creation_timestamp'] ?? 0) <= self::V2_TIMESTAMP) {
$separator = self::SURROGATE_KEY_SEPARATOR_V1;
}
return $encounter . $separator . $form_id;
}

/**
Expand All @@ -282,7 +310,11 @@ public function getSurrogateKeyForRecord(array $record)
*/
public function splitSurrogateKeyIntoParts($key)
{
$parts = explode(self::SURROGATE_KEY_SEPARATOR, $key);
$delimiter = self::SURROGATE_KEY_SEPARATOR_V2;
if (strpos($key, self::SURROGATE_KEY_SEPARATOR_V1) !== false) {
$delimiter = self::SURROGATE_KEY_SEPARATOR_V1;
}
$parts = explode($delimiter, $key);
$key = [
"euuid" => $parts[0] ?? ""
,"form_id" => $parts[1] ?? ""
Expand Down
Expand Up @@ -230,7 +230,12 @@ public function createProvenanceResource($dataRecord, $encode = false)
throw new \BadMethodCallException("Data record should be correct instance class");
}
$fhirProvenanceService = new FhirProvenanceService();
$fhirProvenance = $fhirProvenanceService->createProvenanceForDomainResource($dataRecord, $dataRecord->getAuthor());
$authors = $dataRecord->getAuthor();
$author = null;
if (!empty($authors)) {
$author = reset($authors); // grab the first one, as we only populate one anyways.
}
$fhirProvenance = $fhirProvenanceService->createProvenanceForDomainResource($dataRecord, $author);
if ($encode) {
return json_encode($fhirProvenance);
} else {
Expand Down
Expand Up @@ -212,7 +212,12 @@ public function createProvenanceResource($dataRecord, $encode = false)
}

$fhirProvenanceService = new FhirProvenanceService();
$fhirProvenance = $fhirProvenanceService->createProvenanceForDomainResource($dataRecord, $dataRecord->getAuthor());
$authors = $dataRecord->getAuthor();
$author = null;
if (!empty($authors)) {
$author = reset($authors); // grab the first one, as we only populate one anyways.
}
$fhirProvenance = $fhirProvenanceService->createProvenanceForDomainResource($dataRecord, $author);
if ($encode) {
return json_encode($fhirProvenance);
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/Services/FHIR/FhirCarePlanService.php
Expand Up @@ -113,7 +113,7 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false)
}

if (!empty($dataRecord['provider_uuid']) && !empty($dataRecord['provider_npi'])) {
$carePlanResource->getAuthor(UtilsService::createRelativeReference("Practitioner", $dataRecord['provider_uuid']));
$carePlanResource->setAuthor(UtilsService::createRelativeReference("Practitioner", $dataRecord['provider_uuid']));
}

if ($encode) {
Expand Down
10 changes: 7 additions & 3 deletions src/Services/FHIR/FhirImmunizationService.php
Expand Up @@ -3,6 +3,7 @@
namespace OpenEMR\Services\FHIR;

use OpenEMR\FHIR\R4\FHIRElement\FHIRMeta;
use OpenEMR\FHIR\R4\FHIRResource\FHIRImmunization\FHIRImmunizationPerformer;
use OpenEMR\Services\FHIR\FhirServiceBase;
use OpenEMR\Services\FHIR\Traits\BulkExportSupportAllOperationsTrait;
use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait;
Expand Down Expand Up @@ -165,7 +166,9 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false)
}

if (!empty($dataRecord['provider_uuid']) && !empty($dataRecord['provider_npi'])) {
$immunizationResource->addPerformer(UtilsService::createRelativeReference("Practitioner", $dataRecord['provider_uuid']));
$performer = new FHIRImmunizationPerformer();
$performer->setActor(UtilsService::createRelativeReference("Practitioner", $dataRecord['provider_uuid']));
$immunizationResource->addPerformer($performer);
}

// education is failing ONC validation, since we don't need it for ONC we are going to leave it off for now.
Expand Down Expand Up @@ -201,11 +204,12 @@ public function createProvenanceResource($dataRecord = array(), $encode = false)
throw new \BadMethodCallException("Data record should be correct instance class");
}
$fhirProvenanceService = new FhirProvenanceService();
$performer = null;
$author = null;
if (!empty($dataRecord->getPerformer())) {
$performer = current($dataRecord->getPerformer());
$author = $performer->getActor();
}
$fhirProvenance = $fhirProvenanceService->createProvenanceForDomainResource($dataRecord, $performer);
$fhirProvenance = $fhirProvenanceService->createProvenanceForDomainResource($dataRecord, $author);
if ($encode) {
return json_encode($fhirProvenance);
} else {
Expand Down
11 changes: 11 additions & 0 deletions src/Services/FHIR/FhirMedicationRequestService.php
Expand Up @@ -180,6 +180,17 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false)
$medRequestResource->setReportedBoolean(self::MEDICATION_REQUEST_REPORTED_PRIMARY_SOURCE);

// medication[x] required
/**
* US Core Requirements
* The MedicationRequest resources can represent a medication using either a code, or reference a Medication resource.
* When referencing a Medication resource, the resource may be contained or an external resource.
* The server systems are not required to support both a code and a reference, but SHALL support at least one of these methods.
* If an external reference to Medication is used, the server SHALL support the _include parameter for searching this element.
* The client application SHALL support all methods.
*
* NOTE: for our requirements we support ONLY the medicationCodeableConcept requirement ie code option and NOT the
* embedded medication.
*/
if (!empty($dataRecord['drugcode'])) {
// $rxnormCoding = new FHIRCoding();
$rxnormCode = UtilsService::createCodeableConcept($dataRecord['drugcode'], FhirCodeSystemConstants::RXNORM);
Expand Down

0 comments on commit b11a160

Please sign in to comment.