Skip to content

Commit

Permalink
Validations view Development (openemr#5329)
Browse files Browse the repository at this point in the history
* Validations view Development
- fix missing style sheel in export detail view

* catch an empty encounter end date

* setup for validation of CDA type documents
fix InsuranceCompanyService to return freshid
fix payer code for qrda
cat3 report controller fix

* UI for validation errors.

* exclude validation for command line imports
  • Loading branch information
sjpadgett committed May 20, 2022
1 parent 4c9fda2 commit 1908b85
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use C_Document;
use Document;
use CouchDB;
use OpenEMR\Services\Cda\CdaValidateDocuments;
use xmltoarray_parser_htmlfix;

class CarecoordinationController extends AbstractActionController
Expand Down Expand Up @@ -131,7 +132,6 @@ public function uploadAction()
if ($uploaded_documents[0]['id'] > 0) {
$_REQUEST["document_id"] = $uploaded_documents[0]['id'];
$_REQUEST["batch_import"] = 'YES';
// TODO validate error true then remove uploaded doc
$this->importAction();
}
} else {
Expand Down Expand Up @@ -161,6 +161,10 @@ public function uploadAction()
$fn = $r1['ad_fname'] == $r['ad_fname'];
$ln = $r1['ad_lname'] == $r['ad_lname'];
$dob = $r1['dob_raw'] == $r['dob_raw'];
if (!empty($r1['empty_qrda'] ?? 0)) {
$f = true;
$why = xlt('No QDM content.');
}
if ($dob) {
$f = true;
$why = xlt('Match DOB');
Expand Down Expand Up @@ -191,6 +195,7 @@ public function uploadAction()
}
}
}

$view = new ViewModel(array(
'records' => $records,
'category_id' => $category_details[0]['id'],
Expand All @@ -199,7 +204,9 @@ public function uploadAction()
'listenerObject' => $this->listenerObject
));
// I haven't a clue why this delay is needed to allow batch to work from fetch.
sleep(1);
if (!empty($upload)) {
sleep(1);
}
return $view;
}

Expand Down Expand Up @@ -256,10 +263,8 @@ public function importAction()
}

$document_id = $_REQUEST["document_id"];
$error = $this->getCarecoordinationTable()->import($document_id);
if ($error) {
return $error;
}
$this->getCarecoordinationTable()->import($document_id);

$view = new JsonModel();
$view->setTerminal(true);
return $view;
Expand Down Expand Up @@ -441,6 +446,10 @@ public function getEachCCDAComponentDetailsAction()
$temp = '';

switch ($component) {
case 'schematron':
$validate = new CdaValidateDocuments();
$temp .= $validate->createSchematronHtml($amid);
break;
case 'allergies':
$allergies_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'lists2');
if (count($allergies_audit) > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,12 @@ public function document_fetch($data): array
d.couch_revid,
d.url AS file_url,
d.id AS document_id,
d.document_data,
am.is_qrda_document,
ad.field_value as ad_lname,
ad1.field_value as ad_fname,
ad2.field_value as dob_raw,
(Select field_value From `audit_details` Where audit_master_id = am.id AND table_name = 'patient_data' AND field_name = 'qrda_empty') as qrda_empty,
(Select COUNT(field_name) From `audit_details` Where audit_master_id = am.id AND table_name = 'encounter' AND field_name = 'date') as enc_count,
(Select COUNT(field_name) From `audit_details` Where audit_master_id = am.id AND table_name = 'lists1' AND field_name = 'type' AND field_value = 'medical_problem') as prb_count,
(Select COUNT(field_name) From `audit_details` Where audit_master_id = am.id AND table_name = 'form_care_plan' AND field_name = 'date') as cp_count,
Expand Down Expand Up @@ -174,7 +176,7 @@ public function importNewPatient($document): void
* @param $document_id Document id
*/

public function importCore($xml_content)
public function importCore($xml_content, $doc_id = null)
{
$xml_content_new = preg_replace('#<br />#', '', $xml_content);
$xml_content_new = preg_replace('#<br/>#', '', $xml_content_new);
Expand All @@ -189,26 +191,29 @@ public function importCore($xml_content)

// Document various sectional components
$components = $xml['component']['structuredBody']['component'];
$qrda_log['message'] = $validation_log = null;
// test if a QRDA QDM CAT I document type from header OIDs
$qrda = $xml['templateId'][2]['root'] ?? null;
if ($qrda === '2.16.840.1.113883.10.20.24.1.2') {
$this->is_qrda_import = true;
$this->documentData['empty_qrda'] = 0;
if (!empty($doc_id)) {
$validation_log = $this->validateDocument->validateDocument((string)$xml_content_new, 'qrda1');
}
if (count($components[2]["section"]["entry"] ?? []) < 2) {
$name = $xml["recordTarget"]["patientRole"]["patient"]["name"]["given"] . ' ' .
$xml["recordTarget"]["patientRole"]["patient"]["name"]["family"];
error_log("No QDMs for patient: " . $name);
return true;
}
$valid = $this->validateDocument->validateXmlXsd((string)$xml_content_new, 'qrda1');
if ($valid) {
// Offset to Patient Data section
$this->documentData = $this->parseTemplates->parseQRDAPatientDataSection($components[2]);
$validation_log['xsd'][] = xl("QRDA is empty of content.") . ' ' . text($name);
$this->documentData['empty_qrda'] = 1;
}
// Offset to Patient Data section
$this->documentData = $this->parseTemplates->parseQRDAPatientDataSection($components[2]);
} else {
$valid = $this->validateDocument->validateXmlXsd((string)$xml_content_new, 'ccda');
if ($valid) {
$this->documentData = $this->parseTemplates->parseCDAEntryComponents($components);
if (!empty($doc_id)) {
$validation_log = $this->validateDocument->validateDocument((string)$xml_content_new, 'ccda');
}
$this->documentData = $this->parseTemplates->parseCDAEntryComponents($components);
}

$this->documentData['approval_status'] = 1;
Expand Down Expand Up @@ -319,6 +324,9 @@ public function importCore($xml_content)
}
}
}
if (!empty($doc_id)) {
$this->validateDocument->saveValidationLog($doc_id, $validation_log);
}
}

/*
Expand Down Expand Up @@ -912,10 +920,7 @@ public function buildLabArray($lab_array)
public function import($document_id)
{
$xml_content = $this->getDocument($document_id);
$error = $this->importCore($xml_content);
if ($error) {
return $error;
}
$this->importCore($xml_content, $document_id);
$audit_master_approval_status = 1;
$documentationOf = $this->documentData['field_name_value_array']['documentationOf'][1]['assignedPerson'];
$audit_master_id = CommonPlugin::insert_ccr_into_audit_data($this->documentData, $this->is_qrda_import);
Expand Down Expand Up @@ -1913,7 +1918,7 @@ public function getdocumentationOf($audit_master_id)
*/
public function getCCDAComponents($type)
{
$components = array();
$components = array('schematron' => 'Errors');
$query = "select * from ccda_components where ccda_type = ?";
$appTable = new ApplicationTable();
$result = $appTable->zQuery($query, array($type));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public function retrieveAction()
$style = "ccr.xsl";
break;
case $categoryIds['CCDA']:
$style = "ccda.xsl";
$style = "cda.xsl";
break;
}

Expand Down
11 changes: 10 additions & 1 deletion src/Cqm/QrdaControllers/QrdaReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,21 @@ public function downloadQrdaIII($pids, $measures = '', $options = []): void
}
}

if (is_array($pids)) {
if (count($pids) === 1) {
$pids = $pids[0];
}
}
foreach ($measures as $measure) {
if (is_array($measure)) {
$measure = $measure['measure_id'];
}
$xml = $this->getCategoryIIIReport($pids, $measure, $options);
$filename = $measure . "_all_patients.xml";
$filename = $measure . "_selected_patients.xml";
if (!empty($pids) && !is_array($pids)) {
$meta = sqlQuery("Select `fname`, `lname`, `pid` From `patient_data` Where `pid` = ?", [$pids]);
$filename = $measure . '_' . $pids . '_' . $meta['fname'] . '_' . $meta['lname'] . ".xml";
}
$file = $directory . DIRECTORY_SEPARATOR . $filename;
file_put_contents($file, $xml);
unset($content);
Expand Down
3 changes: 2 additions & 1 deletion src/Services/Cda/CdaTemplateImportDispose.php
Original file line number Diff line number Diff line change
Expand Up @@ -1754,14 +1754,15 @@ public function InsertPayers($payer, $pid, CarecoordinationTable $carecoordinati
$insuranceData = new InsuranceService();

$appTable = new ApplicationTable();
$res_ins = $appTable->zQuery("SELECT `id`, `uuid` FROM `insurance_companies` WHERE `ins_type_code` = ? AND `inactive` = 0 ORDER BY `id` DESC LIMIT 1", array($payer['code'] ?? '1'));
$res_ins = $appTable->zQuery("SELECT `id`, `uuid` FROM `insurance_companies` WHERE `cqm_sop` = ? AND `inactive` = 0 ORDER BY `id` DESC LIMIT 1", array($payer['code'] ?? '1'));
$res_ins_cur = $res_ins->current();
if (empty($res_ins_cur['id'])) {
$data["name"] = 'QRDA Insurance Company Payer ' . $payer['code'] ?? '1';
$data["ins_type_code"] = $payer['code'] ?? '1';
$data["city"] = 'QRDA City';
$data["state"] = 'QRDA State';
$data["zip"] = '33333';
$data["cqm_sop"] = $payer['code'] ?? 1;
$insuranceCompany = new InsuranceCompanyService();
$ins_id = $insuranceCompany->insert($data);
} else {
Expand Down
90 changes: 78 additions & 12 deletions src/Services/Cda/CdaValidateDocuments.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ public function __construct()
{
}

/**
* @param $document
* @param $type
* @return array|bool|null
* @throws Exception
*/
public function validateDocument($document, $type)
{
$xsd = $this->validateXmlXsd($document, $type);
$schema_results = $this->validateSchematron($document, $type);
$totals = array_merge($xsd, $schema_results);

return $totals;
}

/**
* @return bool
* @throws Exception
Expand Down Expand Up @@ -119,20 +134,19 @@ public function validateXmlXsd($document, $type)
$dom = new DomDocument();
$dom->loadXML($document);
$xsd = __DIR__ . '/../../../interface/modules/zend_modules/public/xsd/Schema/CDA2/infrastructure/cda/CDA_SDTC.xsd';
$result = $dom->schemaValidate($xsd);
// TODO phase implementation for schematron
$this->validateSchematron($document, $type);
if ($result) {
return true;
} else {

$xsd_log['xsd'] = [];
if (!$dom->schemaValidate($xsd)) {
$errors = libxml_get_errors();
foreach ($errors as $error) {
error_log($this->formatXsdError($error));
$detail = $this->formatXsdError($error);
$xsd_log['xsd'][] = $detail;
error_log($detail);
}
libxml_clear_errors();

return false;
}

return $xsd_log;
}

/**
Expand Down Expand Up @@ -171,11 +185,63 @@ private function formatXsdError($error): string
break;
}
$error_str .= trim($error->message);
if ($error->file) {
$error_str .= " in $error->file";
}
$error_str .= " on line $error->line\n";

return $error_str;
}

public function createSchematronHtml($amid)
{
$errors = $this->fetchValidationLog($amid);
$xsd = $errors['xsd'];
$schema = $errors['errors'];

$html = "<div class='control-group'>\n" .
"<h5 class='text-info' style='padding: 0 0;'>" . xlt('Schema Definition Errors') . "</h5><hr style='margin: 0 0 10px;' />\n";
if (empty($xsd)) {
$html .= "<p class='text-success'>" . xlt("Passed XSD testing.") . "</p><hr style='margin: 0 0 10px;' />\n";
}
foreach ($xsd as $error) {
$html .= "<blockquote style='margin: 0 0 2px;padding: 0px 5px 0 5px;'>" .
"<p class='text-error' style='font-size:12px;'>" . text($error) . "</p>" .
"</blockquote><hr style='margin: 0 0 10px;' />\n";
}
$html .= "<h5 class='text-info'>" . xlt('Schematron Errors') . "</h5><hr style='margin: 0 0 10px;' />\n";
if (empty($errors['errorCount'])) {
$html .= "<p class='text-success'>" . xlt("Passed Schematron testing.") . "</p><hr style='margin: 0 0 10px;' />\n";
}
foreach ($schema as $error) {
$html .= "<blockquote style='margin: 0 0 2px;padding: 0px 5px 0 5px;'>" .
"<p class='text-error' style='font-size:12px;'>" .
"<span style='color: red;padding-right: 2px;'>" . xlt('Error') . ": </span>" . text($error['description']) .
"<br />" . "<span style='color: red;margin-right: 2px;'>" . xlt('Error Context') . ": </span>" . text($error['context']) .
"<br />" . "<span style='color: red;margin-right: 2px;'>" . xlt('Where') . ": </span>" . text($error['path']) .
"<br />" . "<span style='color: red;margin-right: 2px;'>" . xlt('Line') . "# " . "</span>" . text($error['line']) .
"</p></blockquote><hr style='margin: 0 0 10px;' />\n";
}
$html .= "</div>\n";

return $html;
}

/**
* @param $docId
* @param $log
* @return void
*/
public function saveValidationLog($docId, $log)
{
$content = json_encode($log ?? []);
sqlStatement("UPDATE `documents` SET `document_data` = ? WHERE `id` = ?", array($content, $docId));
}

/**
* @param $docId
* @return mixed
*/
public function fetchValidationLog($audit_id)
{
$log = sqlQuery("SELECT `document_data` FROM `documents` WHERE `audit_master_id` = ?", array($audit_id))['document_data'];
return json_decode($log ?? [], true);
}
}
14 changes: 2 additions & 12 deletions src/Services/InsuranceCompanyService.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ public function insert($data)
$sql .= " alt_cms_id=?,";
$sql .= " cqm_sop=?";

$insuranceResults = sqlInsert(
sqlInsert(
$sql,
array(
$freshId,
Expand All @@ -262,18 +262,8 @@ public function insert($data)
)
);

// sqlInsert returns the last id generated by adodb
if (!$insuranceResults) {
return false;
}

$addressesResults = false;
if (!empty($data["city"] ?? null) && !empty($data["state"] ?? null)) {
$addressesResults = $this->addressService->insert($data, $freshId);
}

if (!$addressesResults) {
return false;
$this->addressService->insert($data, $freshId);
}

return $freshId;
Expand Down
3 changes: 3 additions & 0 deletions src/Services/Qdm/Services/EncounterService.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public function makeQdmModel(QdmRecord $recordObj)
$start_tmp = \DateTime::createFromFormat('Y-m-d H:i:s', $record['date']);
// DateTime->modify() modifies the calling object, so we need to copy our start date
$start = clone $start_tmp;
if (empty($record['date_end'])) {
$record['date_end'] = date('Y-m-d H:i:s', strtotime($record['date']) + 3600);
}
$end = \DateTime::createFromFormat('Y-m-d H:i:s', $record['date_end']);

$days = '';
Expand Down
2 changes: 1 addition & 1 deletion src/Services/Qrda/QrdaReportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public function generateCategoryIIIXml($pid, $measures): string
{
if (empty($pid)) {
$request = new QdmRequestAll();
} else if (is_array($pid)) {
} elseif (is_array($pid)) {
$request = new QdmRequestSome($pid);
} else {
$request = new QdmRequestOne($pid);
Expand Down

0 comments on commit 1908b85

Please sign in to comment.