More Postings EOB features
[openemr.git] / library / parse_era.inc.php
blobf3cdd11bf1dcc21efab786e990f3eb3f074f8d10
1 <?php
2 // Copyright (C) 2006 Rod Roark <rod@sunsetsystems.com>
3 //
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 function parse_era_2100(&$out, $cb)
11 if ($out['loopid'] == '2110' || $out['loopid'] == '2100') {
12 // Production date is posted with adjustments, so make sure it exists.
13 if (!$out['production_date']) {
14 $out['production_date'] = $out['check_date'];
17 // Force the sum of service payments to equal the claim payment
18 // amount, and the sum of service adjustments to equal the CLP's
19 // (charged amount - paid amount - patient responsibility amount).
20 // This may result from claim-level adjustments, and in this case the
21 // first SVC item that we stored was a 'Claim' type. It also may result
22 // from poorly reported payment reversals, in which case we may need to
23 // create the 'Claim' service type here.
25 $paytotal = $out['amount_approved'];
26 $adjtotal = $out['amount_charged'] - $out['amount_approved'] - $out['amount_patient'];
27 foreach ($out['svc'] as $svc) {
28 $paytotal -= $svc['paid'];
29 foreach ($svc['adj'] as $adj) {
30 if ($adj['group_code'] != 'PR') {
31 $adjtotal -= $adj['amount'];
36 $paytotal = round($paytotal, 2);
37 $adjtotal = round($adjtotal, 2);
38 if ($paytotal != 0 || $adjtotal != 0) {
39 if ($out['svc'][0]['code'] != 'Claim') {
40 array_unshift($out['svc'], array());
41 $out['svc'][0]['code'] = 'Claim';
42 $out['svc'][0]['mod'] = '';
43 $out['svc'][0]['chg'] = '0';
44 $out['svc'][0]['paid'] = '0';
45 $out['svc'][0]['adj'] = array();
46 $out['warnings'] .= "Procedure 'Claim' is inserted artificially to " .
47 "force claim balancing.\n";
50 $out['svc'][0]['paid'] += $paytotal;
51 if ($adjtotal) {
52 $j = count($out['svc'][0]['adj']);
53 $out['svc'][0]['adj'][$j] = array();
54 $out['svc'][0]['adj'][$j]['group_code'] = 'CR'; // presuming a correction or reversal
55 $out['svc'][0]['adj'][$j]['reason_code'] = 'Balancing';
56 $out['svc'][0]['adj'][$j]['amount'] = $adjtotal;
59 // if ($out['svc'][0]['code'] != 'Claim') {
60 // $out['warnings'] .= "First service item payment amount " .
61 // "adjusted by $paytotal due to payment imbalance. " .
62 // "This should not happen!\n";
63 // }
66 $cb($out);
70 function parse_era($filename, $cb)
72 $delimiter1 = '~';
73 $delimiter2 = '|';
74 $delimiter3 = '^';
76 $infh = fopen($filename, 'r');
77 if (! $infh) {
78 return "ERA input file open failed";
81 $out = array();
82 $out['loopid'] = '';
83 $out['st_segment_count'] = 0;
84 $buffer = '';
85 $segid = '';
87 while (true) {
88 if (strlen($buffer) < 2048 && ! feof($infh)) {
89 $buffer .= fread($infh, 2048);
92 $tpos = strpos($buffer, $delimiter1);
93 if ($tpos === false) {
94 break;
97 $inline = substr($buffer, 0, $tpos);
98 $buffer = substr($buffer, $tpos + 1);
99 // remove carriage returns and new lines that some payers send
100 $buffer = str_replace(array("\n", "\r"), '', $buffer);
102 // If this is the ISA segment then figure out what the delimiters are.
103 if ($segid === '' && substr($inline, 0, 3) === 'ISA') {
104 $delimiter2 = substr($inline, 3, 1);
105 $delimiter3 = substr($inline, -1);
108 $seg = explode($delimiter2, $inline);
109 $segid = $seg[0];
111 if ($segid == 'ISA') {
112 if ($out['loopid']) {
113 return 'Unexpected ISA segment';
116 $out['isa_sender_id'] = trim($seg[6]);
117 $out['isa_receiver_id'] = trim($seg[8]);
118 $out['isa_control_number'] = trim($seg[13]);
119 // TBD: clear some stuff if we allow multiple transmission files.
120 } else if ($segid == 'GS') {
121 if ($out['loopid']) {
122 return 'Unexpected GS segment';
125 $out['gs_date'] = trim($seg[4]);
126 $out['gs_time'] = trim($seg[5]);
127 $out['gs_control_number'] = trim($seg[6]);
128 } else if ($segid == 'ST') {
129 parse_era_2100($out, $cb);
130 $out['loopid'] = '';
131 $out['st_control_number'] = trim($seg[2]);
132 $out['st_segment_count'] = 0;
133 } else if ($segid == 'BPR') {
134 if ($out['loopid']) {
135 return 'Unexpected BPR segment';
138 $out['check_amount'] = trim($seg[2]);
139 $out['check_date'] = trim($seg[16]); // yyyymmdd
140 // TBD: BPR04 is a payment method code.
141 } else if ($segid == 'TRN') {
142 if ($out['loopid']) {
143 return 'Unexpected TRN segment';
146 $out['check_number'] = trim($seg[2]);
147 $out['payer_tax_id'] = substr($seg[3], 1); // 9 digits
148 $out['payer_id'] = trim($seg[4]);
149 // Note: TRN04 further qualifies the paying entity within the
150 // organization identified by TRN03.
151 } else if ($segid == 'REF' && $seg[1] == 'EV') {
152 if ($out['loopid']) {
153 return 'Unexpected REF|EV segment';
155 } else if ($segid == 'CUR' && ! $out['loopid']) {
156 if ($seg[3] && $seg[3] != 1.0) {
157 return("We cannot handle foreign currencies!");
159 } else if ($segid == 'REF' && ! $out['loopid']) {
160 // ignore
161 } else if ($segid == 'DTM' && $seg[1] == '405') {
162 if ($out['loopid']) {
163 return 'Unexpected DTM|405 segment';
166 $out['production_date'] = trim($seg[2]); // yyyymmdd
167 } //
168 // Loop 1000A is Payer Information.
170 else if ($segid == 'N1' && $seg[1] == 'PR') {
171 if ($out['loopid']) {
172 return 'Unexpected N1|PR segment';
175 $out['loopid'] = '1000A';
176 $out['payer_name'] = trim($seg[2]);
177 } else if ($segid == 'N3' && $out['loopid'] == '1000A') {
178 $out['payer_street'] = trim($seg[1]);
179 // TBD: N302 may exist as an additional address line.
180 } else if ($segid == 'N4' && $out['loopid'] == '1000A') {
181 $out['payer_city'] = trim($seg[1]);
182 $out['payer_state'] = trim($seg[2]);
183 $out['payer_zip'] = trim($seg[3]);
184 } else if ($segid == 'REF' && $out['loopid'] == '1000A') {
185 // Other types of REFs may be given to identify the payer, but we
186 // ignore them.
187 } else if ($segid == 'PER' && $out['loopid'] == '1000A') {
188 // TBD: Report payer contact information as a note.
189 } //
190 // Loop 1000B is Payee Identification.
192 else if ($segid == 'N1' && $seg[1] == 'PE') {
193 if ($out['loopid'] != '1000A') {
194 return 'Unexpected N1|PE segment';
197 $out['loopid'] = '1000B';
198 $out['payee_name'] = trim($seg[2]);
199 $out['payee_tax_id'] = trim($seg[4]);
200 } else if ($segid == 'N3' && $out['loopid'] == '1000B') {
201 $out['payee_street'] = trim($seg[1]);
202 } else if ($segid == 'N4' && $out['loopid'] == '1000B') {
203 $out['payee_city'] = trim($seg[1]);
204 $out['payee_state'] = trim($seg[2]);
205 $out['payee_zip'] = trim($seg[3]);
206 } else if ($segid == 'REF' && $out['loopid'] == '1000B') {
207 // Used to report additional ID numbers. Ignored.
208 } //
209 // Loop 2000 provides for logical grouping of claim payment information.
210 // LX is required if any CLPs are present, but so far we do not care
211 // about loop 2000 content.
213 else if ($segid == 'LX') {
214 if (! $out['loopid']) {
215 return 'Unexpected LX segment';
218 parse_era_2100($out, $cb);
219 $out['loopid'] = '2000';
220 } else if ($segid == 'TS2' && $out['loopid'] == '2000') {
221 // ignore
222 } else if ($segid == 'TS3' && $out['loopid'] == '2000') {
223 // ignore
224 } //
225 // Loop 2100 is Claim Payment Information. The good stuff begins here.
227 else if ($segid == 'CLP') {
228 if (! $out['loopid']) {
229 return 'Unexpected CLP segment';
232 parse_era_2100($out, $cb);
233 $out['loopid'] = '2100';
234 $out['warnings'] = '';
235 // Clear some stuff to start the new claim:
236 $out['subscriber_lname'] = '';
237 $out['subscriber_fname'] = '';
238 $out['subscriber_mname'] = '';
239 $out['subscriber_member_id'] = '';
240 $out['crossover']=0;
241 $out['svc'] = array();
243 // This is the poorly-named "Patient Account Number". For 837p
244 // it comes from CLM01 which we populated as pid-diagid-procid,
245 // where diagid and procid are id values from the billing table.
246 // For HCFA 1500 claims it comes from field 26 which we
247 // populated with our familiar pid-encounter billing key.
249 // The 835 spec calls this the "provider-assigned claim control
250 // number" and notes that it is specifically intended for
251 // identifying the claim in the provider's database.
252 $out['our_claim_id'] = trim($seg[1]);
254 $out['claim_status_code'] = trim($seg[2]);
255 $out['amount_charged'] = trim($seg[3]);
256 $out['amount_approved'] = trim($seg[4]);
257 $out['amount_patient'] = trim($seg[5]); // pt responsibility, copay + deductible
258 $out['payer_claim_id'] = trim($seg[7]); // payer's claim number
259 } else if ($segid == 'CAS' && $out['loopid'] == '2100') {
260 // This is a claim-level adjustment and should be unusual.
261 // Handle it by creating a dummy zero-charge service item and
262 // then populating the adjustments into it. See also code in
263 // parse_era_2100() which will later plug in a payment reversal
264 // amount that offsets these adjustments.
265 $i = 0; // if present, the dummy service item will be first.
266 if (!$out['svc'][$i]) {
267 $out['svc'][$i] = array();
268 $out['svc'][$i]['code'] = 'Claim';
269 $out['svc'][$i]['mod'] = '';
270 $out['svc'][$i]['chg'] = '0';
271 $out['svc'][$i]['paid'] = '0';
272 $out['svc'][$i]['adj'] = array();
275 for ($k = 2; $k < 20; $k += 3) {
276 if (!$seg[$k]) {
277 break;
280 $j = count($out['svc'][$i]['adj']);
281 $out['svc'][$i]['adj'][$j] = array();
282 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
283 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
284 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
286 } // QC = Patient
287 else if ($segid == 'NM1' && $seg[1] == 'QC' && $out['loopid'] == '2100') {
288 $out['patient_lname'] = trim($seg[3]);
289 $out['patient_fname'] = trim($seg[4]);
290 $out['patient_mname'] = trim($seg[5]);
291 $out['patient_member_id'] = trim($seg[9]);
292 } // IL = Insured or Subscriber
293 else if ($segid == 'NM1' && $seg[1] == 'IL' && $out['loopid'] == '2100') {
294 $out['subscriber_lname'] = trim($seg[3]);
295 $out['subscriber_fname'] = trim($seg[4]);
296 $out['subscriber_mname'] = trim($seg[5]);
297 $out['subscriber_member_id'] = trim($seg[9]);
298 } // 82 = Rendering Provider
299 else if ($segid == 'NM1' && $seg[1] == '82' && $out['loopid'] == '2100') {
300 $out['provider_lname'] = trim($seg[3]);
301 $out['provider_fname'] = trim($seg[4]);
302 $out['provider_mname'] = trim($seg[5]);
303 $out['provider_member_id'] = trim($seg[9]);
304 } else if ($segid == 'NM1' && $seg[1] == 'TT' && $out['loopid'] == '2100') {
305 $out['crossover'] = 1;//Claim automatic forward case.
306 } // 74 = Corrected Insured
307 // TT = Crossover Carrier (Transfer To another payer)
308 // PR = Corrected Payer
309 else if ($segid == 'NM1' && $out['loopid'] == '2100') {
310 // $out['warnings'] .= "NM1 segment at claim level ignored.\n";
311 } else if ($segid == 'MOA' && $out['loopid'] == '2100') {
312 $out['warnings'] .= "MOA segment at claim level ignored.\n";
313 } // REF segments may provide various identifying numbers, where REF02
314 // indicates the type of number.
315 else if ($segid == 'REF' && $seg[1] == '1W' && $out['loopid'] == '2100') {
316 $out['claim_comment'] = trim($seg[2]);
317 } else if ($segid == 'REF' && $out['loopid'] == '2100') {
318 // ignore
319 } else if ($segid == 'DTM' && $seg[1] == '050' && $out['loopid'] == '2100') {
320 $out['claim_date'] = trim($seg[2]); // yyyymmdd
321 } // 036 = expiration date of coverage
322 // 050 = date claim received by payer
323 // 232 = claim statement period start
324 // 233 = claim statement period end
325 else if ($segid == 'DTM' && $out['loopid'] == '2100') {
326 // ignore?
327 } else if ($segid == 'PER' && $out['loopid'] == '2100') {
328 $out['payer_insurance'] = trim($seg[2]);
329 $out['warnings'] .= 'Claim contact information: ' .
330 $seg[4] . "\n";
331 } // For AMT01 see the Amount Qualifier Codes on pages 135-135 of the
332 // Implementation Guide. AMT is only good for comments and is not
333 // part of claim balancing.
334 else if ($segid == 'AMT' && $out['loopid'] == '2100') {
335 $out['warnings'] .= "AMT segment at claim level ignored.\n";
336 } // For QTY01 see the Quantity Qualifier Codes on pages 137-138 of the
337 // Implementation Guide. QTY is only good for comments and is not
338 // part of claim balancing.
339 else if ($segid == 'QTY' && $out['loopid'] == '2100') {
340 $out['warnings'] .= "QTY segment at claim level ignored.\n";
341 } //
342 // Loop 2110 is Service Payment Information.
344 else if ($segid == 'SVC') {
345 if (! $out['loopid']) {
346 return 'Unexpected SVC segment';
349 $out['loopid'] = '2110';
350 if ($seg[6]) {
351 // SVC06 if present is our original procedure code that they are changing.
352 // We will not put their crap in our invoice, but rather log a note and
353 // treat it as adjustments to our originally submitted coding.
354 $svc = explode($delimiter3, $seg[6]);
355 $tmp = explode($delimiter3, $seg[1]);
356 $out['warnings'] .= "Payer is restating our procedure " . $svc[1] .
357 " as " . $tmp[1] . ".\n";
358 } else {
359 $svc = explode($delimiter3, $seg[1]);
362 if ($svc[0] != 'HC') {
363 return 'SVC segment has unexpected qualifier';
366 // TBD: Other qualifiers are possible; see IG pages 140-141.
367 $i = count($out['svc']);
368 $out['svc'][$i] = array();
369 // It seems some payers append the modifier with no separator!
370 if (strlen($svc[1]) == 7 && empty($svc[2])) {
371 $out['svc'][$i]['code'] = substr($svc[1], 0, 5);
372 $out['svc'][$i]['mod'] = substr($svc[1], 5);
373 } else {
374 $out['svc'][$i]['code'] = $svc[1];
375 $out['svc'][$i]['mod'] = $svc[2] ? $svc[2] . ':' : '';
376 $out['svc'][$i]['mod'] .= $svc[3] ? $svc[3] . ':' : '';
377 $out['svc'][$i]['mod'] .= $svc[4] ? $svc[4] . ':' : '';
378 $out['svc'][$i]['mod'] .= $svc[5] ? $svc[5] . ':' : '';
379 $out['svc'][$i]['mod'] = preg_replace('/:$/', '', $out['svc'][$i]['mod']);
382 $out['svc'][$i]['chg'] = $seg[2];
383 $out['svc'][$i]['paid'] = $seg[3];
384 $out['svc'][$i]['adj'] = array();
385 // Note: SVC05, if present, indicates the paid units of service.
386 // It defaults to 1.
387 } // DTM01 identifies the type of service date:
388 // 472 = a single date of service
389 // 150 = service period start
390 // 151 = service period end
391 else if ($segid == 'DTM' && $out['loopid'] == '2110') {
392 $out['dos'] = trim($seg[2]); // yyyymmdd
393 } else if ($segid == 'CAS' && $out['loopid'] == '2110') {
394 $i = count($out['svc']) - 1;
395 for ($k = 2; $k < 20; $k += 3) {
396 if (!$seg[$k]) {
397 break;
400 if ($seg[1] == 'CO' && $seg[$k+1] < 0) {
401 $out['warnings'] .= "Negative Contractual Obligation adjustment " .
402 "seems wrong. Inverting, but should be checked!\n";
403 $seg[$k+1] = 0 - $seg[$k+1];
406 $j = count($out['svc'][$i]['adj']);
407 $out['svc'][$i]['adj'][$j] = array();
408 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
409 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
410 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
411 // Note: $seg[$k+2] is "quantity". A value here indicates a change to
412 // the number of units of service. We're ignoring that for now.
414 } else if ($segid == 'REF' && $out['loopid'] == '2110') {
415 // ignore
416 } else if ($segid == 'AMT' && $seg[1] == 'B6' && $out['loopid'] == '2110') {
417 $i = count($out['svc']) - 1;
418 $out['svc'][$i]['allowed'] = $seg[2]; // report this amount as a note
419 } else if ($segid == 'AMT' && $out['loopid'] == '2110') {
420 $out['warnings'] .= "$inline at service level ignored.\n";
421 } else if ($segid == 'LQ' && $seg[1] == 'HE' && $out['loopid'] == '2110') {
422 $i = count($out['svc']) - 1;
423 $out['svc'][$i]['remark'] = $seg[2];
424 } else if ($segid == 'QTY' && $out['loopid'] == '2110') {
425 $out['warnings'] .= "QTY segment at service level ignored.\n";
426 } else if ($segid == 'PLB') {
427 // Provider-level adjustments are a General Ledger thing and should not
428 // alter the A/R for the claim, so we just report them as notes.
429 for ($k = 3; $k < 15; $k += 2) {
430 if (!$seg[$k]) {
431 break;
434 $out['warnings'] .= 'PROVIDER LEVEL ADJUSTMENT (not claim-specific): $' .
435 sprintf('%.2f', $seg[$k+1]) . " with reason code " . $seg[$k] . "\n";
436 // Note: For PLB adjustment reason codes see IG pages 165-170.
438 } else if ($segid == 'SE') {
439 parse_era_2100($out, $cb);
440 $out['loopid'] = '';
441 if ($out['st_control_number'] != trim($seg[2])) {
442 return 'Ending transaction set control number mismatch';
445 if (($out['st_segment_count'] + 1) != trim($seg[1])) {
446 return 'Ending transaction set segment count mismatch';
448 } else if ($segid == 'GE') {
449 if ($out['loopid']) {
450 return 'Unexpected GE segment';
453 if ($out['gs_control_number'] != trim($seg[2])) {
454 return 'Ending functional group control number mismatch';
456 } else if ($segid == 'IEA') {
457 if ($out['loopid']) {
458 return 'Unexpected IEA segment';
461 if ($out['isa_control_number'] != trim($seg[2])) {
462 return 'Ending interchange control number mismatch';
464 } else {
465 return "Unknown or unexpected segment ID $segid";
468 ++$out['st_segment_count'];
471 if ($segid != 'IEA') {
472 return 'Premature end of ERA file';
475 return '';
477 //for getting the check details and provider details
478 function parse_era_for_check($filename)
480 $delimiter1 = '~';
481 $delimiter2 = '|';
482 $delimiter3 = '^';
484 $infh = fopen($filename, 'r');
485 if (! $infh) {
486 return "ERA input file open failed";
489 $out = array();
490 $out['loopid'] = '';
491 $out['st_segment_count'] = 0;
492 $buffer = '';
493 $segid = '';
494 $check_count=0;
495 while (true) {
496 if (strlen($buffer) < 2048 && ! feof($infh)) {
497 $buffer .= fread($infh, 2048);
500 $tpos = strpos($buffer, $delimiter1);
501 if ($tpos === false) {
502 break;
505 $inline = substr($buffer, 0, $tpos);
506 $buffer = substr($buffer, $tpos + 1);
507 // remove carriage returns and new lines that some payers send
508 $buffer = str_replace(array("\n", "\r"), '', $buffer);
510 // If this is the ISA segment then figure out what the delimiters are.
511 if ($segid === '' && substr($inline, 0, 3) === 'ISA') {
512 $delimiter2 = substr($inline, 3, 1);
513 $delimiter3 = substr($inline, -1);
516 $seg = explode($delimiter2, $inline);
517 $segid = $seg[0];
519 if ($segid == 'ISA') {
520 } else if ($segid == 'BPR') {
521 ++$check_count;
522 //if ($out['loopid']) return 'Unexpected BPR segment';
523 $out['check_amount'.$check_count] = trim($seg[2]);
524 $out['check_date'.$check_count] = trim($seg[16]); // yyyymmdd
525 // TBD: BPR04 is a payment method code.
526 } else if ($segid == 'N1' && $seg[1] == 'PE') {
527 //if ($out['loopid'] != '1000A') return 'Unexpected N1|PE segment';
528 $out['loopid'] = '1000B';
529 $out['payee_name'.$check_count] = trim($seg[2]);
530 $out['payee_tax_id'.$check_count] = trim($seg[4]);
531 } else if ($segid == 'TRN') {
532 //if ($out['loopid']) return 'Unexpected TRN segment';
533 $out['check_number'.$check_count] = trim($seg[2]);
534 $out['payer_tax_id'.$check_count] = substr($seg[3], 1); // 9 digits
535 $out['payer_id'.$check_count] = trim($seg[4]);
536 // Note: TRN04 further qualifies the paying entity within the
537 // organization identified by TRN03.
543 ++$out['st_segment_count'];
546 $out['check_count']=$check_count;
547 era_callback_check($out);
549 if ($segid != 'IEA') {
550 return 'Premature end of ERA file';
553 return '';