3 * This processes X12 835 remittances and produces a report.
6 * @link http://www.open-emr.org
7 * @author Rod Roark <rod@sunsetsystems.com>
8 * @author Brady Miller <brady.g.miller@gmail.com>
9 * @copyright Copyright (c) 2006-2010 Rod Roark <rod@sunsetsystems.com>
10 * @copyright Copyright (c) 2018 Brady Miller <brady.g.miller@gmail.com>
11 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
15 // Buffer all output so we can archive it to a file.
18 require_once("../globals.php");
19 require_once("$srcdir/invoice_summary.inc.php");
20 require_once("$srcdir/sl_eob.inc.php");
21 require_once("$srcdir/parse_era.inc.php");
22 require_once("claim_status_codes.php");
23 require_once("adjustment_reason_codes.php");
24 require_once("remark_codes.php");
25 require_once("$srcdir/billing.inc");
27 $debug = $_GET['debug'] ?
1 : 0; // set to 1 for debugging mode
28 $paydate = parse_date($_GET['paydate']);
34 $invoice_total = 0.00;
35 $InsertionId;//last inserted ID of
37 ///////////////////////// Assorted Functions /////////////////////////
39 function parse_date($date)
41 $date = substr(trim($date), 0, 10);
42 if (preg_match('/^(\d\d\d\d)\D*(\d\d)\D*(\d\d)$/', $date, $matches)) {
43 return $matches[1] . '-' . $matches[2] . '-' . $matches[3];
49 function writeMessageLine($bgcolor, $class, $description, $nl2br_process = "false")
52 " <tr bgcolor='" . attr($bgcolor) . "'>\n" .
53 " <td class='" . attr($class) . "' colspan='4'> </td>\n";
55 $dline .= " <td class='" . attr($class) . "'>" . nl2br(text($description)) . "</td>\n";
57 $dline .= " <td class='" . attr($class) . "'>" . text($description) . "</td>\n";
60 " <td class='" . attr($class) . "' colspan='2'> </td>\n" .
65 function writeDetailLine(
77 global $last_ptname, $last_invnumber, $last_code;
78 if ($ptname == $last_ptname) {
81 $last_ptname = $ptname;
84 if ($invnumber == $last_invnumber) {
85 $invnumber = ' ';
87 $last_invnumber = $invnumber;
90 if ($code == $last_code) {
97 $amount = sprintf("%.2f", $amount);
101 $balance = sprintf("%.2f", $balance);
105 " <tr bgcolor='" . attr($bgcolor) . "'>\n" .
106 " <td class='" . attr($class) . "'>" . text($ptname) . "</td>\n" .
107 " <td class='" . attr($class) . "'>" . text($invnumber) . "</td>\n" .
108 " <td class='" . attr($class) . "'>" . text($code) . "</td>\n" .
109 " <td class='" . attr($class) . "'>" . text(oeFormatShortDate($date)) . "</td>\n" .
110 " <td class='" . attr($class) . "'>" . text($description) . "</td>\n" .
111 " <td class='" . attr($class) . "' align='right'>" . text(oeFormatMoney($amount)) . "</td>\n" .
112 " <td class='" . attr($class) . "' align='right'>" . text(oeFormatMoney($balance)) . "</td>\n" .
117 // This writes detail lines that were already in SQL-Ledger for a given
120 function writeOldDetail(&$prev, $ptname, $invnumber, $dos, $code, $bgcolor)
122 global $invoice_total;
123 // $prev['total'] = 0.00; // to accumulate total charges
125 foreach ($prev['dtl'] as $dkey => $ddata) {
126 $ddate = substr($dkey, 0, 10);
127 $description = $ddata['src'] . $ddata['rsn'];
128 if ($ddate == ' ') { // this is the service item
130 $description = 'Service Item';
133 $amount = sprintf("%.2f", $ddata['chg'] - $ddata['pmt']);
134 $invoice_total = sprintf("%.2f", $invoice_total +
$amount);
149 // This is called back by parse_era() once per claim.
151 function era_callback_check(&$out)
153 global $InsertionId;//last inserted ID of
154 global $StringToEcho,$debug;
156 if ($_GET['original']=='original') {
157 $StringToEcho="<br/><br/><br/><br/><br/><br/>";
158 $StringToEcho.="<table border='1' cellpadding='0' cellspacing='0' width='750'>";
159 $StringToEcho.="<tr bgcolor='#cccccc'><td width='50'></td><td class='dehead' width='150' align='center'>" . xlt('Check Number') . "</td><td class='dehead' width='400' align='center'>" . xlt('Payee Name') . "</td><td class='dehead' width='150' align='center'>" . xlt('Check Amount') . "</td></tr>";
161 for ($check_count=1; $check_count<=$out['check_count']; $check_count++
) {
162 if ($check_count%2
==1) {
168 $rs=sqlQ("select reference from ar_session where reference=?", array($out['check_number'.$check_count]));
169 if (sqlNumRows($rs)>0) {
174 $StringToEcho.="<tr bgcolor='" . attr($bgcolor) . "'>";
175 $StringToEcho.="<td><input type='checkbox' name='chk" . attr($out['check_number'.$check_count]) . "' value='" . attr($out['check_number'.$check_count]) . "'/></td>";
176 $StringToEcho.="<td>" . text($out['check_number'.$check_count]) . "</td>";
177 $StringToEcho.="<td>" . text($out['payee_name'.$check_count]) . "</td>";
178 $StringToEcho.="<td align='right'>" . text(number_format($out['check_amount'.$check_count], 2)) . "</td>";
179 $StringToEcho.="</tr>";
182 $StringToEcho.="<tr bgcolor='#cccccc'><td colspan='4' align='center'><input type='submit' name='CheckSubmit' value='Submit'/></td></tr>";
183 if ($WarningFlag==true) {
184 $StringToEcho.="<tr bgcolor='#ff0000'><td colspan='4' align='center'>" . xlt('Warning, Check Number already exist in the database') . "</td></tr>";
187 $StringToEcho.="</table>";
189 for ($check_count=1; $check_count<=$out['check_count']; $check_count++
) {
190 $chk_num=$out['check_number'.$check_count];
191 $chk_num=str_replace(' ', '_', $chk_num);
192 if (isset($_REQUEST['chk'.$chk_num])) {
193 $check_date=$out['check_date'.$check_count]?
$out['check_date'.$check_count]:$_REQUEST['paydate'];
194 $post_to_date=$_REQUEST['post_to_date']!=''?
$_REQUEST['post_to_date']:date('Y-m-d');
195 $deposit_date=$_REQUEST['deposit_date']!=''?
$_REQUEST['deposit_date']:date('Y-m-d');
196 $InsertionId[$out['check_number'.$check_count]]=arPostSession($_REQUEST['InsId'], $out['check_number'.$check_count], $out['check_date'.$check_count], $out['check_amount'.$check_count], $post_to_date, $deposit_date, $debug);
201 function era_callback(&$out)
203 global $encount, $debug, $claim_status_codes, $adjustment_reasons, $remark_codes;
204 global $invoice_total, $last_code, $paydate;
205 global $InsertionId;//last inserted ID of
208 // Some heading information.
209 $chk_123=$out['check_number'];
210 $chk_123=str_replace(' ', '_', $chk_123);
211 if (isset($_REQUEST['chk'.$chk_123])) {
216 "Payer: " . $out['payer_name']
222 "WITHOUT UPDATE is selected; no changes will be applied."
228 $invoice_total = 0.00;
229 $bgcolor = (++
$encount & 1) ?
"#ddddff" : "#ffdddd";
230 list($pid, $encounter, $invnumber) = slInvoiceNumber($out);
232 // Get details, if we have them, for the invoice.
235 if ($pid && $encounter) {
236 // Get invoice data into $arrow or $ferow.
237 $ferow = sqlQuery("SELECT e.*, p.fname, p.mname, p.lname " .
238 "FROM form_encounter AS e, patient_data AS p WHERE " .
239 "e.pid = ? AND e.encounter = ? AND ".
240 "p.pid = e.pid", array($pid, $encounter));
242 $pid = $encounter = 0;
243 $invnumber = $out['our_claim_id'];
246 $codes = ar_get_invoice_summary($pid, $encounter, true);
247 // $svcdate = substr($ferow['date'], 0, 10);
251 // Show the claim status.
252 $csc = $out['claim_status_code'];
254 if ($csc == '1' ||
$csc == '19') {
258 if ($csc == '2' ||
$csc == '20') {
262 if ($csc == '3' ||
$csc == '21') {
266 $primary = ($inslabel == 'Ins1');
270 "Claim status $csc: " . $claim_status_codes[$csc]
273 // Show an error message if the claim is missing or already posted.
278 "The following claim is not in our database"
281 // Skip this test. Claims can get multiple CLPs from the same payer!
283 // $insdone = strtolower($arrow['shipvia']);
284 // if (strpos($insdone, 'ins1') !== false) {
286 // writeMessageLine($bgcolor, 'errdetail',
287 // "Primary insurance EOB was already posted for the following claim");
291 if ($csc == '4') {//Denial case, code is stored in the claims table for display in the billing manager screen with reason explained.
294 if ($pid && $encounter) {
296 foreach ($out['svc'] as $svc) {
297 foreach ($svc['adj'] as $adj) {//Per code and modifier the reason will be showed in the billing manager.
298 $code_value .= $svc['code'].'_'.$svc['mod'].'_'.$adj['group_code'].'_'.$adj['reason_code'].',';
302 $code_value = substr($code_value, 0, -1);
303 //We store the reason code to display it with description in the billing manager screen.
304 //process_file is used as for the denial case file name will not be there, and extra field(to store reason) can be avoided.
305 updateClaim(true, $pid, $encounter, $_REQUEST['InsId'], substr($inslabel, 3), 7, 0, $code_value);
312 "Not posting adjustments for denied claims, please follow up manually!"
314 } else if ($csc == '22') {
319 "Payment reversals are not automated, please enter manually!"
323 if ($out['warnings']) {
324 writeMessageLine($bgcolor, 'infdetail', rtrim($out['warnings']), true);
327 // Simplify some claim attributes for cleaner code.
328 $service_date = parse_date($out['dos']);
329 $check_date = $paydate ?
$paydate : parse_date($out['check_date']);
330 $production_date = $paydate ?
$paydate : parse_date($out['production_date']);
332 $insurance_id = arGetPayerID($pid, $service_date, substr($inslabel, 3));
333 if (empty($ferow['lname'])) {
334 $patient_name = $out['patient_fname'] . ' ' . $out['patient_lname'];
336 $patient_name = $ferow['fname'] . ' ' . $ferow['lname'];
341 // This loops once for each service item in this claim.
342 foreach ($out['svc'] as $svc) {
343 // Treat a modifier in the remit data as part of the procedure key.
344 // This key will then make its way into SQL-Ledger.
345 $codekey = $svc['code'];
347 $codekey .= ':' . $svc['mod'];
350 $prev = $codes[$codekey];
351 $codetype = ''; //will hold code type, if exists
353 // This reports detail lines already on file for this service item.
355 $codetype = $codes[$codekey]['code_type']; //store code type
356 writeOldDetail($prev, $patient_name, $invnumber, $service_date, $codekey, $bgcolor);
357 // Check for sanity in amount charged.
358 $prevchg = sprintf("%.2f", $prev['chg'] +
$prev['adj']);
359 if ($prevchg != abs($svc['chg'])) {
363 "EOB charge amount " . $svc['chg'] . " for this code does not match our invoice"
368 // Check for already-existing primary remittance activity.
369 // Removed this check because it was not allowing for copays manually
370 // entered into the invoice under a non-copay billing code.
372 if ((sprintf("%.2f",$prev['chg']) != sprintf("%.2f",$prev['bal']) ||
373 $prev['adj'] != 0) && $primary)
375 writeMessageLine($bgcolor, 'errdetail',
376 "This service item already has primary payments and/or adjustments!");
381 unset($codes[$codekey]);
382 } // If the service item is not in our database...
384 // This is not an error. If we are not in error mode and not debugging,
385 // insert the service item into SL. Then display it (in green if it
386 // was inserted, or in red if we are in error mode).
387 $description = "CPT4:$codekey Added by $inslabel $production_date";
388 if (!$error && !$debug) {
402 $invoice_total +
= $svc['chg'];
405 $class = $error ?
'errdetail' : 'newdetail';
415 ($error ?
'' : $invoice_total)
419 $class = $error ?
'errdetail' : 'newdetail';
421 // Report Allowed Amount.
422 if ($svc['allowed']) {
423 // A problem here is that some payers will include an adjustment
424 // reflecting the allowed amount, others not. So here we need to
425 // check if the adjustment exists, and if not then create it. We
426 // assume that any nonzero CO (Contractual Obligation) or PI
427 // (Payer Initiated) adjustment is good enough.
428 $contract_adj = sprintf("%.2f", $svc['chg'] - $svc['allowed']);
429 foreach ($svc['adj'] as $adj) {
430 if (($adj['group_code'] == 'CO' ||
$adj['group_code'] == 'PI') && $adj['amount'] != 0) {
435 if ($contract_adj > 0) {
436 $svc['adj'][] = array('group_code' => 'CO', 'reason_code' => 'A2',
437 'amount' => $contract_adj);
443 'Allowed amount is ' . sprintf("%.2f", $svc['allowed'])
447 // Report miscellaneous remarks.
448 if ($svc['remark']) {
449 $rmk = $svc['remark'];
450 writeMessageLine($bgcolor, 'infdetail', "$rmk: " . $remark_codes[$rmk]);
453 // Post and report the payment for this service item from the ERA.
454 // By the way a 'Claim' level payment is probably going to be negative,
455 // i.e. a payment reversal.
457 if (!$error && !$debug) {
461 $InsertionId[$out['check_number']],
462 $svc['paid'], //$InsertionId[$out['check_number']] gives the session id
464 substr($inslabel, 3),
465 $out['check_number'],
470 $invoice_total -= $svc['paid'];
473 $description = "$inslabel/" . $out['check_number'] . ' payment';
474 if ($svc['paid'] < 0) {
475 $description .= ' reversal';
487 ($error ?
'' : $invoice_total)
491 // Post and report adjustments from this ERA. Posted adjustment reasons
492 // must be 25 characters or less in order to fit on patient statements.
493 foreach ($svc['adj'] as $adj) {
494 $description = $adj['reason_code'] . ': ' . $adjustment_reasons[$adj['reason_code']];
495 if ($adj['group_code'] == 'PR' ||
!$primary) {
496 // Group code PR is Patient Responsibility. Enter these as zero
497 // adjustments to retain the note without crediting the claim.
500 $reason = 'Pt resp: '; // Reasons should be 25 chars or less.
501 if ($adj['reason_code'] == '1') $reason = 'To deductible: ';
502 else if ($adj['reason_code'] == '2') $reason = 'Coinsurance: ';
503 else if ($adj['reason_code'] == '3') $reason = 'Co-pay: ';
505 $reason = "$inslabel ptresp: "; // Reasons should be 25 chars or less.
506 if ($adj['reason_code'] == '1') {
507 $reason = "$inslabel dedbl: ";
508 } else if ($adj['reason_code'] == '2') {
509 $reason = "$inslabel coins: ";
510 } else if ($adj['reason_code'] == '3') {
511 $reason = "$inslabel copay: ";
513 } // Non-primary insurance adjustments are garbage, either repeating
514 // the primary or are not adjustments at all. Report them as notes
515 // but do not post any amounts.
517 $reason = "$inslabel note " . $adj['reason_code'] . ': ';
519 $reason .= sprintf("%.2f", $adj['amount']);
523 $reason .= sprintf("%.2f", $adj['amount']);
524 // Post a zero-dollar adjustment just to save it as a comment.
525 if (!$error && !$debug) {
529 $InsertionId[$out['check_number']],
531 $codekey, //$InsertionId[$out['check_number']] gives the session id
532 substr($inslabel, 3),
540 writeMessageLine($bgcolor, $class, $description . ' ' .
541 sprintf("%.2f", $adj['amount']));
542 } // Other group codes for primary insurance are real adjustments.
544 if (!$error && !$debug) {
548 $InsertionId[$out['check_number']],
549 $adj['amount'], //$InsertionId[$out['check_number']] gives the session id
551 substr($inslabel, 3),
552 "Adjust code " . $adj['reason_code'],
557 $invoice_total -= $adj['amount'];
569 ($error ?
'' : $invoice_total)
573 } // End of service item
575 // Report any existing service items not mentioned in the ERA, and
576 // determine if any of them are still missing an insurance response
577 // (if so, then insurance is not yet done with the claim).
578 $insurance_done = true;
579 foreach ($codes as $code => $prev) {
580 // writeOldDetail($prev, $arrow['name'], $invnumber, $service_date, $code, $bgcolor);
581 writeOldDetail($prev, $patient_name, $invnumber, $service_date, $code, $bgcolor);
582 $got_response = false;
583 foreach ($prev['dtl'] as $ddata) {
584 if ($ddata['pmt'] ||
$ddata['rsn']) {
585 $got_response = true;
589 if (!$got_response) {
590 $insurance_done = false;
594 // Cleanup: If all is well, mark Ins<x> done and check for secondary billing.
595 if (!$error && !$debug && $insurance_done) {
596 $level_done = 0 +
substr($inslabel, 3);
598 if ($out['crossover']==1) {//Automatic forward case.So need not again bill from the billing manager screen.
599 sqlStatement("UPDATE form_encounter " .
600 "SET last_level_closed = ?,last_level_billed=? WHERE " .
601 "pid = ? AND encounter = ?", array($level_done, $level_done, $pid, $encounter));
605 'This claim is processed by Insurance '.$level_done.' and automatically forwarded to Insurance '.($level_done+
1) .' for processing. '
608 sqlStatement("UPDATE form_encounter " .
609 "SET last_level_closed = ? WHERE " .
610 "pid = ? AND encounter = ?", array($level_done, $pid, $encounter));
613 // Check for secondary insurance.
614 if ($primary && arGetPayerID($pid, $service_date, 2)) {
615 arSetupSecondary($pid, $encounter, $debug, $out['crossover']);
617 if ($out['crossover']<>1) {
621 'This claim is now re-queued for secondary paper billing'
629 /////////////////////////// End Functions ////////////////////////////
633 if (!verifyCsrfToken($_GET["csrf_token_form"])) {
637 $eraname = $_GET['eraname'];
640 die(xlt("You cannot access this page directly."));
643 // Open the output file early so that in case it fails, we do not post a
644 // bunch of stuff without saving the report. Also be sure to retain any old
645 // report files. Do not save the report if this is a no-update situation.
648 $nameprefix = $GLOBALS['OE_SITE_DIR'] . "/era/$eraname";
650 for ($i = 1; is_file("$nameprefix$namesuffix.html"); ++
$i) {
654 $fnreport = "$nameprefix$namesuffix.html";
655 $fhreport = fopen($fnreport, 'w');
657 die(xlt("Cannot create") . " '" . text($fnreport) . "'");
664 <?php
html_header_show();?
>
665 <link rel
=stylesheet href
="<?php echo $css_header;?>" type
="text/css">
666 <style type
="text/css">
667 body
{ font
-family
:sans
-serif
; font
-size
:8pt
; font
-weight
:normal
}
668 .dehead
{ color
:#000000; font-family:sans-serif; font-size:9pt; font-weight:bold }
669 .olddetail
{ color
:#000000; font-family:sans-serif; font-size:9pt; font-weight:normal }
670 .newdetail
{ color
:#00dd00; font-family:sans-serif; font-size:9pt; font-weight:normal }
671 .errdetail
{ color
:#dd0000; font-family:sans-serif; font-size:9pt; font-weight:normal }
672 .infdetail
{ color
:#0000ff; font-family:sans-serif; font-size:9pt; font-weight:normal }
674 <title
><?php
echo xlt('EOB Posting - Electronic Remittances')?
></title
>
675 <script language
="JavaScript">
678 <body leftmargin
='0' topmargin
='0' marginwidth
='0' marginheight
='0'>
679 <form action
="sl_eob_process.php" method
="get" >
680 <input type
="hidden" name
="csrf_token_form" value
="<?php echo attr(collectCsrfToken()); ?>" />
684 if ($_GET['original']=='original') {
685 $alertmsg = parse_era_for_check($GLOBALS['OE_SITE_DIR'] . "/era/$eraname.edi", 'era_callback');
689 <table border
='0' cellpadding
='2' cellspacing
='0' width
='100%'>
691 <tr bgcolor
="#cccccc">
693 <?php
echo xlt('Patient'); ?
>
696 <?php
echo xlt('Invoice'); ?
>
699 <?php
echo xlt('Code'); ?
>
702 <?php
echo xlt('Date'); ?
>
705 <?php
echo xlt('Description'); ?
>
707 <td
class="dehead" align
="right">
708 <?php
echo xlt('Amount'); ?
> 
;
710 <td
class="dehead" align
="right">
711 <?php
echo xl('Balance'); ?
> 
;
718 $eraname=$_REQUEST['eraname'];
719 $alertmsg = parse_era_for_check($GLOBALS['OE_SITE_DIR'] . "/era/$eraname.edi");
720 $alertmsg = parse_era($GLOBALS['OE_SITE_DIR'] . "/era/$eraname.edi", 'era_callback');
722 $StringIssue=xl("Total Distribution for following check number is not full").': ';
724 foreach ($InsertionId as $key => $value) {
725 $rs= sqlQ("select pay_total from ar_session where session_id=?", array($value));
726 $row=sqlFetchArray($rs);
727 $pay_total=$row['pay_total'];
728 $rs= sqlQ("select sum(pay_amount) sum_pay_amount from ar_activity where session_id=?", array($value));
729 $row=sqlFetchArray($rs);
730 $pay_amount=$row['sum_pay_amount'];
732 if (($pay_total-$pay_amount)<>0) {
733 $StringIssue.=$key.' ';
738 if ($StringPrint=='Yes') {
739 echo "<script>alert('" . addslashes($StringIssue) . "')</script>";
750 <script language
="JavaScript">
753 echo " alert('" . addslashes($alertmsg) . "');\n";
757 <input type
="hidden" name
="paydate" value
="<?php echo attr(DateToYYYYMMDD($_REQUEST['paydate'])); ?>" />
758 <input type
="hidden" name
="post_to_date" value
="<?php echo attr(DateToYYYYMMDD($_REQUEST['post_to_date'])); ?>" />
759 <input type
="hidden" name
="deposit_date" value
="<?php echo attr(DateToYYYYMMDD($_REQUEST['deposit_date'])); ?>" />
760 <input type
="hidden" name
="debug" value
="<?php echo attr($_REQUEST['debug']); ?>" />
761 <input type
="hidden" name
="InsId" value
="<?php echo attr($_REQUEST['InsId']); ?>" />
762 <input type
="hidden" name
="eraname" value
="<?php echo attr($eraname); ?>" />
767 // Save all of this script's output to a report file.
769 fwrite($fhreport, ob_get_contents());