From c4910e461d87cb879cfe816b834dd58646f0cede Mon Sep 17 00:00:00 2001 From: Jerry Padgett Date: Thu, 24 Oct 2024 09:46:56 -0400 Subject: [PATCH] Eligibility Fixes (#7774) * Eligibility Fixes Change eligibilty password and login to use SFTP inputs. Rework MIME parsing. Fix conditional preventing any request calls. * Prevent auto select unassigned for multiselect if other items are selected. i.e only select for empty selects. * add title for review encounters * refactor out oeHttp (cherry picked from commit 13d17525381c29bb567ad9ca8f25af44564fbcba) --- interface/patient_file/encounter/forms.php | 14 +- library/options.inc.php | 20 ++- src/Billing/EDI270.php | 198 +++++++++++---------- templates/x12_partners/general_edit.html | 4 +- 4 files changed, 130 insertions(+), 106 deletions(-) diff --git a/interface/patient_file/encounter/forms.php b/interface/patient_file/encounter/forms.php index e1d390505be..806ab4cfa51 100644 --- a/interface/patient_file/encounter/forms.php +++ b/interface/patient_file/encounter/forms.php @@ -60,9 +60,10 @@ ?> - + + @@ -293,11 +294,11 @@ ?> - $("body table:first").hide(); - $(".encounter-summary-column").hide(); - $(".btn").hide(); - $(".encounter-summary-column:first").show(); - $(".title:first").text( + " " + $(".title:first").text() + " ( " + + " )"); + $("body table:first").hide(); + $(".encounter-summary-column").hide(); + $(".btn").hide(); + $(".encounter-summary-column:first").show(); + $(".title:first").text( + " (" + + ")"); }); @@ -310,7 +311,6 @@ function deleteme() { return false; } - // create new follow-up Encounter. function createFollowUpEncounter() { diff --git a/library/options.inc.php b/library/options.inc.php index c47fc06decb..d38016865fd 100644 --- a/library/options.inc.php +++ b/library/options.inc.php @@ -177,14 +177,22 @@ function generate_select_list( preg_match_all('/select2/m', ($class ?? ''), $matches, PREG_SET_ORDER, 0); if (array_key_exists('placeholder', $attributes) && count($matches) > 0) { // We have a placeholder attribute as well as a select2 class indicating there - // should be provide a truley empty option. + // should be provided a truly empty option. $_options[] = []; } else { - $_options[] = [ - 'label' => $selectEmptyName, - 'value' => '', - 'isSelected' => true, - ]; + if ($multiple && !empty($currvalue)) { + $_options[] = [ + 'label' => $selectEmptyName, + 'value' => '', + 'isSelected' => false, + ]; + } else { + $_options[] = [ + 'label' => $selectEmptyName, + 'value' => '', + 'isSelected' => true, + ]; + } } } diff --git a/src/Billing/EDI270.php b/src/Billing/EDI270.php index 8c267edc2ad..90e0e1cdfb8 100644 --- a/src/Billing/EDI270.php +++ b/src/Billing/EDI270.php @@ -27,8 +27,10 @@ require_once(dirname(__FILE__) . "/../../library/edihistory/codes/edih_271_code_class.php"); use edih_271_codes; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\MultipartStream; use OpenEMR\Billing\BillingProcessor\BillingClaimBatchControlNumber; -use OpenEMR\Common\Http\oeHttp; +use OpenEMR\Common\Crypto\CryptoGen; use OpenEMR\Common\Utils\RandomGenUtils; // @TODO global to become private var when this goes to a class. @@ -49,13 +51,13 @@ public static function createISA($row, $X12info, $segTer, $compEleSep) $ISA = array(); $ISA[0] = "ISA"; // Interchange Control Header Segment ID $ISA[1] = "00"; // Author Info Qualifier - $ISA[2] = str_pad("0000000", 10, " "); // Author Information + $ISA[2] = str_pad("", 10, " "); // Author Information $ISA[3] = "00"; // Security Information Qualifier // MEDI-CAL NOTE: For Leased-Line & Dial-Up use '01', // for BATCH use '00'. // '00' No Security Information Present // (No Meaningful Information in I04) - $ISA[4] = str_pad("0000000000", 10, " "); // Security Information + $ISA[4] = str_pad("", 10, " "); // Security Information $ISA[5] = str_pad($X12info['x12_isa05'], 2, " "); // Interchange ID Qualifier $ISA[6] = str_pad($X12info['x12_sender_id'], 15, " "); // INTERCHANGE SENDER ID $ISA[7] = str_pad($X12info['x12_isa07'], 2, " "); // Interchange ID Qualifier @@ -430,8 +432,11 @@ public static function requestEligibleTransaction($pid = 0, $eFlag = false) LEFT JOIN insurance_companies as c ON (c.id = i.provider) WHERE p.pid = ?"; $res = sqlStatement($query, array($pid)); - - $details = self::requestRealTimeEligible($res, '', "~", ':', true); + $resArray = array(); + while ($row = sqlFetchArray($res)) { + $resArray[] = $row; + } + $details = self::requestRealTimeEligible($resArray, '', "~", ':', true); if ($details === false) { $details = "Error: Nothing returned from X12 Partner."; } @@ -666,9 +671,6 @@ public static function showEligibilityInformation($pid, $flag = false) } $col++; } - if ($col === 2) { - $showString .= ""; - } if ($title === 1) { $showString .= "
" . xlt("Nothing To Report") . "
"; } @@ -789,82 +791,83 @@ public static function requestEligibility($partner = '', $x12_270 = '') $data[8] = chr(ord($data[8]) & 0x3f | 0x80); $payloadId = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } - $boundary = RandomGenUtils::createUniqueToken(12); - $rt_passwrd = $X12info['x12_isa04']; - $rt_user = $X12info['x12_isa02']; + + $cryptoGen = new CryptoGen(); + $decrypted_password = $cryptoGen->decryptStandard($X12info['x12_sftp_pass']); + $rt_password = $decrypted_password; + $rt_user = $X12info['x12_sftp_login']; $sender_id = $X12info['x12_sender_id']; $receiver_id = $X12info['x12_receiver_id']; $now_date = date("Y-m-d\TH:i:s\Z"); - $headers = array( - 'Content-Type' => "multipart/form-data; boundary=$boundary", - 'Host' => ' wsd.officeally.com' - ); - -// IMPORTANT: Do not change the format of $mime_body below. -// HTTP MIME Multipart is non-normative. LFs matter... -// - $mime_body = << 'PayloadType', + 'contents' => 'X12_270_Request_005010X279A1', + ], + [ + 'name' => 'ProcessingMode', + 'contents' => 'RealTime', + ], + [ + 'name' => 'PayloadId', + 'contents' => $payloadId, + ], + [ + 'name' => 'TimeStamp', + 'contents' => $now_date, + ], + [ + 'name' => 'UserName', + 'contents' => $rt_user, + ], + [ + 'name' => 'Password', + 'contents' => $rt_password, + ], + [ + 'name' => 'SenderId', + 'contents' => $sender_id, + ], + [ + 'name' => 'ReceiverId', + 'contents' => $receiver_id, + ], + [ + 'name' => 'CORERuleVersion', + 'contents' => '2.2.0', + ], + [ + 'name' => 'Payload', + 'contents' => $x12_270, + ] + + ]; + + $params = [ + 'headers' => [ + 'Connection' => 'close', + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ], + 'body' => new MultipartStream($multipart_form, $boundary), // here is all the magic + ]; -$receiver_id ---$boundary -Content-Disposition: form-data; name="SenderID" - -$sender_id ---$boundary -Content-Disposition: form-data; name="PayloadType" - -X12_270_Request_005010X279A1 ---$boundary -Content-Disposition: form-data; name="UserName" - -$rt_user ---$boundary -Content-Disposition: form-data; name="Password" - -$rt_passwrd ---$boundary -Content-Disposition: form-data; name="Payload" - -$x12_270 ---$boundary-- -MIMEBODY; // send the request - $response = oeHttp::bodyFormat('body') - //->setDebug('5000')/* @todo uncomment and set proxy port to debug eg Fiddler */ - ->usingHeaders($headers) - ->post($X12info['x12_eligibility_endpoint'], $mime_body); - - $formBody = $response->body(); - $contentType = $response->header('Content-Type')[0]; - $hContentLength = (int)$response->header('Content-Length')[0]; - $cksum = ($hContentLength - strlen($formBody)) === 0 ? true : false; // validate content size - $formData = self::mimeParse($formBody, $contentType); + $response = (new Client())->request('POST', $url, $params); + $formBody = $response->getBody(); + $contentType = $response->getHeader('Content-Type')[0]; + $hContentLength = (int)$response->getHeader('Content-Length')[0]; + $cksum = ($hContentLength - strlen($formBody)) === 0; // validate content size + $formData = self::mimeParse($formBody, $contentType); + // validate the response $errors = ''; if (!$cksum) { $errors .= "Error:" . xlt("Request Content Fails Integrity Test"); } - if ($response->status() !== 200) { - $errors .= "\nError:" . xlt("Http Error") . ": " . $response->getReasonPhrase() . " : " . $response->status(); + if ($response->getStatusCode() !== 200) { + $errors .= "\nError:" . xlt("Http Error") . ": " . $response->getReasonPhrase() . " : " . $response->getStatusCode(); } if ($formData['ErrorCode'] != "Success") { $errors .= "\nError:" . $formData['ErrorCode'] . "\n" . $formData['ErrorMessage']; @@ -874,28 +877,41 @@ public static function requestEligibility($partner = '', $x12_270 = '') return $errors; } - $x12_271 = $formData['Payload']; - - return $x12_271; + return $formData['Payload']; } - public static function mimeParse(string $formBody = null, $contentType) + public static function mimeParse(string $formBody, $contentType) { - $mimeBody = preg_replace('~\r\n?~', "\r", $formBody); - list($contentType, $bound, $cs) = explode(";", trim($contentType)); // $contentType & $cs are throwaways - $bound = explode("=", trim($bound, ' '))[1]; - $mimeFields = preg_split("/-+$bound/", $mimeBody); - array_pop($mimeFields); - $hold = $isMatches = []; - foreach ($mimeFields as $id => $field) { + // Normalize + $mimeBody = preg_replace('~\r\n?~', "\r\n", $formBody); + // Extract boundary from content type + list($contentType, $boundaryDirective) = explode(";", trim($contentType)); + $boundary = trim(explode("=", trim($boundaryDirective))[1], '"'); + $boundary = preg_quote($boundary, '/'); + // Split the body using boundary + $pattern = "/--" . $boundary . "(--)?\r?\n/"; + $mimeFields = preg_split($pattern, $mimeBody); + // Remove any empty elements, like the final one after the closing boundary + $mimeFields = array_filter($mimeFields); + $mimeData = []; + foreach ($mimeFields as $field) { if (empty($field)) { continue; } - preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $field, $isMatches); - if (preg_match('/^(.*)\[\]$/i', $isMatches[1], $hold)) { - $mimeData[$hold[1]][] = $isMatches[2]; - } else { - $mimeData[$isMatches[1]] = $isMatches[2]; + // extract name and content from the field + if (preg_match('/Content-Disposition:.*?name="([^"]+)"[\r\n]+(?:[^\r\n]+\r\n)?([\s\S]+)\r\n$/', $field, $matches)) { + $name = $matches[1]; + $content = trim($matches[2]); + // Check if the field is an array (i.e., name ends with "[]") + if (substr($name, -2) === '[]') { + $name = substr($name, 0, -2); + if (!isset($mimeData[$name])) { + $mimeData[$name] = []; + } + $mimeData[$name][] = $content; + } else { + $mimeData[$name] = $content; + } } } return $mimeData; @@ -927,7 +943,7 @@ public static function parseEdi271($content) $responses = $content; } -// Loop through each 271. '\n' delims records in batch. + // Loop through each 271. '\n' delims records in batch. foreach ($responses as $new) { if (empty($new)) { continue; @@ -1017,7 +1033,7 @@ public static function parseEdi271($content) $trace++; if ($in['pid']) { $in['benefits'] = $benefits ? $benefits : []; - array_push($subscribers, $in); + $subscribers[] = $in; $loop['context'] = $elements[0]; $benefits = []; } diff --git a/templates/x12_partners/general_edit.html b/templates/x12_partners/general_edit.html index 2f2350c5b49..1f1de47e40e 100644 --- a/templates/x12_partners/general_edit.html +++ b/templates/x12_partners/general_edit.html @@ -140,13 +140,13 @@
- +
- +