minor 835 remittance processing fixes
[openemr.git] / library / parse_era.inc.php
blob374400d038cf6a1f47669d05b27a2401c05b92b7
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') {
11 $cb($out);
15 function parse_era($filename, $cb) {
16 $infh = fopen($filename, 'r');
17 if (! $infh) return "ERA input file open failed";
19 $out = array();
20 $out['loopid'] = '';
21 $out['st_segment_count'] = 0;
22 $buffer = '';
23 $segid = '';
25 while (true) {
26 if (strlen($buffer) < 2048 && ! feof($infh))
27 $buffer .= fread($infh, 2048);
28 $tpos = strpos($buffer, '~');
29 if ($tpos === false) break;
30 $inline = substr($buffer, 0, $tpos);
31 $buffer = substr($buffer, $tpos + 1);
33 // echo $inline . "\n"; // debugging
35 $seg = explode('|', $inline);
36 $segid = $seg[0];
38 if ($segid == 'ISA') {
39 if ($out['loopid']) return 'Unexpected ISA segment';
40 $out['isa_sender_id'] = trim($seg[6]);
41 $out['isa_receiver_id'] = trim($seg[8]);
42 $out['isa_control_number'] = trim($seg[13]);
43 // TBD: clear some stuff if we allow multiple transmission files.
45 else if ($segid == 'GS') {
46 if ($out['loopid']) return 'Unexpected GS segment';
47 $out['gs_date'] = trim($seg[4]);
48 $out['gs_time'] = trim($seg[5]);
49 $out['gs_control_number'] = trim($seg[6]);
51 else if ($segid == 'ST') {
52 parse_era_2100($out, $cb);
53 $out['loopid'] = '';
54 $out['st_control_number'] = trim($seg[2]);
55 $out['st_segment_count'] = 0;
57 else if ($segid == 'BPR') {
58 if ($out['loopid']) return 'Unexpected BPR segment';
59 $out['check_amount'] = trim($seg[2]);
60 $out['check_date'] = trim($seg[16]); // yyyymmdd
62 else if ($segid == 'TRN') {
63 if ($out['loopid']) return 'Unexpected TRN segment';
64 $out['check_number'] = trim($seg[2]);
65 $out['payer_tax_id'] = substr($seg[3], 1); // 9 digits
66 $out['payer_id'] = trim($seg[4]);
68 else if ($segid == 'REF' && $seg[1] == 'EV') {
69 if ($out['loopid']) return 'Unexpected REF|EV segment';
71 else if ($segid == 'CUR' && ! $out['loopid']) {
72 // ignore
74 else if ($segid == 'REF' && ! $out['loopid']) {
75 // ignore
77 else if ($segid == 'DTM' && $seg[1] == '405') {
78 if ($out['loopid']) return 'Unexpected DTM|405 segment';
79 $out['production_date'] = trim($seg[2]); // yyyymmdd
81 else if ($segid == 'N1' && $seg[1] == 'PR') {
82 if ($out['loopid']) return 'Unexpected N1|PR segment';
83 $out['loopid'] = '1000A';
84 $out['payer_name'] = trim($seg[2]);
86 else if ($segid == 'N3' && $out['loopid'] == '1000A') {
87 $out['payer_street'] = trim($seg[1]);
89 else if ($segid == 'N4' && $out['loopid'] == '1000A') {
90 $out['payer_city'] = trim($seg[1]);
91 $out['payer_state'] = trim($seg[2]);
92 $out['payer_zip'] = trim($seg[3]);
94 else if ($segid == 'REF' && $out['loopid'] == '1000A') {
95 // ignore
97 else if ($segid == 'PER' && $out['loopid'] == '1000A') {
98 // ignore
100 else if ($segid == 'N1' && $seg[1] == 'PE') {
101 if ($out['loopid'] != '1000A') return 'Unexpected N1|PE segment';
102 $out['loopid'] = '1000B';
103 $out['payee_name'] = trim($seg[2]);
104 $out['payee_tax_id'] = trim($seg[4]);
106 else if ($segid == 'N3' && $out['loopid'] == '1000B') {
107 $out['payee_street'] = trim($seg[1]);
109 else if ($segid == 'N4' && $out['loopid'] == '1000B') {
110 $out['payee_city'] = trim($seg[1]);
111 $out['payee_state'] = trim($seg[2]);
112 $out['payee_zip'] = trim($seg[3]);
114 else if ($segid == 'REF' && $out['loopid'] == '1000B') {
115 // ignore
117 else if ($segid == 'LX') {
118 if (! $out['loopid']) return 'Unexpected LX segment';
119 parse_era_2100($out, $cb);
120 $out['loopid'] = '2000';
122 else if ($segid == 'TS2' && $out['loopid'] == '2000') {
123 // ignore
125 else if ($segid == 'TS3' && $out['loopid'] == '2000') {
126 // ignore
128 else if ($segid == 'CLP') {
129 if (! $out['loopid']) return 'Unexpected CLP segment';
130 parse_era_2100($out, $cb);
131 $out['loopid'] = '2100';
132 $out['warnings'] = '';
133 // Clear some stuff to start the new claim:
134 $out['subscriber_lname'] = '';
135 $out['subscriber_fname'] = '';
136 $out['subscriber_mname'] = '';
137 $out['subscriber_member_id'] = '';
138 $out['svc'] = array();
140 // This is the poorly-named "Patient Account Number". For 837p
141 // it comes from CLM01 which we populated as pid-diagid-procid,
142 // where diagid and procid are id values from the billing table.
143 // For HCFA 1500 claims it comes from field 26 which we
144 // populated with our familiar pid-encounter billing key.
146 // The 835 spec calls this the "provider-assigned claim control
147 // number" and notes that it is specifically intended for
148 // identifying the claim in the provider's database.
150 // When this field does not conform to either of our formats,
151 // it's likely that the claim pre-dates the clinic's OpenEMR
152 // installation and we should probably just flag it for manual
153 // posting. A better solution might be tailored for sites
154 // where A/R was converted from a prior system.
155 $out['our_claim_id'] = trim($seg[1]);
157 $out['claim_status_code'] = trim($seg[2]);
158 $out['amount_charged'] = trim($seg[3]);
159 $out['amount_approved'] = trim($seg[4]);
160 $out['amount_patient'] = trim($seg[5]); // pt responsibility, copay + deductible
161 $out['payer_claim_id'] = trim($seg[7]); // payer's claim number
163 else if ($segid == 'CAS' && $out['loopid'] == '2100') {
164 // TBD: It is technically valid for adjustments to occur at the claim
165 // level. I guess we need to create a dummy service item for these.
166 $out['warnings'] .= "Adjustment at claim level not handled!\n";
168 else if ($segid == 'NM1' && $seg[1] == 'QC' && $out['loopid'] == '2100') {
169 $out['patient_lname'] = trim($seg[3]);
170 $out['patient_fname'] = trim($seg[4]);
171 $out['patient_mname'] = trim($seg[5]);
172 $out['patient_member_id'] = trim($seg[9]);
174 else if ($segid == 'NM1' && $seg[1] == 'IL' && $out['loopid'] == '2100') {
175 $out['subscriber_lname'] = trim($seg[3]);
176 $out['subscriber_fname'] = trim($seg[4]);
177 $out['subscriber_mname'] = trim($seg[5]);
178 $out['subscriber_member_id'] = trim($seg[9]);
180 else if ($segid == 'NM1' && $seg[1] == '82' && $out['loopid'] == '2100') {
181 $out['provider_lname'] = trim($seg[3]);
182 $out['provider_fname'] = trim($seg[4]);
183 $out['provider_mname'] = trim($seg[5]);
184 $out['provider_member_id'] = trim($seg[9]);
186 else if ($segid == 'NM1' && $out['loopid'] == '2100') {
187 // $out['warnings'] .= "NM1 segment at claim level ignored.\n";
189 else if ($segid == 'MOA' && $out['loopid'] == '2100') {
190 $out['warnings'] .= "MOA segment at claim level ignored.\n";
192 else if ($segid == 'REF' && $seg[1] == '1W' && $out['loopid'] == '2100') {
193 $out['claim_comment'] = trim($seg[2]);
195 else if ($segid == 'REF' && $out['loopid'] == '2100') {
196 // ignore; saw a "REF|EA|X" from Tricare, dunno what that is.
198 else if ($segid == 'DTM' && $seg[1] == '050' && $out['loopid'] == '2100') {
199 $out['claim_date'] = trim($seg[2]); // yyyymmdd
201 else if ($segid == 'DTM' && $out['loopid'] == '2100') {
202 // ignore?
204 else if ($segid == 'PER' && $out['loopid'] == '2100') {
205 $out['warnings'] .= 'Claim contact information: ' .
206 $seg[4] . "\n";
208 else if ($segid == 'AMT' && $out['loopid'] == '2100') {
209 $out['warnings'] .= "AMT segment at claim level ignored.\n";
211 else if ($segid == 'QTY' && $out['loopid'] == '2100') {
212 $out['warnings'] .= "QTY segment at claim level ignored.\n";
214 else if ($segid == 'SVC') {
215 if (! $out['loopid']) return 'Unexpected SVC segment';
216 $out['loopid'] = '2110';
217 $svc = explode('^', $seg[1]);
218 if ($svc[0] != 'HC') return 'SVC segment has unexpected qualifier';
219 $i = count($out['svc']);
220 $out['svc'][$i] = array();
221 $out['svc'][$i]['code'] = $svc[1];
222 $out['svc'][$i]['chg'] = $seg[2];
223 $out['svc'][$i]['paid'] = $seg[3];
224 $out['svc'][$i]['adj'] = array();
226 else if ($segid == 'DTM' && $out['loopid'] == '2110') {
227 $out['dos'] = trim($seg[2]); // yyyymmdd
229 else if ($segid == 'CAS' && $out['loopid'] == '2110') {
230 // There may be multiple adjustments per service item.
231 $i = count($out['svc']) - 1;
232 $j = count($out['svc'][$i]['adj']);
233 $out['svc'][$i]['adj'][$j] = array();
234 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
235 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[2];
236 $out['svc'][$i]['adj'][$j]['amount'] = $seg[3];
238 else if ($segid == 'REF' && $out['loopid'] == '2110') {
239 // ignore
241 else if ($segid == 'AMT' && $seg[1] == 'B6' && $out['loopid'] == '2110') {
242 $i = count($out['svc']) - 1;
243 $out['svc'][$i]['allowed'] = $seg[2]; // report this amount as a note
245 else if ($segid == 'LQ' && $seg[1] == 'HE' && $out['loopid'] == '2110') {
246 $i = count($out['svc']) - 1;
247 $out['svc'][$i]['remark'] = $seg[2];
249 else if ($segid == 'QTY' && $out['loopid'] == '2110') {
250 $out['warnings'] .= "QTY segment at service level ignored.\n";
252 else if ($segid == 'PLB') {
253 $out['warnings'] .= 'PROVIDER LEVEL ADJUSTMENT (not claim-specific): $' .
254 sprintf('%.2f', $seg[4]) . "\n";
256 else if ($segid == 'SE') {
257 parse_era_2100($out, $cb);
258 $out['loopid'] = '';
259 if ($out['st_control_number'] != trim($seg[2])) {
260 return 'Ending transaction set control number mismatch';
262 if (($out['st_segment_count'] + 1) != trim($seg[1])) {
263 return 'Ending transaction set segment count mismatch';
266 else if ($segid == 'GE') {
267 if ($out['loopid']) return 'Unexpected GE segment';
268 if ($out['gs_control_number'] != trim($seg[2])) {
269 return 'Ending functional group control number mismatch';
272 else if ($segid == 'IEA') {
273 if ($out['loopid']) return 'Unexpected IEA segment';
274 if ($out['isa_control_number'] != trim($seg[2])) {
275 return 'Ending interchange control number mismatch';
278 else {
279 return "Unknown or unexpected segment ID $segid";
282 ++$out['st_segment_count'];
285 if ($segid != 'IEA') return 'Premature end of ERA file';
286 return '';