yet more changes for new frame layout
[openemr.git] / library / parse_era.inc.php
blob6a2e6b45994aa73df8b8044b570321256fc41fb3
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') {
12 // Force the sum of service payments to equal the claim payment
13 // amount. Whenever this is an issue it should result from
14 // claim-level adjustments, and in this case the first SVC item
15 // that we stored was a 'Claim' type.
16 $paytotal = $out['amount_approved'];
17 foreach ($out['svc'] as $svc) $paytotal -= $svc['paid'];
18 $paytotal = round($paytotal, 2);
19 if ($paytotal != 0) {
20 $out['svc'][0]['paid'] += $paytotal;
21 if ($out['svc'][0]['code'] != 'Claim') {
22 $out['warnings'] .= "First service item payment amount " .
23 "adjusted by $paytotal due to payment imbalance. " .
24 "This should not happen!\n";
28 $cb($out);
32 function parse_era($filename, $cb) {
33 $infh = fopen($filename, 'r');
34 if (! $infh) return "ERA input file open failed";
36 $out = array();
37 $out['loopid'] = '';
38 $out['st_segment_count'] = 0;
39 $buffer = '';
40 $segid = '';
42 while (true) {
43 if (strlen($buffer) < 2048 && ! feof($infh))
44 $buffer .= fread($infh, 2048);
45 $tpos = strpos($buffer, '~');
46 if ($tpos === false) break;
47 $inline = substr($buffer, 0, $tpos);
48 $buffer = substr($buffer, $tpos + 1);
50 $seg = explode('|', $inline);
51 $segid = $seg[0];
53 if ($segid == 'ISA') {
54 if ($out['loopid']) return 'Unexpected ISA segment';
55 $out['isa_sender_id'] = trim($seg[6]);
56 $out['isa_receiver_id'] = trim($seg[8]);
57 $out['isa_control_number'] = trim($seg[13]);
58 // TBD: clear some stuff if we allow multiple transmission files.
60 else if ($segid == 'GS') {
61 if ($out['loopid']) return 'Unexpected GS segment';
62 $out['gs_date'] = trim($seg[4]);
63 $out['gs_time'] = trim($seg[5]);
64 $out['gs_control_number'] = trim($seg[6]);
66 else if ($segid == 'ST') {
67 parse_era_2100($out, $cb);
68 $out['loopid'] = '';
69 $out['st_control_number'] = trim($seg[2]);
70 $out['st_segment_count'] = 0;
72 else if ($segid == 'BPR') {
73 if ($out['loopid']) return 'Unexpected BPR segment';
74 $out['check_amount'] = trim($seg[2]);
75 $out['check_date'] = trim($seg[16]); // yyyymmdd
76 // TBD: BPR04 is a payment method code.
78 else if ($segid == 'TRN') {
79 if ($out['loopid']) return 'Unexpected TRN segment';
80 $out['check_number'] = trim($seg[2]);
81 $out['payer_tax_id'] = substr($seg[3], 1); // 9 digits
82 $out['payer_id'] = trim($seg[4]);
83 // Note: TRN04 further qualifies the paying entity within the
84 // organization identified by TRN03.
86 else if ($segid == 'REF' && $seg[1] == 'EV') {
87 if ($out['loopid']) return 'Unexpected REF|EV segment';
89 else if ($segid == 'CUR' && ! $out['loopid']) {
90 if ($seg[3] && $seg[3] != 1.0) {
91 return("We cannot handle foreign currencies!");
94 else if ($segid == 'REF' && ! $out['loopid']) {
95 // ignore
97 else if ($segid == 'DTM' && $seg[1] == '405') {
98 if ($out['loopid']) return 'Unexpected DTM|405 segment';
99 $out['production_date'] = trim($seg[2]); // yyyymmdd
102 // Loop 1000A is Payer Information.
104 else if ($segid == 'N1' && $seg[1] == 'PR') {
105 if ($out['loopid']) return 'Unexpected N1|PR segment';
106 $out['loopid'] = '1000A';
107 $out['payer_name'] = trim($seg[2]);
109 else if ($segid == 'N3' && $out['loopid'] == '1000A') {
110 $out['payer_street'] = trim($seg[1]);
111 // TBD: N302 may exist as an additional address line.
113 else if ($segid == 'N4' && $out['loopid'] == '1000A') {
114 $out['payer_city'] = trim($seg[1]);
115 $out['payer_state'] = trim($seg[2]);
116 $out['payer_zip'] = trim($seg[3]);
118 else if ($segid == 'REF' && $out['loopid'] == '1000A') {
119 // Other types of REFs may be given to identify the payer, but we
120 // ignore them.
122 else if ($segid == 'PER' && $out['loopid'] == '1000A') {
123 // TBD: Report payer contact information as a note.
126 // Loop 1000B is Payee Identification.
128 else if ($segid == 'N1' && $seg[1] == 'PE') {
129 if ($out['loopid'] != '1000A') return 'Unexpected N1|PE segment';
130 $out['loopid'] = '1000B';
131 $out['payee_name'] = trim($seg[2]);
132 $out['payee_tax_id'] = trim($seg[4]);
134 else if ($segid == 'N3' && $out['loopid'] == '1000B') {
135 $out['payee_street'] = trim($seg[1]);
137 else if ($segid == 'N4' && $out['loopid'] == '1000B') {
138 $out['payee_city'] = trim($seg[1]);
139 $out['payee_state'] = trim($seg[2]);
140 $out['payee_zip'] = trim($seg[3]);
142 else if ($segid == 'REF' && $out['loopid'] == '1000B') {
143 // Used to report additional ID numbers. Ignored.
146 // Loop 2000 provides for logical grouping of claim payment information.
147 // LX is required if any CLPs are present, but so far we do not care
148 // about loop 2000 content.
150 else if ($segid == 'LX') {
151 if (! $out['loopid']) return 'Unexpected LX segment';
152 parse_era_2100($out, $cb);
153 $out['loopid'] = '2000';
155 else if ($segid == 'TS2' && $out['loopid'] == '2000') {
156 // ignore
158 else if ($segid == 'TS3' && $out['loopid'] == '2000') {
159 // ignore
162 // Loop 2100 is Claim Payment Information. The good stuff begins here.
164 else if ($segid == 'CLP') {
165 if (! $out['loopid']) return 'Unexpected CLP segment';
166 parse_era_2100($out, $cb);
167 $out['loopid'] = '2100';
168 $out['warnings'] = '';
169 // Clear some stuff to start the new claim:
170 $out['subscriber_lname'] = '';
171 $out['subscriber_fname'] = '';
172 $out['subscriber_mname'] = '';
173 $out['subscriber_member_id'] = '';
174 $out['svc'] = array();
176 // This is the poorly-named "Patient Account Number". For 837p
177 // it comes from CLM01 which we populated as pid-diagid-procid,
178 // where diagid and procid are id values from the billing table.
179 // For HCFA 1500 claims it comes from field 26 which we
180 // populated with our familiar pid-encounter billing key.
182 // The 835 spec calls this the "provider-assigned claim control
183 // number" and notes that it is specifically intended for
184 // identifying the claim in the provider's database.
185 $out['our_claim_id'] = trim($seg[1]);
187 $out['claim_status_code'] = trim($seg[2]);
188 $out['amount_charged'] = trim($seg[3]);
189 $out['amount_approved'] = trim($seg[4]);
190 $out['amount_patient'] = trim($seg[5]); // pt responsibility, copay + deductible
191 $out['payer_claim_id'] = trim($seg[7]); // payer's claim number
193 else if ($segid == 'CAS' && $out['loopid'] == '2100') {
194 // This is a claim-level adjustment and should be unusual.
195 // Handle it by creating a dummy zero-charge service item and
196 // then populating the adjustments into it. See also code in
197 // parse_era_2100() which will later plug in a payment reversal
198 // amount that offsets these adjustments.
199 $i = 0; // if present, the dummy service item will be first.
200 if (!$out['svc'][$i]) {
201 $out['svc'][$i] = array();
202 $out['svc'][$i]['code'] = 'Claim';
203 $out['svc'][$i]['mod'] = '';
204 $out['svc'][$i]['chg'] = '0';
205 $out['svc'][$i]['paid'] = '0';
206 $out['svc'][$i]['adj'] = array();
208 for ($k = 2; $k < 20; $k += 3) {
209 if (!$seg[$k]) break;
210 $j = count($out['svc'][$i]['adj']);
211 $out['svc'][$i]['adj'][$j] = array();
212 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
213 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
214 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
217 // QC = Patient
218 else if ($segid == 'NM1' && $seg[1] == 'QC' && $out['loopid'] == '2100') {
219 $out['patient_lname'] = trim($seg[3]);
220 $out['patient_fname'] = trim($seg[4]);
221 $out['patient_mname'] = trim($seg[5]);
222 $out['patient_member_id'] = trim($seg[9]);
224 // IL = Insured or Subscriber
225 else if ($segid == 'NM1' && $seg[1] == 'IL' && $out['loopid'] == '2100') {
226 $out['subscriber_lname'] = trim($seg[3]);
227 $out['subscriber_fname'] = trim($seg[4]);
228 $out['subscriber_mname'] = trim($seg[5]);
229 $out['subscriber_member_id'] = trim($seg[9]);
231 // 82 = Rendering Provider
232 else if ($segid == 'NM1' && $seg[1] == '82' && $out['loopid'] == '2100') {
233 $out['provider_lname'] = trim($seg[3]);
234 $out['provider_fname'] = trim($seg[4]);
235 $out['provider_mname'] = trim($seg[5]);
236 $out['provider_member_id'] = trim($seg[9]);
238 // 74 = Corrected Insured
239 // TT = Crossover Carrier (Transfer To another payer)
240 // PR = Corrected Payer
241 else if ($segid == 'NM1' && $out['loopid'] == '2100') {
242 // $out['warnings'] .= "NM1 segment at claim level ignored.\n";
244 else if ($segid == 'MOA' && $out['loopid'] == '2100') {
245 $out['warnings'] .= "MOA segment at claim level ignored.\n";
247 // REF segments may provide various identifying numbers, where REF02
248 // indicates the type of number.
249 else if ($segid == 'REF' && $seg[1] == '1W' && $out['loopid'] == '2100') {
250 $out['claim_comment'] = trim($seg[2]);
252 else if ($segid == 'REF' && $out['loopid'] == '2100') {
253 // ignore
255 else if ($segid == 'DTM' && $seg[1] == '050' && $out['loopid'] == '2100') {
256 $out['claim_date'] = trim($seg[2]); // yyyymmdd
258 // 036 = expiration date of coverage
259 // 050 = date claim received by payer
260 // 232 = claim statement period start
261 // 233 = claim statement period end
262 else if ($segid == 'DTM' && $out['loopid'] == '2100') {
263 // ignore?
265 else if ($segid == 'PER' && $out['loopid'] == '2100') {
266 $out['warnings'] .= 'Claim contact information: ' .
267 $seg[4] . "\n";
269 // For AMT01 see the Amount Qualifier Codes on pages 135-135 of the
270 // Implementation Guide. AMT is only good for comments and is not
271 // part of claim balancing.
272 else if ($segid == 'AMT' && $out['loopid'] == '2100') {
273 $out['warnings'] .= "AMT segment at claim level ignored.\n";
275 // For QTY01 see the Quantity Qualifier Codes on pages 137-138 of the
276 // Implementation Guide. QTY is only good for comments and is not
277 // part of claim balancing.
278 else if ($segid == 'QTY' && $out['loopid'] == '2100') {
279 $out['warnings'] .= "QTY segment at claim level ignored.\n";
282 // Loop 2110 is Service Payment Information.
284 else if ($segid == 'SVC') {
285 if (! $out['loopid']) return 'Unexpected SVC segment';
286 $out['loopid'] = '2110';
287 $svc = explode('^', $seg[1]);
288 if ($svc[0] != 'HC') return 'SVC segment has unexpected qualifier';
289 // TBD: Other qualifiers are possible; see IG pages 140-141.
290 $i = count($out['svc']);
291 $out['svc'][$i] = array();
292 $out['svc'][$i]['code'] = $svc[1];
293 $out['svc'][$i]['mod'] = $svc[2] ? $svc[2] : '';
294 // TBD: There may be up to 4 procedure modifiers in $svc[2] thru [5].
295 $out['svc'][$i]['chg'] = $seg[2];
296 $out['svc'][$i]['paid'] = $seg[3];
297 $out['svc'][$i]['adj'] = array();
298 // Note: SVC05, if present, indicates the paid units of service.
299 // It defaults to 1.
300 // Note: In the case of bundling, SVC06 reports the original procedure
301 // code, there are adjustments of the old procedure codes that zero out
302 // the original charge amounts, a negative adjustment of the new
303 // procedure code(s) reflecting the old charges, and other "normal"
304 // adjustments to these charges.
306 // DTM01 identifies the type of service date:
307 // 472 = a single date of service
308 // 150 = service period start
309 // 151 = service period end
310 else if ($segid == 'DTM' && $out['loopid'] == '2110') {
311 $out['dos'] = trim($seg[2]); // yyyymmdd
313 else if ($segid == 'CAS' && $out['loopid'] == '2110') {
314 $i = count($out['svc']) - 1;
315 for ($k = 2; $k < 20; $k += 3) {
316 if (!$seg[$k]) break;
317 $j = count($out['svc'][$i]['adj']);
318 $out['svc'][$i]['adj'][$j] = array();
319 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
320 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
321 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
322 // Note: $seg[$k+2] is "quantity". A value here indicates a change to
323 // the number of units of service. We're ignoring that for now.
326 else if ($segid == 'REF' && $out['loopid'] == '2110') {
327 // ignore
329 else if ($segid == 'AMT' && $seg[1] == 'B6' && $out['loopid'] == '2110') {
330 $i = count($out['svc']) - 1;
331 $out['svc'][$i]['allowed'] = $seg[2]; // report this amount as a note
333 else if ($segid == 'LQ' && $seg[1] == 'HE' && $out['loopid'] == '2110') {
334 $i = count($out['svc']) - 1;
335 $out['svc'][$i]['remark'] = $seg[2];
337 else if ($segid == 'QTY' && $out['loopid'] == '2110') {
338 $out['warnings'] .= "QTY segment at service level ignored.\n";
340 else if ($segid == 'PLB') {
341 // Provider-level adjustments are a General Ledger thing and should not
342 // alter the A/R for the claim, so we just report them as notes.
343 for ($k = 3; $k < 15; $k += 2) {
344 if (!$seg[$k]) break;
345 $out['warnings'] .= 'PROVIDER LEVEL ADJUSTMENT (not claim-specific): $' .
346 sprintf('%.2f', $seg[$k+1]) . " with reason code " . $seg[$k] . "\n";
347 // Note: For PLB adjustment reason codes see IG pages 165-170.
350 else if ($segid == 'SE') {
351 parse_era_2100($out, $cb);
352 $out['loopid'] = '';
353 if ($out['st_control_number'] != trim($seg[2])) {
354 return 'Ending transaction set control number mismatch';
356 if (($out['st_segment_count'] + 1) != trim($seg[1])) {
357 return 'Ending transaction set segment count mismatch';
360 else if ($segid == 'GE') {
361 if ($out['loopid']) return 'Unexpected GE segment';
362 if ($out['gs_control_number'] != trim($seg[2])) {
363 return 'Ending functional group control number mismatch';
366 else if ($segid == 'IEA') {
367 if ($out['loopid']) return 'Unexpected IEA segment';
368 if ($out['isa_control_number'] != trim($seg[2])) {
369 return 'Ending interchange control number mismatch';
372 else {
373 return "Unknown or unexpected segment ID $segid";
376 ++$out['st_segment_count'];
379 if ($segid != 'IEA') return 'Premature end of ERA file';
380 return '';