Added access controls for encounter categories
[openemr.git] / library / parse_era.inc.php
blobcbf4d1d867d0db30dd5e10d48f2e1011f083b7f7
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') {
13 // Production date is posted with adjustments, so make sure it exists.
14 if (!$out['production_date']) $out['production_date'] = $out['check_date'];
16 // Force the sum of service payments to equal the claim payment
17 // amount, and the sum of service adjustments to equal the CLP's
18 // (charged amount - paid amount - patient responsibility amount).
19 // This may result from claim-level adjustments, and in this case the
20 // first SVC item that we stored was a 'Claim' type. It also may result
21 // from poorly reported payment reversals, in which case we may need to
22 // create the 'Claim' service type here.
24 $paytotal = $out['amount_approved'];
25 $adjtotal = $out['amount_charged'] - $out['amount_approved'] - $out['amount_patient'];
26 foreach ($out['svc'] as $svc) {
27 $paytotal -= $svc['paid'];
28 foreach ($svc['adj'] as $adj) {
29 if ($adj['group_code'] != 'PR') $adjtotal -= $adj['amount'];
32 $paytotal = round($paytotal, 2);
33 $adjtotal = round($adjtotal, 2);
34 if ($paytotal != 0 || $adjtotal != 0) {
35 if ($out['svc'][0]['code'] != 'Claim') {
36 array_unshift($out['svc'], array());
37 $out['svc'][0]['code'] = 'Claim';
38 $out['svc'][0]['mod'] = '';
39 $out['svc'][0]['chg'] = '0';
40 $out['svc'][0]['paid'] = '0';
41 $out['svc'][0]['adj'] = array();
42 $out['warnings'] .= "Procedure 'Claim' is inserted artificially to " .
43 "force claim balancing.\n";
45 $out['svc'][0]['paid'] += $paytotal;
46 if ($adjtotal) {
47 $j = count($out['svc'][0]['adj']);
48 $out['svc'][0]['adj'][$j] = array();
49 $out['svc'][0]['adj'][$j]['group_code'] = 'CR'; // presuming a correction or reversal
50 $out['svc'][0]['adj'][$j]['reason_code'] = 'Balancing';
51 $out['svc'][0]['adj'][$j]['amount'] = $adjtotal;
53 // if ($out['svc'][0]['code'] != 'Claim') {
54 // $out['warnings'] .= "First service item payment amount " .
55 // "adjusted by $paytotal due to payment imbalance. " .
56 // "This should not happen!\n";
57 // }
60 $cb($out);
64 function parse_era($filename, $cb)
66 $delimiter1 = '~';
67 $delimiter2 = '|';
68 $delimiter3 = '^';
70 $infh = fopen($filename, 'r');
71 if (! $infh) return "ERA input file open failed";
73 $out = array();
74 $out['loopid'] = '';
75 $out['st_segment_count'] = 0;
76 $buffer = '';
77 $segid = '';
79 while (true) {
80 if (strlen($buffer) < 2048 && ! feof($infh)) $buffer .= fread($infh, 2048);
81 $tpos = strpos($buffer, $delimiter1);
82 if ($tpos === false) break;
83 $inline = substr($buffer, 0, $tpos);
84 $buffer = substr($buffer, $tpos + 1);
86 // If this is the ISA segment then figure out what the delimiters are.
87 if ($segid === '' && substr($inline, 0, 3) === 'ISA') {
88 $delimiter2 = substr($inline, 3, 1);
89 $delimiter3 = substr($inline, -1);
92 $seg = explode($delimiter2, $inline);
93 $segid = $seg[0];
95 if ($segid == 'ISA') {
96 if ($out['loopid']) return 'Unexpected ISA segment';
97 $out['isa_sender_id'] = trim($seg[6]);
98 $out['isa_receiver_id'] = trim($seg[8]);
99 $out['isa_control_number'] = trim($seg[13]);
100 // TBD: clear some stuff if we allow multiple transmission files.
102 else if ($segid == 'GS') {
103 if ($out['loopid']) return 'Unexpected GS segment';
104 $out['gs_date'] = trim($seg[4]);
105 $out['gs_time'] = trim($seg[5]);
106 $out['gs_control_number'] = trim($seg[6]);
108 else if ($segid == 'ST') {
109 parse_era_2100($out, $cb);
110 $out['loopid'] = '';
111 $out['st_control_number'] = trim($seg[2]);
112 $out['st_segment_count'] = 0;
114 else if ($segid == 'BPR') {
115 if ($out['loopid']) return 'Unexpected BPR segment';
116 $out['check_amount'] = trim($seg[2]);
117 $out['check_date'] = trim($seg[16]); // yyyymmdd
118 // TBD: BPR04 is a payment method code.
120 else if ($segid == 'TRN') {
121 if ($out['loopid']) return 'Unexpected TRN segment';
122 $out['check_number'] = trim($seg[2]);
123 $out['payer_tax_id'] = substr($seg[3], 1); // 9 digits
124 $out['payer_id'] = trim($seg[4]);
125 // Note: TRN04 further qualifies the paying entity within the
126 // organization identified by TRN03.
128 else if ($segid == 'REF' && $seg[1] == 'EV') {
129 if ($out['loopid']) return 'Unexpected REF|EV segment';
131 else if ($segid == 'CUR' && ! $out['loopid']) {
132 if ($seg[3] && $seg[3] != 1.0) {
133 return("We cannot handle foreign currencies!");
136 else if ($segid == 'REF' && ! $out['loopid']) {
137 // ignore
139 else if ($segid == 'DTM' && $seg[1] == '405') {
140 if ($out['loopid']) return 'Unexpected DTM|405 segment';
141 $out['production_date'] = trim($seg[2]); // yyyymmdd
144 // Loop 1000A is Payer Information.
146 else if ($segid == 'N1' && $seg[1] == 'PR') {
147 if ($out['loopid']) return 'Unexpected N1|PR segment';
148 $out['loopid'] = '1000A';
149 $out['payer_name'] = trim($seg[2]);
151 else if ($segid == 'N3' && $out['loopid'] == '1000A') {
152 $out['payer_street'] = trim($seg[1]);
153 // TBD: N302 may exist as an additional address line.
155 else if ($segid == 'N4' && $out['loopid'] == '1000A') {
156 $out['payer_city'] = trim($seg[1]);
157 $out['payer_state'] = trim($seg[2]);
158 $out['payer_zip'] = trim($seg[3]);
160 else if ($segid == 'REF' && $out['loopid'] == '1000A') {
161 // Other types of REFs may be given to identify the payer, but we
162 // ignore them.
164 else if ($segid == 'PER' && $out['loopid'] == '1000A') {
165 // TBD: Report payer contact information as a note.
168 // Loop 1000B is Payee Identification.
170 else if ($segid == 'N1' && $seg[1] == 'PE') {
171 if ($out['loopid'] != '1000A') return 'Unexpected N1|PE segment';
172 $out['loopid'] = '1000B';
173 $out['payee_name'] = trim($seg[2]);
174 $out['payee_tax_id'] = trim($seg[4]);
176 else if ($segid == 'N3' && $out['loopid'] == '1000B') {
177 $out['payee_street'] = trim($seg[1]);
179 else if ($segid == 'N4' && $out['loopid'] == '1000B') {
180 $out['payee_city'] = trim($seg[1]);
181 $out['payee_state'] = trim($seg[2]);
182 $out['payee_zip'] = trim($seg[3]);
184 else if ($segid == 'REF' && $out['loopid'] == '1000B') {
185 // Used to report additional ID numbers. Ignored.
188 // Loop 2000 provides for logical grouping of claim payment information.
189 // LX is required if any CLPs are present, but so far we do not care
190 // about loop 2000 content.
192 else if ($segid == 'LX') {
193 if (! $out['loopid']) return 'Unexpected LX segment';
194 parse_era_2100($out, $cb);
195 $out['loopid'] = '2000';
197 else if ($segid == 'TS2' && $out['loopid'] == '2000') {
198 // ignore
200 else if ($segid == 'TS3' && $out['loopid'] == '2000') {
201 // ignore
204 // Loop 2100 is Claim Payment Information. The good stuff begins here.
206 else if ($segid == 'CLP') {
207 if (! $out['loopid']) return 'Unexpected CLP segment';
208 parse_era_2100($out, $cb);
209 $out['loopid'] = '2100';
210 $out['warnings'] = '';
211 // Clear some stuff to start the new claim:
212 $out['subscriber_lname'] = '';
213 $out['subscriber_fname'] = '';
214 $out['subscriber_mname'] = '';
215 $out['subscriber_member_id'] = '';
216 $out['crossover']=0;
217 $out['svc'] = array();
219 // This is the poorly-named "Patient Account Number". For 837p
220 // it comes from CLM01 which we populated as pid-diagid-procid,
221 // where diagid and procid are id values from the billing table.
222 // For HCFA 1500 claims it comes from field 26 which we
223 // populated with our familiar pid-encounter billing key.
225 // The 835 spec calls this the "provider-assigned claim control
226 // number" and notes that it is specifically intended for
227 // identifying the claim in the provider's database.
228 $out['our_claim_id'] = trim($seg[1]);
230 $out['claim_status_code'] = trim($seg[2]);
231 $out['amount_charged'] = trim($seg[3]);
232 $out['amount_approved'] = trim($seg[4]);
233 $out['amount_patient'] = trim($seg[5]); // pt responsibility, copay + deductible
234 $out['payer_claim_id'] = trim($seg[7]); // payer's claim number
236 else if ($segid == 'CAS' && $out['loopid'] == '2100') {
237 // This is a claim-level adjustment and should be unusual.
238 // Handle it by creating a dummy zero-charge service item and
239 // then populating the adjustments into it. See also code in
240 // parse_era_2100() which will later plug in a payment reversal
241 // amount that offsets these adjustments.
242 $i = 0; // if present, the dummy service item will be first.
243 if (!$out['svc'][$i]) {
244 $out['svc'][$i] = array();
245 $out['svc'][$i]['code'] = 'Claim';
246 $out['svc'][$i]['mod'] = '';
247 $out['svc'][$i]['chg'] = '0';
248 $out['svc'][$i]['paid'] = '0';
249 $out['svc'][$i]['adj'] = array();
251 for ($k = 2; $k < 20; $k += 3) {
252 if (!$seg[$k]) break;
253 $j = count($out['svc'][$i]['adj']);
254 $out['svc'][$i]['adj'][$j] = array();
255 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
256 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
257 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
260 // QC = Patient
261 else if ($segid == 'NM1' && $seg[1] == 'QC' && $out['loopid'] == '2100') {
262 $out['patient_lname'] = trim($seg[3]);
263 $out['patient_fname'] = trim($seg[4]);
264 $out['patient_mname'] = trim($seg[5]);
265 $out['patient_member_id'] = trim($seg[9]);
267 // IL = Insured or Subscriber
268 else if ($segid == 'NM1' && $seg[1] == 'IL' && $out['loopid'] == '2100') {
269 $out['subscriber_lname'] = trim($seg[3]);
270 $out['subscriber_fname'] = trim($seg[4]);
271 $out['subscriber_mname'] = trim($seg[5]);
272 $out['subscriber_member_id'] = trim($seg[9]);
274 // 82 = Rendering Provider
275 else if ($segid == 'NM1' && $seg[1] == '82' && $out['loopid'] == '2100') {
276 $out['provider_lname'] = trim($seg[3]);
277 $out['provider_fname'] = trim($seg[4]);
278 $out['provider_mname'] = trim($seg[5]);
279 $out['provider_member_id'] = trim($seg[9]);
281 else if ($segid == 'NM1' && $seg[1] == 'TT' && $out['loopid'] == '2100') {
282 $out['crossover'] = 1;//Claim automatic forward case.
285 // 74 = Corrected Insured
286 // TT = Crossover Carrier (Transfer To another payer)
287 // PR = Corrected Payer
288 else if ($segid == 'NM1' && $out['loopid'] == '2100') {
289 // $out['warnings'] .= "NM1 segment at claim level ignored.\n";
291 else if ($segid == 'MOA' && $out['loopid'] == '2100') {
292 $out['warnings'] .= "MOA segment at claim level ignored.\n";
294 // REF segments may provide various identifying numbers, where REF02
295 // indicates the type of number.
296 else if ($segid == 'REF' && $seg[1] == '1W' && $out['loopid'] == '2100') {
297 $out['claim_comment'] = trim($seg[2]);
299 else if ($segid == 'REF' && $out['loopid'] == '2100') {
300 // ignore
302 else if ($segid == 'DTM' && $seg[1] == '050' && $out['loopid'] == '2100') {
303 $out['claim_date'] = trim($seg[2]); // yyyymmdd
305 // 036 = expiration date of coverage
306 // 050 = date claim received by payer
307 // 232 = claim statement period start
308 // 233 = claim statement period end
309 else if ($segid == 'DTM' && $out['loopid'] == '2100') {
310 // ignore?
312 else if ($segid == 'PER' && $out['loopid'] == '2100') {
314 $out['payer_insurance'] = trim($seg[2]);
315 $out['warnings'] .= 'Claim contact information: ' .
316 $seg[4] . "\n";
318 // For AMT01 see the Amount Qualifier Codes on pages 135-135 of the
319 // Implementation Guide. AMT is only good for comments and is not
320 // part of claim balancing.
321 else if ($segid == 'AMT' && $out['loopid'] == '2100') {
322 $out['warnings'] .= "AMT segment at claim level ignored.\n";
324 // For QTY01 see the Quantity Qualifier Codes on pages 137-138 of the
325 // Implementation Guide. QTY is only good for comments and is not
326 // part of claim balancing.
327 else if ($segid == 'QTY' && $out['loopid'] == '2100') {
328 $out['warnings'] .= "QTY segment at claim level ignored.\n";
331 // Loop 2110 is Service Payment Information.
333 else if ($segid == 'SVC') {
334 if (! $out['loopid']) return 'Unexpected SVC segment';
335 $out['loopid'] = '2110';
336 if ($seg[6]) {
337 // SVC06 if present is our original procedure code that they are changing.
338 // We will not put their crap in our invoice, but rather log a note and
339 // treat it as adjustments to our originally submitted coding.
340 $svc = explode($delimiter3, $seg[6]);
341 $tmp = explode($delimiter3, $seg[1]);
342 $out['warnings'] .= "Payer is restating our procedure " . $svc[1] .
343 " as " . $tmp[1] . ".\n";
344 } else {
345 $svc = explode($delimiter3, $seg[1]);
347 if ($svc[0] != 'HC') return 'SVC segment has unexpected qualifier';
348 // TBD: Other qualifiers are possible; see IG pages 140-141.
349 $i = count($out['svc']);
350 $out['svc'][$i] = array();
351 // It seems some payers append the modifier with no separator!
352 if (strlen($svc[1]) == 7 && empty($svc[2])) {
353 $out['svc'][$i]['code'] = substr($svc[1], 0, 5);
354 $out['svc'][$i]['mod'] = substr($svc[1], 5);
355 } else {
356 $out['svc'][$i]['code'] = $svc[1];
357 $out['svc'][$i]['mod'] = $svc[2] ? $svc[2] . ':' : '';
358 $out['svc'][$i]['mod'] .= $svc[3] ? $svc[3] . ':' : '';
359 $out['svc'][$i]['mod'] .= $svc[4] ? $svc[4] . ':' : '';
360 $out['svc'][$i]['mod'] .= $svc[5] ? $svc[5] . ':' : '';
361 $out['svc'][$i]['mod'] = preg_replace('/:$/','',$out['svc'][$i]['mod']);
363 $out['svc'][$i]['chg'] = $seg[2];
364 $out['svc'][$i]['paid'] = $seg[3];
365 $out['svc'][$i]['adj'] = array();
366 // Note: SVC05, if present, indicates the paid units of service.
367 // It defaults to 1.
369 // DTM01 identifies the type of service date:
370 // 472 = a single date of service
371 // 150 = service period start
372 // 151 = service period end
373 else if ($segid == 'DTM' && $out['loopid'] == '2110') {
374 $out['dos'] = trim($seg[2]); // yyyymmdd
376 else if ($segid == 'CAS' && $out['loopid'] == '2110') {
377 $i = count($out['svc']) - 1;
378 for ($k = 2; $k < 20; $k += 3) {
379 if (!$seg[$k]) break;
380 if ($seg[1] == 'CO' && $seg[$k+1] < 0) {
381 $out['warnings'] .= "Negative Contractual Obligation adjustment " .
382 "seems wrong. Inverting, but should be checked!\n";
383 $seg[$k+1] = 0 - $seg[$k+1];
385 $j = count($out['svc'][$i]['adj']);
386 $out['svc'][$i]['adj'][$j] = array();
387 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
388 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
389 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
390 // Note: $seg[$k+2] is "quantity". A value here indicates a change to
391 // the number of units of service. We're ignoring that for now.
394 else if ($segid == 'REF' && $out['loopid'] == '2110') {
395 // ignore
397 else if ($segid == 'AMT' && $seg[1] == 'B6' && $out['loopid'] == '2110') {
398 $i = count($out['svc']) - 1;
399 $out['svc'][$i]['allowed'] = $seg[2]; // report this amount as a note
401 else if ($segid == 'AMT' && $out['loopid'] == '2110') {
402 $out['warnings'] .= "$inline at service level ignored.\n";
404 else if ($segid == 'LQ' && $seg[1] == 'HE' && $out['loopid'] == '2110') {
405 $i = count($out['svc']) - 1;
406 $out['svc'][$i]['remark'] = $seg[2];
408 else if ($segid == 'QTY' && $out['loopid'] == '2110') {
409 $out['warnings'] .= "QTY segment at service level ignored.\n";
411 else if ($segid == 'PLB') {
412 // Provider-level adjustments are a General Ledger thing and should not
413 // alter the A/R for the claim, so we just report them as notes.
414 for ($k = 3; $k < 15; $k += 2) {
415 if (!$seg[$k]) break;
416 $out['warnings'] .= 'PROVIDER LEVEL ADJUSTMENT (not claim-specific): $' .
417 sprintf('%.2f', $seg[$k+1]) . " with reason code " . $seg[$k] . "\n";
418 // Note: For PLB adjustment reason codes see IG pages 165-170.
421 else if ($segid == 'SE') {
422 parse_era_2100($out, $cb);
423 $out['loopid'] = '';
424 if ($out['st_control_number'] != trim($seg[2])) {
425 return 'Ending transaction set control number mismatch';
427 if (($out['st_segment_count'] + 1) != trim($seg[1])) {
428 return 'Ending transaction set segment count mismatch';
431 else if ($segid == 'GE') {
432 if ($out['loopid']) return 'Unexpected GE segment';
433 if ($out['gs_control_number'] != trim($seg[2])) {
434 return 'Ending functional group control number mismatch';
437 else if ($segid == 'IEA') {
438 if ($out['loopid']) return 'Unexpected IEA segment';
439 if ($out['isa_control_number'] != trim($seg[2])) {
440 return 'Ending interchange control number mismatch';
443 else {
444 return "Unknown or unexpected segment ID $segid";
447 ++$out['st_segment_count'];
450 if ($segid != 'IEA') return 'Premature end of ERA file';
451 return '';
453 //for getting the check details and provider details
454 function parse_era_for_check($filename)
456 $delimiter1 = '~';
457 $delimiter2 = '|';
458 $delimiter3 = '^';
460 $infh = fopen($filename, 'r');
461 if (! $infh) return "ERA input file open failed";
463 $out = array();
464 $out['loopid'] = '';
465 $out['st_segment_count'] = 0;
466 $buffer = '';
467 $segid = '';
468 $check_count=0;
469 while (true) {
471 if (strlen($buffer) < 2048 && ! feof($infh)) $buffer .= fread($infh, 2048);
472 $tpos = strpos($buffer, $delimiter1);
473 if ($tpos === false) break;
474 $inline = substr($buffer, 0, $tpos);
475 $buffer = substr($buffer, $tpos + 1);
477 // If this is the ISA segment then figure out what the delimiters are.
478 if ($segid === '' && substr($inline, 0, 3) === 'ISA') {
479 $delimiter2 = substr($inline, 3, 1);
480 $delimiter3 = substr($inline, -1);
483 $seg = explode($delimiter2, $inline);
484 $segid = $seg[0];
486 if ($segid == 'ISA') {
489 else if ($segid == 'BPR') {
490 ++$check_count;
491 //if ($out['loopid']) return 'Unexpected BPR segment';
492 $out['check_amount'.$check_count] = trim($seg[2]);
493 $out['check_date'.$check_count] = trim($seg[16]); // yyyymmdd
494 // TBD: BPR04 is a payment method code.
497 else if ($segid == 'N1' && $seg[1] == 'PE') {
498 //if ($out['loopid'] != '1000A') return 'Unexpected N1|PE segment';
499 $out['loopid'] = '1000B';
500 $out['payee_name'.$check_count] = trim($seg[2]);
501 $out['payee_tax_id'.$check_count] = trim($seg[4]);
504 else if ($segid == 'TRN') {
505 //if ($out['loopid']) return 'Unexpected TRN segment';
506 $out['check_number'.$check_count] = trim($seg[2]);
507 $out['payer_tax_id'.$check_count] = substr($seg[3], 1); // 9 digits
508 $out['payer_id'.$check_count] = trim($seg[4]);
509 // Note: TRN04 further qualifies the paying entity within the
510 // organization identified by TRN03.
516 ++$out['st_segment_count'];
518 $out['check_count']=$check_count;
519 era_callback_check($out);
521 if ($segid != 'IEA') return 'Premature end of ERA file';
522 return '';