CCDA server fixups (#4356)
[openemr.git] / src / Billing / ParseERA.php
blob42615e6294ed027fbdbee7056eb17961be5eeb6a
1 <?php
3 /*
4 * The functions of this class support the billing process like the script billing_process.php.
6 * @package OpenEMR
7 * @author Rod Roark <rod@sunsetsystems.com>
8 * @author Stephen Waite <stephen.waite@cmsvt.com>
9 * @copyright Copyright (c) 2006 Rod Roark <rod@sunsetsystems.com>
10 * @copyright Copyright (c) 2019 Stephen Waite <stephen.waite@cmsvt.com>
11 * @link https://www.open-emr.org
12 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
15 namespace OpenEMR\Billing;
17 class ParseERA
19 public static function parseERA2100(&$out, $cb)
21 if ($out['loopid'] == '2110' || $out['loopid'] == '2100') {
22 // Production date is posted with adjustments, so make sure it exists.
23 if (!$out['production_date']) {
24 $out['production_date'] = $out['check_date'];
27 // Force the sum of service payments to equal the claim payment
28 // amount, and the sum of service adjustments to equal the CLP's
29 // (charged amount - paid amount - patient responsibility amount).
30 // This may result from claim-level adjustments, and in this case the
31 // first SVC item that we stored was a 'Claim' type. It also may result
32 // from poorly reported payment reversals, in which case we may need to
33 // create the 'Claim' service type here.
35 $paytotal = $out['amount_approved'];
36 $adjtotal = $out['amount_charged'] - $out['amount_approved'] - $out['amount_patient'];
37 foreach ($out['svc'] as $svc) {
38 $paytotal -= $svc['paid'];
39 foreach ($svc['adj'] as $adj) {
40 if ($adj['group_code'] != 'PR') {
41 $adjtotal -= $adj['amount'];
46 $paytotal = round($paytotal, 2);
47 $adjtotal = round($adjtotal, 2);
48 if ($paytotal != 0 || $adjtotal != 0) {
49 if ($out['svc'][0]['code'] != 'Claim') {
50 array_unshift($out['svc'], array());
51 $out['svc'][0]['code'] = 'Claim';
52 $out['svc'][0]['mod'] = '';
53 $out['svc'][0]['chg'] = '0';
54 $out['svc'][0]['paid'] = '0';
55 $out['svc'][0]['adj'] = array();
56 $out['warnings'] .= "Procedure 'Claim' is inserted artificially to " .
57 "force claim balancing.\n";
60 $out['svc'][0]['paid'] += $paytotal;
61 if ($adjtotal) {
62 $j = count($out['svc'][0]['adj']);
63 $out['svc'][0]['adj'][$j] = array();
64 $out['svc'][0]['adj'][$j]['group_code'] = 'CR'; // presuming a correction or reversal
65 $out['svc'][0]['adj'][$j]['reason_code'] = 'Balancing';
66 $out['svc'][0]['adj'][$j]['amount'] = $adjtotal;
69 // if ($out['svc'][0]['code'] != 'Claim') {
70 // $out['warnings'] .= "First service item payment amount " .
71 // "adjusted by $paytotal due to payment imbalance. " .
72 // "This should not happen!\n";
73 // }
76 $cb($out);
80 public static function parseERA($filename, $cb)
82 $delimiter1 = '~';
83 $delimiter2 = '|';
84 $delimiter3 = '^';
86 $infh = fopen($filename, 'r');
87 if (!$infh) {
88 return "ERA input file open failed";
91 $out = array();
92 $out['loopid'] = '';
93 $out['st_segment_count'] = 0;
94 $buffer = '';
95 $segid = '';
97 while (true) {
98 if (strlen($buffer) < 2048 && !feof($infh)) {
99 $buffer .= fread($infh, 2048);
102 $tpos = strpos($buffer, $delimiter1);
103 if ($tpos === false) {
104 break;
107 $inline = substr($buffer, 0, $tpos);
108 $buffer = substr($buffer, $tpos + 1);
109 // remove carriage returns and new lines that some payers send
110 $buffer = str_replace(array("\n", "\r"), '', $buffer);
112 // If this is the ISA segment then figure out what the delimiters are.
113 if ($segid === '' && substr($inline, 0, 3) === 'ISA') {
114 $delimiter2 = substr($inline, 3, 1);
115 $delimiter3 = substr($inline, -1);
118 $seg = explode($delimiter2, $inline);
119 $segid = $seg[0];
121 if ($segid == 'ISA') {
122 if ($out['loopid']) {
123 return 'Unexpected ISA segment';
126 $out['isa_sender_id'] = trim($seg[6]);
127 $out['isa_receiver_id'] = trim($seg[8]);
128 $out['isa_control_number'] = trim($seg[13]);
129 // TBD: clear some stuff if we allow multiple transmission files.
130 } elseif ($segid == 'GS') {
131 if ($out['loopid']) {
132 return 'Unexpected GS segment';
135 $out['gs_date'] = trim($seg[4]);
136 $out['gs_time'] = trim($seg[5]);
137 $out['gs_control_number'] = trim($seg[6]);
138 } elseif ($segid == 'ST') {
139 self::parseERA2100($out, $cb);
140 $out['loopid'] = '';
141 $out['st_control_number'] = trim($seg[2]);
142 $out['st_segment_count'] = 0;
143 } elseif ($segid == 'BPR') {
144 if ($out['loopid']) {
145 return 'Unexpected BPR segment';
148 $out['check_amount'] = trim($seg[2]);
149 $out['check_date'] = trim($seg[16]); // yyyymmdd
150 // TBD: BPR04 is a payment method code.
151 } elseif ($segid == 'TRN') {
152 if ($out['loopid']) {
153 return 'Unexpected TRN segment';
156 $out['check_number'] = trim($seg[2]);
157 $out['payer_tax_id'] = substr($seg[3], 1); // 9 digits
158 // $out['payer_id'] = trim($seg[4]); no payer id in TRN04 for 5010
159 // Note: TRN04 further qualifies the paying entity within the
160 // organization identified by TRN03.
161 } elseif ($segid == 'REF' && $seg[1] == 'EV') {
162 if ($out['loopid']) {
163 return 'Unexpected REF|EV segment';
165 } elseif ($segid == 'CUR' && !$out['loopid']) {
166 if ($seg[3] && $seg[3] != 1.0) {
167 return ("We cannot handle foreign currencies!");
169 } elseif ($segid == 'REF' && !$out['loopid']) {
170 // ignore
171 } elseif ($segid == 'DTM' && $seg[1] == '405') {
172 if ($out['loopid']) {
173 return 'Unexpected DTM|405 segment';
176 $out['production_date'] = trim($seg[2]); // yyyymmdd
177 } elseif ($segid == 'N1' && $seg[1] == 'PR') { // Loop 1000A is Payer Information.
178 if ($out['loopid']) {
179 return 'Unexpected N1|PR segment';
181 $out['loopid'] = '1000A';
182 $out['payer_name'] = trim($seg[2]);
183 $out['payer_id'] = trim($seg[4]); // will be overwritten if in REF*2U below
184 } elseif ($segid == 'N3' && $out['loopid'] == '1000A') {
185 $out['payer_street'] = trim($seg[1]);
186 // TBD: N302 may exist as an additional address line.
187 } elseif ($segid == 'N4' && $out['loopid'] == '1000A') {
188 $out['payer_city'] = trim($seg[1]);
189 $out['payer_state'] = trim($seg[2]);
190 $out['payer_zip'] = trim($seg[3]);
191 } elseif ($segid == 'REF' && $out['loopid'] == '1000A') {
192 // Other types of REFs may be given to identify the payer, but we
193 // ignore them.
194 if (trim($seg[1] == '2U')) {
195 $out['payer_id'] = trim($seg[2]);
197 } elseif ($segid == 'PER' && $out['loopid'] == '1000A') {
198 // TBD: Report payer contact information as a note.
199 } elseif ($segid == 'N1' && $seg[1] == 'PE') { // Loop 1000B is Payee Identification.
200 if ($out['loopid'] != '1000A') {
201 return 'Unexpected N1|PE segment';
203 $out['loopid'] = '1000B';
204 $out['payee_name'] = trim($seg[2]);
205 $out['payee_tax_id'] = trim($seg[4]);
206 } elseif ($segid == 'N3' && $out['loopid'] == '1000B') {
207 $out['payee_street'] = trim($seg[1]);
208 } elseif ($segid == 'N4' && $out['loopid'] == '1000B') {
209 $out['payee_city'] = trim($seg[1]);
210 $out['payee_state'] = trim($seg[2]);
211 $out['payee_zip'] = trim($seg[3]);
212 } elseif ($segid == 'REF' && $out['loopid'] == '1000B') {
213 // Used to report additional ID numbers. Ignored.
214 } elseif ($segid == 'LX') {
216 // Loop 2000 provides for logical grouping of claim payment information.
217 // LX is required if any CLPs are present, but so far we do not care
218 // about loop 2000 content.
220 if (!$out['loopid']) {
221 return 'Unexpected LX segment';
224 self::parseERA2100($out, $cb);
225 $out['loopid'] = '2000';
226 } elseif ($segid == 'TS2' && $out['loopid'] == '2000') {
227 // ignore
228 } elseif ($segid == 'TS3' && $out['loopid'] == '2000') {
229 // ignore
230 } elseif ($segid == 'CLP') {
231 // Loop 2100 is Claim Payment Information. The good stuff begins here.
232 if (!$out['loopid']) {
233 return 'Unexpected CLP segment';
236 self::parseERA2100($out, $cb);
237 $out['loopid'] = '2100';
238 $out['warnings'] = '';
239 // Clear some stuff to start the new claim:
240 $out['subscriber_lname'] = '';
241 $out['subscriber_fname'] = '';
242 $out['subscriber_mname'] = '';
243 $out['subscriber_member_id'] = '';
244 $out['crossover'] = 0;
245 $out['corrected'] = 0;
246 $out['svc'] = array();
248 // This is the poorly-named "Patient Account Number". For 837p
249 // it comes from CLM01 which we populated as pid-diagid-procid,
250 // where diagid and procid are id values from the billing table.
251 // For HCFA 1500 claims it comes from field 26 which we
252 // populated with our familiar pid-encounter billing key.
254 // The 835 spec calls this the "provider-assigned claim control
255 // number" and notes that it is specifically intended for
256 // identifying the claim in the provider's database.
257 $out['our_claim_id'] = trim($seg[1]);
259 $out['claim_status_code'] = trim($seg[2]);
260 $out['amount_charged'] = trim($seg[3]);
261 $out['amount_approved'] = trim($seg[4]);
262 $out['amount_patient'] = trim($seg[5]); // pt responsibility, copay + deductible
263 $out['payer_claim_id'] = trim($seg[7]); // payer's claim number
264 } elseif ($segid == 'CAS' && $out['loopid'] == '2100') {
265 // This is a claim-level adjustment and should be unusual.
266 // Handle it by creating a dummy zero-charge service item and
267 // then populating the adjustments into it. See also code in
268 // self::parseERA2100() which will later plug in a payment reversal
269 // amount that offsets these adjustments.
270 $i = 0; // if present, the dummy service item will be first.
271 if (!$out['svc'][$i]) {
272 $out['svc'][$i] = array();
273 $out['svc'][$i]['code'] = 'Claim';
274 $out['svc'][$i]['mod'] = '';
275 $out['svc'][$i]['chg'] = '0';
276 $out['svc'][$i]['paid'] = '0';
277 $out['svc'][$i]['adj'] = array();
280 for ($k = 2; $k < 20; $k += 3) {
281 if (!$seg[$k]) {
282 break;
285 $j = count($out['svc'][$i]['adj']);
286 $out['svc'][$i]['adj'][$j] = array();
287 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
288 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
289 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k + 1];
291 } elseif ($segid == 'NM1' && $seg[1] == 'QC' && $out['loopid'] == '2100') { // QC = Patient
292 $out['patient_lname'] = trim($seg[3]);
293 $out['patient_fname'] = trim($seg[4]);
294 $out['patient_mname'] = trim($seg[5]);
295 $out['patient_member_id'] = trim($seg[9]);
296 } elseif ($segid == 'NM1' && $seg[1] == 'IL' && $out['loopid'] == '2100') { // IL = Insured or Subscriber
297 $out['subscriber_lname'] = trim($seg[3]);
298 $out['subscriber_fname'] = trim($seg[4]);
299 $out['subscriber_mname'] = trim($seg[5]);
300 $out['subscriber_member_id'] = trim($seg[9]);
301 } elseif ($segid == 'NM1' && $seg[1] == '82' && $out['loopid'] == '2100') { // 82 = Rendering Provider
302 $out['provider_lname'] = trim($seg[3]);
303 $out['provider_fname'] = trim($seg[4]);
304 $out['provider_mname'] = trim($seg[5]);
305 $out['provider_member_id'] = trim($seg[9]);
306 } elseif ($segid == 'NM1' && $seg[1] == 'TT' && $out['loopid'] == '2100') { // TT = Crossover Carrier (Transfer To another payer)
307 $out['crossover'] = 1; //Claim automatic forward case.
308 } elseif ($segid == 'NM1' && $seg[1] == '74' && $out['loopid'] == '2100') { // 74 = Corrected Insured
309 $out['corrected'] = 1; // Updated policy number case.
310 $out['corrected_mbi'] = trim($seg[9]); // Usually MBI from Medicare
311 } elseif ($segid == 'NM1' && $out['loopid'] == '2100') { // PR = Corrected Payer
312 // $out['warnings'] .= "NM1 segment at claim level ignored.\n";
313 } elseif ($segid == 'MOA' && $out['loopid'] == '2100') {
314 $out['warnings'] .= "MOA segment at claim level ignored.\n";
315 } elseif ($segid == 'REF' && $seg[1] == '1W' && $out['loopid'] == '2100') {
316 // REF segments may provide various identifying numbers, where REF02
317 // indicates the type of number.
318 $out['claim_comment'] = trim($seg[2]);
319 } elseif ($segid == 'REF' && $out['loopid'] == '2100') {
320 // ignore
321 } elseif ($segid == 'DTM' && $seg[1] == '050' && $out['loopid'] == '2100') {
322 $out['claim_date'] = trim($seg[2]); // yyyymmdd
323 } elseif ($segid == 'DTM' && $out['loopid'] == '2100') { // 036 = expiration date of coverage
324 // 050 = date claim received by payer
325 // 232 = claim statement period start
326 // 233 = claim statement period end
327 // ignore?
328 } elseif ($segid == 'PER' && $out['loopid'] == '2100') {
329 $out['payer_insurance'] = trim($seg[2]);
330 $out['warnings'] .= 'Claim contact information: ' .
331 $seg[4] . "\n";
332 } elseif ($segid == 'AMT' && $out['loopid'] == '2100') { // For AMT01 see the Amount Qualifier Codes on
333 // pages 135-135 of the Implementation Guide.
334 // AMT is only good for comments and is not part of claim balancing.
335 $out['warnings'] .= "AMT segment at claim level ignored.\n";
336 } elseif ($segid == 'QTY' && $out['loopid'] == '2100') { // For QTY01 see the Quantity Qualifier Codes on
337 // pages 137-138 of the Implementation Guide.
338 // QTY is only good for comments and is not part of claim balancing.
339 $out['warnings'] .= "QTY segment at claim level ignored.\n";
340 } elseif ($segid == 'SVC') { // Loop 2110 is Service Payment Information.
341 if (!$out['loopid']) {
342 return 'Unexpected SVC segment';
345 $out['loopid'] = '2110';
346 if ($seg[6]) {
347 // SVC06 if present is our original procedure code that they are changing.
348 // We will not put their crap in our invoice, but rather log a note and
349 // treat it as adjustments to our originally submitted coding.
350 $svc = explode($delimiter3, $seg[6]);
351 $tmp = explode($delimiter3, $seg[1]);
352 $out['warnings'] .= "Payer is restating our procedure " . $svc[1] .
353 " as " . $tmp[1] . ".\n";
354 } else {
355 $svc = explode($delimiter3, $seg[1]);
358 if ($svc[0] != 'HC') {
359 return 'SVC segment has unexpected qualifier';
362 // TBD: Other qualifiers are possible; see IG pages 140-141.
363 $i = count($out['svc']);
364 $out['svc'][$i] = array();
365 // It seems some payers append the modifier with no separator!
366 if (strlen($svc[1]) == 7 && empty($svc[2])) {
367 $out['svc'][$i]['code'] = substr($svc[1], 0, 5);
368 $out['svc'][$i]['mod'] = substr($svc[1], 5);
369 } else {
370 $out['svc'][$i]['code'] = $svc[1];
371 $out['svc'][$i]['mod'] = $svc[2] ? $svc[2] . ':' : '';
372 $out['svc'][$i]['mod'] .= $svc[3] ? $svc[3] . ':' : '';
373 $out['svc'][$i]['mod'] .= $svc[4] ? $svc[4] . ':' : '';
374 $out['svc'][$i]['mod'] .= $svc[5] ? $svc[5] . ':' : '';
375 $out['svc'][$i]['mod'] = preg_replace('/:$/', '', $out['svc'][$i]['mod']);
378 $out['svc'][$i]['chg'] = $seg[2];
379 $out['svc'][$i]['paid'] = $seg[3];
380 $out['svc'][$i]['adj'] = array();
381 // Note: SVC05, if present, indicates the paid units of service.
382 // It defaults to 1.
383 } elseif ($segid == 'DTM' && $out['loopid'] == '2110') { // DTM01 identifies the type of service date:
384 // 472 = a single date of service
385 // 150 = service period start
386 // 151 = service period end
387 $out['dos'] = trim($seg[2]); // yyyymmdd
388 } elseif ($segid == 'CAS' && $out['loopid'] == '2110') {
389 $i = count($out['svc']) - 1;
390 for ($k = 2; $k < 20; $k += 3) {
391 if (!$seg[$k]) {
392 break;
395 // removing inversion for CO*144 MIPS incentive adjustment to prevent claim balancing
396 if ($seg[1] == 'CO' && $seg[$k + 1] < 0 && $seg[$k] !== '144') {
397 $out['warnings'] .= "Negative Contractual Obligation adjustment " .
398 "seems wrong. Inverting, but should be checked!\n";
399 $seg[$k + 1] = 0 - $seg[$k + 1];
402 $j = count($out['svc'][$i]['adj']);
403 $out['svc'][$i]['adj'][$j] = array();
404 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
405 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
406 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k + 1];
407 // Note: $seg[$k+2] is "quantity". A value here indicates a change to
408 // the number of units of service. We're ignoring that for now.
410 } elseif ($segid == 'REF' && $out['loopid'] == '2110') {
411 // ignore
412 } elseif ($segid == 'AMT' && $seg[1] == 'B6' && $out['loopid'] == '2110') {
413 $i = count($out['svc']) - 1;
414 $out['svc'][$i]['allowed'] = $seg[2]; // report this amount as a note
415 } elseif ($segid == 'AMT' && $out['loopid'] == '2110') {
416 $out['warnings'] .= "$inline at service level ignored.\n";
417 } elseif ($segid == 'LQ' && $seg[1] == 'HE' && $out['loopid'] == '2110') {
418 $i = count($out['svc']) - 1;
419 $out['svc'][$i]['remark'] = $seg[2];
420 } elseif ($segid == 'QTY' && $out['loopid'] == '2110') {
421 $out['warnings'] .= "QTY segment at service level ignored.\n";
422 } elseif ($segid == 'PLB') {
423 // Provider-level adjustments are a General Ledger thing and should not
424 // alter the A/R for the claim, so we just report them as notes.
425 for ($k = 3; $k < 15; $k += 2) {
426 if (!$seg[$k]) {
427 break;
430 $out['warnings'] .= 'PROVIDER LEVEL ADJUSTMENT (not claim-specific): $' .
431 sprintf('%.2f', $seg[$k + 1]) . " with reason code " . $seg[$k] . "\n";
432 // Note: For PLB adjustment reason codes see IG pages 165-170.
434 } elseif ($segid == 'SE') {
435 self::parseERA2100($out, $cb);
436 $out['loopid'] = '';
437 if ($out['st_control_number'] != trim($seg[2])) {
438 return 'Ending transaction set control number mismatch';
441 if (($out['st_segment_count'] + 1) != trim($seg[1])) {
442 return 'Ending transaction set segment count mismatch';
444 } elseif ($segid == 'GE') {
445 if ($out['loopid']) {
446 return 'Unexpected GE segment';
449 if ($out['gs_control_number'] != trim($seg[2])) {
450 return 'Ending functional group control number mismatch';
452 } elseif ($segid == 'IEA') {
453 if ($out['loopid']) {
454 return 'Unexpected IEA segment';
457 if ($out['isa_control_number'] != trim($seg[2])) {
458 return 'Ending interchange control number mismatch';
460 } else {
461 return "Unknown or unexpected segment ID $segid";
464 ++$out['st_segment_count'];
467 if ($segid != 'IEA') {
468 return 'Premature end of ERA file';
471 return '';
474 //for getting the check details and provider details
475 public static function parseERAForCheck($filename)
477 $delimiter1 = '~';
478 $delimiter2 = '|';
479 $delimiter3 = '^';
481 $infh = fopen($filename, 'r');
482 if (!$infh) {
483 return "ERA input file open failed";
486 $out = array();
487 $out['loopid'] = '';
488 $out['st_segment_count'] = 0;
489 $buffer = '';
490 $segid = '';
491 $check_count = 0;
492 while (true) {
493 if (strlen($buffer) < 2048 && !feof($infh)) {
494 $buffer .= fread($infh, 2048);
497 $tpos = strpos($buffer, $delimiter1);
498 if ($tpos === false) {
499 break;
502 $inline = substr($buffer, 0, $tpos);
503 $buffer = substr($buffer, $tpos + 1);
504 // remove carriage returns and new lines that some payers send
505 $buffer = str_replace(array("\n", "\r"), '', $buffer);
507 // If this is the ISA segment then figure out what the delimiters are.
508 if ($segid === '' && substr($inline, 0, 3) === 'ISA') {
509 $delimiter2 = substr($inline, 3, 1);
510 $delimiter3 = substr($inline, -1);
513 $seg = explode($delimiter2, $inline);
514 $segid = $seg[0];
516 if ($segid == 'ISA') {
517 } elseif ($segid == 'BPR') {
518 ++$check_count;
519 //if ($out['loopid']) return 'Unexpected BPR segment';
520 $out['check_amount' . $check_count] = trim($seg[2]);
521 $out['check_date' . $check_count] = trim($seg[16]); // yyyymmdd
522 // TBD: BPR04 is a payment method code.
523 } elseif ($segid == 'N1' && $seg[1] == 'PE') {
524 //if ($out['loopid'] != '1000A') return 'Unexpected N1|PE segment';
525 $out['loopid'] = '1000B';
526 $out['payee_name' . $check_count] = trim($seg[2]);
527 $out['payee_tax_id' . $check_count] = trim($seg[4]);
528 } elseif ($segid == 'TRN') {
529 //if ($out['loopid']) return 'Unexpected TRN segment';
530 $out['check_number' . $check_count] = trim($seg[2]);
531 $out['payer_tax_id' . $check_count] = substr($seg[3], 1); // 9 digits
532 $out['payer_id' . $check_count] = trim($seg[4]);
533 // Note: TRN04 further qualifies the paying entity within the
534 // organization identified by TRN03.
538 ++$out['st_segment_count'];
541 $out['check_count'] = $check_count;
542 era_callback_check($out);
544 if ($segid != 'IEA') {
545 return 'Premature end of ERA file';
548 return '';