hopefully fixed problem with quote characters ' " breaking form or appearing with...
[openemr.git] / library / parse_era.inc.php
blobf4aaaeda3881d408d82974f6fc770899de5afca6
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) {
10 if ($out['loopid'] == '2110' || $out['loopid'] == '2100') {
12 // Production date is posted with adjustments, so make sure it exists.
13 if (!$out['production_date']) $out['production_date'] = $out['check_date'];
15 // Force the sum of service payments to equal the claim payment
16 // amount, and the sum of service adjustments to equal the CLP's
17 // (charged amount - paid amount - patient responsibility amount).
18 // This may result from claim-level adjustments, and in this case the
19 // first SVC item that we stored was a 'Claim' type. It also may result
20 // from poorly reported payment reversals, in which case we may need to
21 // create the 'Claim' service type here.
23 $paytotal = $out['amount_approved'];
24 $adjtotal = $out['amount_charged'] - $out['amount_approved'] - $out['amount_patient'];
25 foreach ($out['svc'] as $svc) {
26 $paytotal -= $svc['paid'];
27 foreach ($svc['adj'] as $adj) {
28 if ($adj['group_code'] != 'PR') $adjtotal -= $adj['amount'];
31 $paytotal = round($paytotal, 2);
32 $adjtotal = round($adjtotal, 2);
33 if ($paytotal != 0 || $adjtotal != 0) {
34 if ($out['svc'][0]['code'] != 'Claim') {
35 array_unshift($out['svc'], array());
36 $out['svc'][0]['code'] = 'Claim';
37 $out['svc'][0]['mod'] = '';
38 $out['svc'][0]['chg'] = '0';
39 $out['svc'][0]['paid'] = '0';
40 $out['svc'][0]['adj'] = array();
41 $out['warnings'] .= "Procedure 'Claim' is inserted artificially to " .
42 "force claim balancing.\n";
44 $out['svc'][0]['paid'] += $paytotal;
45 if ($adjtotal) {
46 $j = count($out['svc'][0]['adj']);
47 $out['svc'][0]['adj'][$j] = array();
48 $out['svc'][0]['adj'][$j]['group_code'] = 'CR'; // presuming a correction or reversal
49 $out['svc'][0]['adj'][$j]['reason_code'] = 'Balancing';
50 $out['svc'][0]['adj'][$j]['amount'] = $adjtotal;
52 // if ($out['svc'][0]['code'] != 'Claim') {
53 // $out['warnings'] .= "First service item payment amount " .
54 // "adjusted by $paytotal due to payment imbalance. " .
55 // "This should not happen!\n";
56 // }
59 $cb($out);
63 function parse_era($filename, $cb) {
64 $infh = fopen($filename, 'r');
65 if (! $infh) return "ERA input file open failed";
67 $out = array();
68 $out['loopid'] = '';
69 $out['st_segment_count'] = 0;
70 $buffer = '';
71 $segid = '';
73 while (true) {
74 if (strlen($buffer) < 2048 && ! feof($infh))
75 $buffer .= fread($infh, 2048);
76 $tpos = strpos($buffer, '~');
77 if ($tpos === false) break;
78 $inline = substr($buffer, 0, $tpos);
79 $buffer = substr($buffer, $tpos + 1);
81 $seg = explode('|', $inline);
82 $segid = $seg[0];
84 if ($segid == 'ISA') {
85 if ($out['loopid']) return 'Unexpected ISA segment';
86 $out['isa_sender_id'] = trim($seg[6]);
87 $out['isa_receiver_id'] = trim($seg[8]);
88 $out['isa_control_number'] = trim($seg[13]);
89 // TBD: clear some stuff if we allow multiple transmission files.
91 else if ($segid == 'GS') {
92 if ($out['loopid']) return 'Unexpected GS segment';
93 $out['gs_date'] = trim($seg[4]);
94 $out['gs_time'] = trim($seg[5]);
95 $out['gs_control_number'] = trim($seg[6]);
97 else if ($segid == 'ST') {
98 parse_era_2100($out, $cb);
99 $out['loopid'] = '';
100 $out['st_control_number'] = trim($seg[2]);
101 $out['st_segment_count'] = 0;
103 else if ($segid == 'BPR') {
104 if ($out['loopid']) return 'Unexpected BPR segment';
105 $out['check_amount'] = trim($seg[2]);
106 $out['check_date'] = trim($seg[16]); // yyyymmdd
107 // TBD: BPR04 is a payment method code.
109 else if ($segid == 'TRN') {
110 if ($out['loopid']) return 'Unexpected TRN segment';
111 $out['check_number'] = trim($seg[2]);
112 $out['payer_tax_id'] = substr($seg[3], 1); // 9 digits
113 $out['payer_id'] = trim($seg[4]);
114 // Note: TRN04 further qualifies the paying entity within the
115 // organization identified by TRN03.
117 else if ($segid == 'REF' && $seg[1] == 'EV') {
118 if ($out['loopid']) return 'Unexpected REF|EV segment';
120 else if ($segid == 'CUR' && ! $out['loopid']) {
121 if ($seg[3] && $seg[3] != 1.0) {
122 return("We cannot handle foreign currencies!");
125 else if ($segid == 'REF' && ! $out['loopid']) {
126 // ignore
128 else if ($segid == 'DTM' && $seg[1] == '405') {
129 if ($out['loopid']) return 'Unexpected DTM|405 segment';
130 $out['production_date'] = trim($seg[2]); // yyyymmdd
133 // Loop 1000A is Payer Information.
135 else if ($segid == 'N1' && $seg[1] == 'PR') {
136 if ($out['loopid']) return 'Unexpected N1|PR segment';
137 $out['loopid'] = '1000A';
138 $out['payer_name'] = trim($seg[2]);
140 else if ($segid == 'N3' && $out['loopid'] == '1000A') {
141 $out['payer_street'] = trim($seg[1]);
142 // TBD: N302 may exist as an additional address line.
144 else if ($segid == 'N4' && $out['loopid'] == '1000A') {
145 $out['payer_city'] = trim($seg[1]);
146 $out['payer_state'] = trim($seg[2]);
147 $out['payer_zip'] = trim($seg[3]);
149 else if ($segid == 'REF' && $out['loopid'] == '1000A') {
150 // Other types of REFs may be given to identify the payer, but we
151 // ignore them.
153 else if ($segid == 'PER' && $out['loopid'] == '1000A') {
154 // TBD: Report payer contact information as a note.
157 // Loop 1000B is Payee Identification.
159 else if ($segid == 'N1' && $seg[1] == 'PE') {
160 if ($out['loopid'] != '1000A') return 'Unexpected N1|PE segment';
161 $out['loopid'] = '1000B';
162 $out['payee_name'] = trim($seg[2]);
163 $out['payee_tax_id'] = trim($seg[4]);
165 else if ($segid == 'N3' && $out['loopid'] == '1000B') {
166 $out['payee_street'] = trim($seg[1]);
168 else if ($segid == 'N4' && $out['loopid'] == '1000B') {
169 $out['payee_city'] = trim($seg[1]);
170 $out['payee_state'] = trim($seg[2]);
171 $out['payee_zip'] = trim($seg[3]);
173 else if ($segid == 'REF' && $out['loopid'] == '1000B') {
174 // Used to report additional ID numbers. Ignored.
177 // Loop 2000 provides for logical grouping of claim payment information.
178 // LX is required if any CLPs are present, but so far we do not care
179 // about loop 2000 content.
181 else if ($segid == 'LX') {
182 if (! $out['loopid']) return 'Unexpected LX segment';
183 parse_era_2100($out, $cb);
184 $out['loopid'] = '2000';
186 else if ($segid == 'TS2' && $out['loopid'] == '2000') {
187 // ignore
189 else if ($segid == 'TS3' && $out['loopid'] == '2000') {
190 // ignore
193 // Loop 2100 is Claim Payment Information. The good stuff begins here.
195 else if ($segid == 'CLP') {
196 if (! $out['loopid']) return 'Unexpected CLP segment';
197 parse_era_2100($out, $cb);
198 $out['loopid'] = '2100';
199 $out['warnings'] = '';
200 // Clear some stuff to start the new claim:
201 $out['subscriber_lname'] = '';
202 $out['subscriber_fname'] = '';
203 $out['subscriber_mname'] = '';
204 $out['subscriber_member_id'] = '';
205 $out['svc'] = array();
207 // This is the poorly-named "Patient Account Number". For 837p
208 // it comes from CLM01 which we populated as pid-diagid-procid,
209 // where diagid and procid are id values from the billing table.
210 // For HCFA 1500 claims it comes from field 26 which we
211 // populated with our familiar pid-encounter billing key.
213 // The 835 spec calls this the "provider-assigned claim control
214 // number" and notes that it is specifically intended for
215 // identifying the claim in the provider's database.
216 $out['our_claim_id'] = trim($seg[1]);
218 $out['claim_status_code'] = trim($seg[2]);
219 $out['amount_charged'] = trim($seg[3]);
220 $out['amount_approved'] = trim($seg[4]);
221 $out['amount_patient'] = trim($seg[5]); // pt responsibility, copay + deductible
222 $out['payer_claim_id'] = trim($seg[7]); // payer's claim number
224 else if ($segid == 'CAS' && $out['loopid'] == '2100') {
225 // This is a claim-level adjustment and should be unusual.
226 // Handle it by creating a dummy zero-charge service item and
227 // then populating the adjustments into it. See also code in
228 // parse_era_2100() which will later plug in a payment reversal
229 // amount that offsets these adjustments.
230 $i = 0; // if present, the dummy service item will be first.
231 if (!$out['svc'][$i]) {
232 $out['svc'][$i] = array();
233 $out['svc'][$i]['code'] = 'Claim';
234 $out['svc'][$i]['mod'] = '';
235 $out['svc'][$i]['chg'] = '0';
236 $out['svc'][$i]['paid'] = '0';
237 $out['svc'][$i]['adj'] = array();
239 for ($k = 2; $k < 20; $k += 3) {
240 if (!$seg[$k]) break;
241 $j = count($out['svc'][$i]['adj']);
242 $out['svc'][$i]['adj'][$j] = array();
243 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
244 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
245 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
248 // QC = Patient
249 else if ($segid == 'NM1' && $seg[1] == 'QC' && $out['loopid'] == '2100') {
250 $out['patient_lname'] = trim($seg[3]);
251 $out['patient_fname'] = trim($seg[4]);
252 $out['patient_mname'] = trim($seg[5]);
253 $out['patient_member_id'] = trim($seg[9]);
255 // IL = Insured or Subscriber
256 else if ($segid == 'NM1' && $seg[1] == 'IL' && $out['loopid'] == '2100') {
257 $out['subscriber_lname'] = trim($seg[3]);
258 $out['subscriber_fname'] = trim($seg[4]);
259 $out['subscriber_mname'] = trim($seg[5]);
260 $out['subscriber_member_id'] = trim($seg[9]);
262 // 82 = Rendering Provider
263 else if ($segid == 'NM1' && $seg[1] == '82' && $out['loopid'] == '2100') {
264 $out['provider_lname'] = trim($seg[3]);
265 $out['provider_fname'] = trim($seg[4]);
266 $out['provider_mname'] = trim($seg[5]);
267 $out['provider_member_id'] = trim($seg[9]);
269 // 74 = Corrected Insured
270 // TT = Crossover Carrier (Transfer To another payer)
271 // PR = Corrected Payer
272 else if ($segid == 'NM1' && $out['loopid'] == '2100') {
273 // $out['warnings'] .= "NM1 segment at claim level ignored.\n";
275 else if ($segid == 'MOA' && $out['loopid'] == '2100') {
276 $out['warnings'] .= "MOA segment at claim level ignored.\n";
278 // REF segments may provide various identifying numbers, where REF02
279 // indicates the type of number.
280 else if ($segid == 'REF' && $seg[1] == '1W' && $out['loopid'] == '2100') {
281 $out['claim_comment'] = trim($seg[2]);
283 else if ($segid == 'REF' && $out['loopid'] == '2100') {
284 // ignore
286 else if ($segid == 'DTM' && $seg[1] == '050' && $out['loopid'] == '2100') {
287 $out['claim_date'] = trim($seg[2]); // yyyymmdd
289 // 036 = expiration date of coverage
290 // 050 = date claim received by payer
291 // 232 = claim statement period start
292 // 233 = claim statement period end
293 else if ($segid == 'DTM' && $out['loopid'] == '2100') {
294 // ignore?
296 else if ($segid == 'PER' && $out['loopid'] == '2100') {
297 $out['warnings'] .= 'Claim contact information: ' .
298 $seg[4] . "\n";
300 // For AMT01 see the Amount Qualifier Codes on pages 135-135 of the
301 // Implementation Guide. AMT is only good for comments and is not
302 // part of claim balancing.
303 else if ($segid == 'AMT' && $out['loopid'] == '2100') {
304 $out['warnings'] .= "AMT segment at claim level ignored.\n";
306 // For QTY01 see the Quantity Qualifier Codes on pages 137-138 of the
307 // Implementation Guide. QTY is only good for comments and is not
308 // part of claim balancing.
309 else if ($segid == 'QTY' && $out['loopid'] == '2100') {
310 $out['warnings'] .= "QTY segment at claim level ignored.\n";
313 // Loop 2110 is Service Payment Information.
315 else if ($segid == 'SVC') {
316 if (! $out['loopid']) return 'Unexpected SVC segment';
317 $out['loopid'] = '2110';
318 if ($seg[6]) {
319 // SVC06 if present is our original procedure code that they are changing.
320 // We will not put their crap in our invoice, but rather log a note and
321 // treat it as adjustments to our originally submitted coding.
322 $svc = explode('^', $seg[6]);
323 $tmp = explode('^', $seg[1]);
324 $out['warnings'] .= "Payer is restating our procedure " . $svc[1] .
325 " as " . $tmp[1] . ".\n";
326 } else {
327 $svc = explode('^', $seg[1]);
329 if ($svc[0] != 'HC') return 'SVC segment has unexpected qualifier';
330 // TBD: Other qualifiers are possible; see IG pages 140-141.
331 $i = count($out['svc']);
332 $out['svc'][$i] = array();
333 // It seems some payers append the modifier with no separator!
334 if (strlen($svc[1]) == 7 && empty($svc[2])) {
335 $out['svc'][$i]['code'] = substr($svc[1], 0, 5);
336 $out['svc'][$i]['mod'] = substr($svc[1], 5);
337 } else {
338 $out['svc'][$i]['code'] = $svc[1];
339 $out['svc'][$i]['mod'] = $svc[2] ? $svc[2] : '';
341 // TBD: There may be up to 4 procedure modifiers in $svc[2] thru [5].
342 $out['svc'][$i]['chg'] = $seg[2];
343 $out['svc'][$i]['paid'] = $seg[3];
344 $out['svc'][$i]['adj'] = array();
345 // Note: SVC05, if present, indicates the paid units of service.
346 // It defaults to 1.
348 // DTM01 identifies the type of service date:
349 // 472 = a single date of service
350 // 150 = service period start
351 // 151 = service period end
352 else if ($segid == 'DTM' && $out['loopid'] == '2110') {
353 $out['dos'] = trim($seg[2]); // yyyymmdd
355 else if ($segid == 'CAS' && $out['loopid'] == '2110') {
356 $i = count($out['svc']) - 1;
357 for ($k = 2; $k < 20; $k += 3) {
358 if (!$seg[$k]) break;
359 $j = count($out['svc'][$i]['adj']);
360 $out['svc'][$i]['adj'][$j] = array();
361 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
362 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
363 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
364 // Note: $seg[$k+2] is "quantity". A value here indicates a change to
365 // the number of units of service. We're ignoring that for now.
368 else if ($segid == 'REF' && $out['loopid'] == '2110') {
369 // ignore
371 else if ($segid == 'AMT' && $seg[1] == 'B6' && $out['loopid'] == '2110') {
372 $i = count($out['svc']) - 1;
373 $out['svc'][$i]['allowed'] = $seg[2]; // report this amount as a note
375 else if ($segid == 'LQ' && $seg[1] == 'HE' && $out['loopid'] == '2110') {
376 $i = count($out['svc']) - 1;
377 $out['svc'][$i]['remark'] = $seg[2];
379 else if ($segid == 'QTY' && $out['loopid'] == '2110') {
380 $out['warnings'] .= "QTY segment at service level ignored.\n";
382 else if ($segid == 'PLB') {
383 // Provider-level adjustments are a General Ledger thing and should not
384 // alter the A/R for the claim, so we just report them as notes.
385 for ($k = 3; $k < 15; $k += 2) {
386 if (!$seg[$k]) break;
387 $out['warnings'] .= 'PROVIDER LEVEL ADJUSTMENT (not claim-specific): $' .
388 sprintf('%.2f', $seg[$k+1]) . " with reason code " . $seg[$k] . "\n";
389 // Note: For PLB adjustment reason codes see IG pages 165-170.
392 else if ($segid == 'SE') {
393 parse_era_2100($out, $cb);
394 $out['loopid'] = '';
395 if ($out['st_control_number'] != trim($seg[2])) {
396 return 'Ending transaction set control number mismatch';
398 if (($out['st_segment_count'] + 1) != trim($seg[1])) {
399 return 'Ending transaction set segment count mismatch';
402 else if ($segid == 'GE') {
403 if ($out['loopid']) return 'Unexpected GE segment';
404 if ($out['gs_control_number'] != trim($seg[2])) {
405 return 'Ending functional group control number mismatch';
408 else if ($segid == 'IEA') {
409 if ($out['loopid']) return 'Unexpected IEA segment';
410 if ($out['isa_control_number'] != trim($seg[2])) {
411 return 'Ending interchange control number mismatch';
414 else {
415 return "Unknown or unexpected segment ID $segid";
418 ++$out['st_segment_count'];
421 if ($segid != 'IEA') return 'Premature end of ERA file';
422 return '';