2 // Copyright (C) 2006-2007 Rod Roark <rod@sunsetsystems.com>
4 // This program is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU General Public License
6 // as published by the Free Software Foundation; either version 2
7 // of the License, or (at your option) any later version.
9 // This processes X12 835 remittances and produces a report.
11 // Buffer all output so we can archive it to a file.
14 include_once("../globals.php");
15 include_once("../../library/invoice_summary.inc.php");
16 include_once("../../library/sl_eob.inc.php");
17 include_once("../../library/parse_era.inc.php");
18 include_once("claim_status_codes.php");
19 include_once("adjustment_reason_codes.php");
20 include_once("remark_codes.php");
22 $debug = $_GET['debug'] ?
1 : 0; // set to 1 for debugging mode
23 $paydate = parse_date($_GET['paydate']);
29 $invoice_total = 0.00;
31 ///////////////////////// Assorted Functions /////////////////////////
33 function parse_date($date) {
34 $date = substr(trim($date), 0, 10);
35 if (preg_match('/^(\d\d\d\d)\D*(\d\d)\D*(\d\d)$/', $date, $matches)) {
36 return $matches[1] . '-' . $matches[2] . '-' . $matches[3];
41 function writeMessageLine($bgcolor, $class, $description) {
43 " <tr bgcolor='$bgcolor'>\n" .
44 " <td class='$class' colspan='4'> </td>\n" .
45 " <td class='$class'>$description</td>\n" .
46 " <td class='$class' colspan='2'> </td>\n" .
51 function writeDetailLine($bgcolor, $class, $ptname, $invnumber,
52 $code, $date, $description, $amount, $balance)
54 global $last_ptname, $last_invnumber, $last_code;
55 if ($ptname == $last_ptname) $ptname = ' ';
56 else $last_ptname = $ptname;
57 if ($invnumber == $last_invnumber) $invnumber = ' ';
58 else $last_invnumber = $invnumber;
59 if ($code == $last_code) $code = ' ';
60 else $last_code = $code;
61 if ($amount ) $amount = sprintf("%.2f", $amount );
62 if ($balance) $balance = sprintf("%.2f", $balance);
64 " <tr bgcolor='$bgcolor'>\n" .
65 " <td class='$class'>$ptname</td>\n" .
66 " <td class='$class'>$invnumber</td>\n" .
67 " <td class='$class'>$code</td>\n" .
68 " <td class='$class'>$date</td>\n" .
69 " <td class='$class'>$description</td>\n" .
70 " <td class='$class' align='right'>$amount</td>\n" .
71 " <td class='$class' align='right'>$balance</td>\n" .
76 // This writes detail lines that were already in SQL-Ledger for a given
79 function writeOldDetail(&$prev, $ptname, $invnumber, $dos, $code, $bgcolor) {
80 global $invoice_total;
81 // $prev['total'] = 0.00; // to accumulate total charges
83 foreach ($prev['dtl'] as $dkey => $ddata) {
84 $ddate = substr($dkey, 0, 10);
85 $description = $ddata['src'] . $ddata['rsn'];
86 if ($ddate == ' ') { // this is the service item
88 $description = 'Service Item';
90 $amount = sprintf("%.2f", $ddata['chg'] - $ddata['pmt']);
91 $invoice_total = sprintf("%.2f", $invoice_total +
$amount);
92 writeDetailLine($bgcolor, 'olddetail', $ptname, $invnumber,
93 $code, $ddate, $description, $amount, $invoice_total);
97 // This is called back by parse_era() once per claim.
99 function era_callback(&$out) {
100 global $encount, $debug, $claim_status_codes, $adjustment_reasons, $remark_codes;
101 global $invoice_total, $last_code, $paydate;
103 // Some heading information.
105 writeMessageLine('#ffffff', 'infdetail',
106 "Payer: " . htmlentities($out['payer_name']));
108 writeMessageLine('#ffffff', 'infdetail',
109 "WITHOUT UPDATE is selected; no changes will be applied.");
114 $invoice_total = 0.00;
115 $bgcolor = (++
$encount & 1) ?
"#ddddff" : "#ffdddd";
116 list($pid, $encounter, $invnumber) = slInvoiceNumber($out);
118 // Get details, if we have them, for the invoice.
121 if ($pid && $encounter) {
122 // Get invoice data into $arrow.
123 $arres = SLQuery("SELECT ar.id, ar.notes, ar.shipvia, customer.name " .
124 "FROM ar, customer WHERE ar.invnumber = '$invnumber' AND " .
125 "customer.id = ar.customer_id");
126 if ($sl_err) die($sl_err);
127 $arrow = SLGetRow($arres, 0);
130 $codes = get_invoice_summary($arrow['id'], true);
131 } else { // oops, no such invoice
132 $pid = $encounter = 0;
133 $invnumber = $out['our_claim_id'];
138 foreach ($codes as $cdata) {
140 $insurance_id = $cdata['ins'];
145 // Show the claim status.
146 $csc = $out['claim_status_code'];
148 if ($csc == '1' ||
$csc == '19') $inslabel = 'Ins1';
149 if ($csc == '2' ||
$csc == '20') $inslabel = 'Ins2';
150 if ($csc == '3' ||
$csc == '21') $inslabel = 'Ins3';
151 $primary = ($inslabel == 'Ins1');
152 writeMessageLine($bgcolor, 'infdetail',
153 "Claim status $csc: " . $claim_status_codes[$csc]);
155 // Show an error message if the claim is missing or already posted.
157 writeMessageLine($bgcolor, 'errdetail',
158 "The following claim is not in our database");
161 // Skip this test. Claims can get multiple CLPs from the same payer!
163 // $insdone = strtolower($arrow['shipvia']);
164 // if (strpos($insdone, 'ins1') !== false) {
166 // writeMessageLine($bgcolor, 'errdetail',
167 // "Primary insurance EOB was already posted for the following claim");
173 writeMessageLine($bgcolor, 'errdetail',
174 "Not posting adjustments for denied claims, please follow up manually!");
176 else if ($csc == '22') {
178 writeMessageLine($bgcolor, 'errdetail',
179 "Payment reversals are not automated, please enter manually!");
182 if ($out['warnings']) {
183 writeMessageLine($bgcolor, 'infdetail', nl2br(rtrim($out['warnings'])));
186 // Simplify some claim attributes for cleaner code.
187 $service_date = parse_date($out['dos']);
188 $check_date = $paydate ?
$paydate : parse_date($out['check_date']);
189 $production_date = $paydate ?
$paydate : parse_date($out['production_date']);
190 $patient_name = $arrow['name'] ?
$arrow['name'] :
191 ($out['patient_fname'] . ' ' . $out['patient_lname']);
195 // This loops once for each service item in this claim.
196 foreach ($out['svc'] as $svc) {
198 // Treat a modifier in the remit data as part of the procedure key.
199 // This key will then make its way into SQL-Ledger.
200 $codekey = $svc['code'];
201 if ($svc['mod']) $codekey .= ':' . $svc['mod'];
202 $prev = $codes[$codekey];
204 // This reports detail lines already on file for this service item.
206 writeOldDetail($prev, $patient_name, $invnumber, $service_date, $codekey, $bgcolor);
207 // Check for sanity in amount charged.
208 $prevchg = sprintf("%.2f", $prev['chg'] +
$prev['adj']);
209 if ($prevchg != abs($svc['chg'])) {
210 writeMessageLine($bgcolor, 'errdetail',
211 "EOB charge amount " . $svc['chg'] . " for this code does not match our invoice");
215 // Check for already-existing primary remittance activity.
216 // Removed this check because it was not allowing for copays manually
217 // entered into the invoice under a non-copay billing code.
219 if ((sprintf("%.2f",$prev['chg']) != sprintf("%.2f",$prev['bal']) ||
220 $prev['adj'] != 0) && $primary)
222 writeMessageLine($bgcolor, 'errdetail',
223 "This service item already has primary payments and/or adjustments!");
228 unset($codes[$codekey]);
231 // If the service item is not in our database...
234 // This is not an error. If we are not in error mode and not debugging,
235 // insert the service item into SL. Then display it (in green if it
236 // was inserted, or in red if we are in error mode).
237 $description = "CPT4:$codekey Added by $inslabel $production_date";
238 if (!$error && !$debug) {
239 slPostCharge($arrow['id'], $svc['chg'], $service_date, $codekey,
240 $insurance_id, $description, $debug);
241 $invoice_total +
= $svc['chg'];
243 $class = $error ?
'errdetail' : 'newdetail';
244 writeDetailLine($bgcolor, $class, $patient_name, $invnumber,
245 $codekey, $production_date, $description,
246 $svc['chg'], ($error ?
'' : $invoice_total));
250 $class = $error ?
'errdetail' : 'newdetail';
252 // Report Allowed Amount.
253 if ($svc['allowed']) {
254 // A problem here is that some payers will include an adjustment
255 // reflecting the allowed amount, others not. So here we need to
256 // check if the adjustment exists, and if not then create it. We
257 // assume that any nonzero CO (Contractual Obligation) adjustment
259 $contract_adj = sprintf("%.2f", $svc['chg'] - $svc['allowed']);
260 foreach ($svc['adj'] as $adj) {
261 if ($adj['group_code'] == 'CO' && $adj['amount'] != 0)
264 if ($contract_adj > 0) {
265 $svc['adj'][] = array('group_code' => 'CO', 'reason_code' => 'A2',
266 'amount' => $contract_adj);
268 writeMessageLine($bgcolor, 'infdetail',
269 'Allowed amount is ' . sprintf("%.2f", $svc['allowed']));
272 // Report miscellaneous remarks.
273 if ($svc['remark']) {
274 $rmk = $svc['remark'];
275 writeMessageLine($bgcolor, 'infdetail', "$rmk: " . $remark_codes[$rmk]);
278 // Post and report the payment for this service item from the ERA.
279 // By the way a 'Claim' level payment is probably going to be negative,
280 // i.e. a payment reversal.
282 if (!$error && !$debug) {
283 slPostPayment($arrow['id'], $svc['paid'], $check_date,
284 "$inslabel/" . $out['check_number'], $codekey, $insurance_id, $debug);
285 $invoice_total -= $svc['paid'];
287 $description = "$inslabel/" . $out['check_number'] . ' payment';
288 if ($svc['paid'] < 0) $description .= ' reversal';
289 writeDetailLine($bgcolor, $class, $patient_name, $invnumber,
290 $codekey, $check_date, $description,
291 0 - $svc['paid'], ($error ?
'' : $invoice_total));
294 // Post and report adjustments from this ERA. Posted adjustment reasons
295 // must be 25 characters or less in order to fit on patient statements.
296 foreach ($svc['adj'] as $adj) {
297 $description = $adj['reason_code'] . ': ' . $adjustment_reasons[$adj['reason_code']];
298 if ($adj['group_code'] == 'PR' ||
!$primary) {
299 // Group code PR is Patient Responsibility. Enter these as zero
300 // adjustments to retain the note without crediting the claim.
303 $reason = 'Pt resp: '; // Reasons should be 25 chars or less.
304 if ($adj['reason_code'] == '1') $reason = 'To deductible: ';
305 else if ($adj['reason_code'] == '2') $reason = 'Coinsurance: ';
306 else if ($adj['reason_code'] == '3') $reason = 'Co-pay: ';
308 $reason = "$inslabel ptresp: "; // Reasons should be 25 chars or less.
309 if ($adj['reason_code'] == '1') $reason = "$inslabel dedbl: ";
310 else if ($adj['reason_code'] == '2') $reason = "$inslabel coins: ";
311 else if ($adj['reason_code'] == '3') $reason = "$inslabel copay: ";
313 // Non-primary insurance adjustments are garbage, either repeating
314 // the primary or are not adjustments at all. Report them as notes
315 // but do not post any amounts.
317 $reason = "$inslabel note " . $adj['reason_code'] . ': ';
319 $reason .= sprintf("%.2f", $adj['amount']);
322 $reason .= sprintf("%.2f", $adj['amount']);
323 // Post a zero-dollar adjustment just to save it as a comment.
324 if (!$error && !$debug) {
325 slPostAdjustment($arrow['id'], 0, $production_date,
326 $out['check_number'], $codekey, $insurance_id,
329 writeMessageLine($bgcolor, $class, $description . ' ' .
330 sprintf("%.2f", $adj['amount']));
332 // Other group codes for primary insurance are real adjustments.
334 if (!$error && !$debug) {
335 slPostAdjustment($arrow['id'], $adj['amount'], $production_date,
336 $out['check_number'], $codekey, $insurance_id,
337 "$inslabel adjust code " . $adj['reason_code'], $debug);
338 $invoice_total -= $adj['amount'];
340 writeDetailLine($bgcolor, $class, $patient_name, $invnumber,
341 $codekey, $production_date, $description,
342 0 - $adj['amount'], ($error ?
'' : $invoice_total));
346 } // End of service item
348 // Report any existing service items not mentioned in the ERA, and
349 // determine if any of them are still missing an insurance response
350 // (if so, then insurance is not yet done with the claim).
351 $insurance_done = true;
352 foreach ($codes as $code => $prev) {
353 writeOldDetail($prev, $arrow['name'], $invnumber, $service_date, $code, $bgcolor);
354 $got_response = false;
355 foreach ($prev['dtl'] as $ddata) {
356 if ($ddata['pmt'] ||
$ddata['rsn']) $got_response = true;
358 if (!$got_response) $insurance_done = false;
361 // Cleanup: If all is well, mark Ins<x> done and check for secondary billing.
362 if (!$error && !$debug && $insurance_done) {
363 $shipvia = 'Done: Ins1';
364 if ($inslabel != 'Ins1') $shipvia .= ',Ins2';
365 if ($inslabel == 'Ins3') $shipvia .= ',Ins3';
366 $query = "UPDATE ar SET shipvia = '$shipvia' WHERE id = " . $arrow['id'];
368 if ($sl_err) die($sl_err);
369 // Check for secondary insurance.
370 $insgot = strtolower($arrow['notes']);
371 if ($primary && strpos($insgot, 'ins2') !== false) {
372 slSetupSecondary($arrow['id'], $debug);
373 writeMessageLine($bgcolor, 'infdetail',
374 'This claim is now re-queued for secondary paper billing');
380 /////////////////////////// End Functions ////////////////////////////
384 $eraname = $_GET['eraname'];
385 if (! $eraname) die(xl("You cannot access this page directly."));
387 // Open the output file early so that in case it fails, we do not post a
388 // bunch of stuff without saving the report. Also be sure to retain any old
389 // report files. Do not save the report if this is a no-update situation.
392 $nameprefix = "$webserver_root/era/$eraname";
394 for ($i = 1; is_file("$nameprefix$namesuffix.html"); ++
$i) {
397 $fnreport = "$nameprefix$namesuffix.html";
398 $fhreport = fopen($fnreport, 'w');
399 if (!$fhreport) die(xl("Cannot create") . " '$fnreport'");
406 <link rel
=stylesheet href
="<?php echo $css_header;?>" type
="text/css">
407 <style type
="text/css">
408 body
{ font
-family
:sans
-serif
; font
-size
:8pt
; font
-weight
:normal
}
409 .dehead
{ color
:#000000; font-family:sans-serif; font-size:9pt; font-weight:bold }
410 .olddetail
{ color
:#000000; font-family:sans-serif; font-size:9pt; font-weight:normal }
411 .newdetail
{ color
:#00dd00; font-family:sans-serif; font-size:9pt; font-weight:normal }
412 .errdetail
{ color
:#dd0000; font-family:sans-serif; font-size:9pt; font-weight:normal }
413 .infdetail
{ color
:#0000ff; font-family:sans-serif; font-size:9pt; font-weight:normal }
415 <title
><?php
xl('EOB Posting - Electronic Remittances','e')?
></title
>
416 <script language
="JavaScript">
419 <body leftmargin
='0' topmargin
='0' marginwidth
='0' marginheight
='0'>
422 <table border
='0' cellpadding
='2' cellspacing
='0' width
='100%'>
424 <tr bgcolor
="#cccccc">
426 <?php
xl('Patient','e') ?
>
429 <?php
xl('Invoice','e') ?
>
432 <?php
xl('Code','e') ?
>
435 <?php
xl('Date','e') ?
>
438 <?php
xl('Description','e') ?
>
440 <td
class="dehead" align
="right">
441 <?php
xl('Amount','e') ?
> 
;
443 <td
class="dehead" align
="right">
444 <?php
xl('Balance','e') ?
> 
;
449 $alertmsg = parse_era("$webserver_root/era/$eraname.edi", 'era_callback');
454 <script language
="JavaScript">
456 if ($alertmsg) echo " alert('" . htmlentities($alertmsg) . "');\n";
462 // Save all of this script's output to a report file.
464 fwrite($fhreport, ob_get_contents());