4 * test_edih_835_accounting.php
6 * Copyright 2016 Kevin McCormick <kevin@kt61p>
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
27 // comment out below exit when need to use this script
29 use OpenEMR\Billing\ParseERA
;
31 function edih_835_accounting($segments, $delimiters)
33 // accounting information is in
34 // BPR TRN CLP SVC PLB
38 *$out['check_number'] = trim($seg[2]); TRN
39 *$out['payer_tax_id'] = substr($seg[3], 1); // 9 digits
40 *$out['payer_id'] = trim($seg[4]);
41 *$out['production_date'] = trim($seg[2]); DTM 405
42 *$out['payer_name'] = trim($seg[2]); N1 loop 1000A
43 * $out['payer_street'] = trim($seg[1]); N3
44 * $out['payer_city'] = trim($seg[1]); N4
45 * $out['payer_state'] = trim($seg[2]);
46 * $out['payer_zip'] = trim($seg[3]);
47 * $out['payee_name'] = trim($seg[2]); N1 loop 1000B
48 *$out['payee_street'] = trim($seg[1]);
49 * $out['payee_city'] = trim($seg[1]);
50 * $out['payee_state'] = trim($seg[2]);
51 *$out['payee_zip'] = trim($seg[3]);
53 * // Clear some stuff to start the new claim:
54 $out['subscriber_lname'] = '';
55 $out['subscriber_fname'] = '';
56 $out['subscriber_mname'] = '';
57 $out['subscriber_member_id'] = '';
59 $out['svc'] = array();
60 * $out['our_claim_id'] = trim($seg[1]);
61 *$out['claim_status_code'] = trim($seg[2]);
62 *$out['amount_charged'] = trim($seg[3]);
63 * $out['amount_approved'] = trim($seg[4]);
64 * $out['amount_patient'] = trim($seg[5]); // pt responsibility, copay + deductible
65 * $out['payer_claim_id'] = trim($seg[7]); // payer's claim number
67 * else if ($segid == 'CAS' && $out['loopid'] == '2100') {
68 // This is a claim-level adjustment and should be unusual.
69 // Handle it by creating a dummy zero-charge service item and
70 // then populating the adjustments into it. See also code in
71 // ParseERA::parseERA2100() which will later plug in a payment reversal
72 // amount that offsets these adjustments.
73 $i = 0; // if present, the dummy service item will be first.
74 if (!$out['svc'][$i]) {
75 $out['svc'][$i] = array();
76 $out['svc'][$i]['code'] = 'Claim';
77 $out['svc'][$i]['mod'] = '';
78 $out['svc'][$i]['chg'] = '0';
79 $out['svc'][$i]['paid'] = '0';
80 $out['svc'][$i]['adj'] = array();
82 for ($k = 2; $k < 20; $k += 3) {
84 $j = count($out['svc'][$i]['adj']);
85 $out['svc'][$i]['adj'][$j] = array();
86 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
87 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
88 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
93 else if ($segid == 'NM1' && $seg[1] == 'QC' && $out['loopid'] == '2100') {
94 $out['patient_lname'] = trim($seg[3]);
95 $out['patient_fname'] = trim($seg[4]);
96 $out['patient_mname'] = trim($seg[5]);
97 $out['patient_member_id'] = trim($seg[9]);
99 // IL = Insured or Subscriber
100 else if ($segid == 'NM1' && $seg[1] == 'IL' && $out['loopid'] == '2100') {
101 $out['subscriber_lname'] = trim($seg[3]);
102 $out['subscriber_fname'] = trim($seg[4]);
103 $out['subscriber_mname'] = trim($seg[5]);
104 $out['subscriber_member_id'] = trim($seg[9]);
106 // 82 = Rendering Provider
107 else if ($segid == 'NM1' && $seg[1] == '82' && $out['loopid'] == '2100') {
108 $out['provider_lname'] = trim($seg[3]);
109 $out['provider_fname'] = trim($seg[4]);
110 $out['provider_mname'] = trim($seg[5]);
111 $out['provider_member_id'] = trim($seg[9]);
113 else if ($segid == 'NM1' && $seg[1] == 'TT' && $out['loopid'] == '2100') {
114 $out['crossover'] = 1;//Claim automatic forward case.
118 * else if ($segid == 'REF' && $seg[1] == '1W' && $out['loopid'] == '2100') {
119 $out['claim_comment'] = trim($seg[2]);
122 * else if ($segid == 'DTM' && $seg[1] == '050' && $out['loopid'] == '2100') {
123 $out['claim_date'] = trim($seg[2]); // yyyymmdd
126 * else if ($segid == 'PER' && $out['loopid'] == '2100') {
128 $out['payer_insurance'] = trim($seg[2]);
129 $out['warnings'] .= 'Claim contact information: ' .
133 * else if ($segid == 'SVC') {
134 if (! $out['loopid']) return 'Unexpected SVC segment';
135 * $out['loopid'] = '2110';
137 // SVC06 if present is our original procedure code that they are changing.
138 // We will not put their crap in our invoice, but rather log a note and
139 // treat it as adjustments to our originally submitted coding.
140 $svc = explode($delimiter3, $seg[6]);
141 $tmp = explode($delimiter3, $seg[1]);
142 $out['warnings'] .= "Payer is restating our procedure " . $svc[1] .
143 " as " . $tmp[1] . ".\n";
145 $svc = explode($delimiter3, $seg[1]);
147 if ($svc[0] != 'HC') return 'SVC segment has unexpected qualifier';
148 // TBD: Other qualifiers are possible; see IG pages 140-141.
149 $i = count($out['svc']);
150 $out['svc'][$i] = array();
152 * // It seems some payers append the modifier with no separator!
153 if (strlen($svc[1]) == 7 && empty($svc[2])) {
154 $out['svc'][$i]['code'] = substr($svc[1], 0, 5);
155 $out['svc'][$i]['mod'] = substr($svc[1], 5);
157 $out['svc'][$i]['code'] = $svc[1];
158 $out['svc'][$i]['mod'] = $svc[2] ? $svc[2] . ':' : '';
159 $out['svc'][$i]['mod'] .= $svc[3] ? $svc[3] . ':' : '';
160 $out['svc'][$i]['mod'] .= $svc[4] ? $svc[4] . ':' : '';
161 $out['svc'][$i]['mod'] .= $svc[5] ? $svc[5] . ':' : '';
162 $out['svc'][$i]['mod'] = preg_replace('/:$/','',$out['svc'][$i]['mod']);
164 $out['svc'][$i]['chg'] = $seg[2];
165 $out['svc'][$i]['paid'] = $seg[3];
166 $out['svc'][$i]['adj'] = array();
167 // Note: SVC05, if present, indicates the paid units of service.
170 * // DTM01 identifies the type of service date:
171 // 472 = a single date of service
172 // 150 = service period start
173 // 151 = service period end
174 else if ($segid == 'DTM' && $out['loopid'] == '2110') {
175 $out['dos'] = trim($seg[2]); // yyyymmdd
177 else if ($segid == 'CAS' && $out['loopid'] == '2110') {
178 $i = count($out['svc']) - 1;
179 for ($k = 2; $k < 20; $k += 3) {
180 if (!$seg[$k]) break;
181 if ($seg[1] == 'CO' && $seg[$k+1] < 0) {
182 $out['warnings'] .= "Negative Contractual Obligation adjustment " .
183 "seems wrong. Inverting, but should be checked!\n";
184 $seg[$k+1] = 0 - $seg[$k+1];
186 $j = count($out['svc'][$i]['adj']);
187 $out['svc'][$i]['adj'][$j] = array();
188 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
189 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
190 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
191 // Note: $seg[$k+2] is "quantity". A value here indicates a change to
192 // the number of units of service. We're ignoring that for now.
195 *else if ($segid == 'LQ' && $seg[1] == 'HE' && $out['loopid'] == '2110') {
196 $i = count($out['svc']) - 1;
197 $out['svc'][$i]['remark'] = $seg[2];
201 * else if ($segid == 'PLB') {
202 // Provider-level adjustments are a General Ledger thing and should not
203 // alter the A/R for the claim, so we just report them as notes.
204 for ($k = 3; $k < 15; $k += 2) {
205 if (!$seg[$k]) break;
206 $out['warnings'] .= 'PROVIDER LEVEL ADJUSTMENT (not claim-specific): $' .
207 sprintf('%.2f', $seg[$k+1]) . " with reason code " . $seg[$k] . "\n";
208 // Note: For PLB adjustment reason codes see IG pages 165-170.
211 else if ($segid == 'SE') {
212 ParseERA::parseERA2100($out, $cb);
214 if ($out['st_control_number'] != trim($seg[2])) {
215 return 'Ending transaction set control number mismatch';
217 if (($out['st_segment_count'] + 1) != trim($seg[1])) {
218 return 'Ending transaction set segment count mismatch';
227 if (is_array($segments) && count($segments)) {
230 csv_edihist_log("edih_835_accounting: invalid segments argument");
231 return "835 accounting: invalid segments argument";
234 foreach ($segments as $seg) {
235 if (strncmp('GS' . $de, $seg, 3) === 0) {
236 $sar = explode($de, $seg);
237 $gs_date = (isset($sar[4]) && $sar[4]) ?
trim($sar[4]) : '';
240 if (strncmp('BPR' . $de, $seg, 4) === 0) {
241 $sar = explode($de, $seg);
242 $check_amount = (isset($sar[2]) && $sar[2]) ?
trim($sar[2]) : '';
243 $check_date = (isset($sar[16]) && $sar[16]) ?
trim($sar[16]) : '';
246 if (strncmp('TRN' . $de, $seg, 4) === 0) {
247 $sar = explode($de, $seg);
248 $ck = (isset($sar[2]) && $sar[2]) ?
trim($sar[2]) : count($out);
249 $out[$ck]['gs_date'] = $gs_date;
250 $out[$ck]['check_amount'] = $check_amount;
251 $out[$ck]['check_date'] = $check_date;
252 $out[$ck]['check_number'] = (isset($sar[2]) && $sar[2]) ?
trim($sar[2]) : '';
255 if (strncmp('LX' . $de, $seg, 3) === 0) {
258 if (strncmp('CLP' . $de, $seg, 4) === 0) {
259 $sar = explode($de, $seg);
262 $i = (isset($out[$ck]['clp'])) ?
count($out[$ck]['clp']) : 0;
264 $out[$ck]['loopid'] = '2100';
265 $out[$ck]['warnings'] = '';
266 // Clear some stuff to start the new claim:
267 $out[$ck]['clp'][$i]['subscriber_lname'] = '';
268 $out[$ck]['clp'][$i]['subscriber_fname'] = '';
269 $out[$ck]['clp'][$i]['subscriber_mname'] = '';
270 $out[$ck]['clp'][$i]['subscriber_member_id'] = '';
271 $out[$ck]['clp'][$i]['crossover'] = 0;
272 $out[$ck]['clp'][$i]['svc'] = array();
274 // This is the poorly-named "Patient Account Number". For 837p
275 // it comes from CLM01 which we populated as pid-diagid-procid,
276 // where diagid and procid are id values from the billing table.
277 // For HCFA 1500 claims it comes from field 26 which we
278 // populated with our familiar pid-encounter billing key.
280 // The 835 spec calls this the "provider-assigned claim control
281 // number" and notes that it is specifically intended for
282 // identifying the claim in the provider's database.
283 $out[$ck]['clp'][$i]['our_claim_id'] = (isset($sar[1]) && $sar[1]) ?
trim($sar[1]) : "";
285 $out[$ck]['clp'][$i]['claim_status_code'] = (isset($sar[2]) && $sar[2]) ?
trim($sar[2]) : "";
286 $out[$ck]['clp'][$i]['amount_charged'] = (isset($sar[3]) && $sar[3]) ?
trim($sar[3]) : "";
287 $out[$ck]['clp'][$i]['amount_approved'] = (isset($sar[4]) && $sar[4]) ?
trim($sar[4]) : "";
288 $out[$ck]['clp'][$i]['amount_patient'] = (isset($sar[5]) && $sar[5]) ?
trim($sar[5]) : ""; // pt responsibility, copay + deductible
289 $out[$ck]['clp'][$i]['payer_claim_id'] = (isset($sar[7]) && $sar[7]) ?
trim($sar[7]) : ""; // payer's claim number
292 if (strncmp('CAS' . $de, $seg, 4) === 0) {
293 $sar = explode($de, $seg);
294 if ($loop == '2100') {
296 // This is a claim-level adjustment and should be unusual.
297 // Handle it by creating a dummy zero-charge service item and
298 // then populating the adjustments into it. See also code in
299 // ParseERA::parseERA2100() which will later plug in a payment reversal
300 // amount that offsets these adjustments.
301 $j = 0; // if present, the dummy service item will be first.
302 if (!$out['svc'][$j]) {
303 $out[$ck]['clp'][$i]['svc'][$j] = array();
304 $out[$ck]['clp'][$i]['svc'][$j]['code'] = 'Claim';
305 $out[$ck]['clp'][$i]['svc'][$j]['mod'] = '';
306 $out[$ck]['clp'][$i]['svc'][$j]['chg'] = '0';
307 $out[$ck]['clp'][$i]['svc'][$j]['paid'] = '0';
308 $out[$ck]['clp'][$i]['svc'][$j]['adj'] = array();
311 for ($k = 2; $k < 20; $k +
= 3) {
312 if (!isset($sar[$k])) {
316 $k = count($out['svc'][$j]['adj']);
317 $out[$ck]['clp'][$i]['svc'][$j]['adj'][$k] = array();
318 $out[$ck]['clp'][$i]['svc'][$j]['adj'][$k]['group_code'] = $sar[1];
319 $out[$ck]['clp'][$i]['svc'][$j]['adj'][$k]['reason_code'] = $sar[$k];
320 $out[$ck]['clp'][$i]['svc'][$j]['adj'][$k]['amount'] = $sar[$k +
1];
322 } elseif ($loopid == '2110') {
323 $sar = explode($de, $seg);
324 $j = count($out[$ck]['clp'][$i]['svc']);
325 $out[$ck]['clp'][$i]['svc'][$j] = array();
326 if (! $out['loopid']) {
327 return 'Unexpected SVC segment';
331 if (isset($sar[6]) && $sar[6]) {
332 // SVC06 if present is our original procedure code that they are changing.
333 // We will not put their crap in our invoice, but rather log a note and
334 // treat it as adjustments to our originally submitted coding.
335 $svc = explode($ds, $sar[6]);
336 $tmp = (isset($sar[1]) && $sar[1]) ?
explode($ds, $sar[1]) : "";
337 $out[$ck]['clp'][$i]['warnings'] .= "Submitted procedure modified " . $svc[1] .
338 " as " . $tmp[1] . ".\n";
340 $svc = explode($delimiter3, $seg[1]);
343 if ($svc[0] != 'HC') {
344 return 'SVC segment has unexpected qualifier';
347 // TBD: Other qualifiers are possible; see IG pages 140-141.
348 $j = count($out[$ck]['clp'][$i]['svc']);
349 $out['svc'][$j] = array();
350 // It seems some payers append the modifier with no separator!
351 if (strlen($svc[1]) == 7 && empty($svc[2])) {
352 $out['svc'][$j]['code'] = substr($svc[1], 0, 5);
353 $out['svc'][$j]['mod'] = substr($svc[1], 5);
355 $out['svc'][$j]['code'] = $svc[1];
356 $out['svc'][$j]['mod'] = $svc[2] ?
$svc[2] . ':' : '';
357 $out['svc'][$j]['mod'] .= $svc[3] ?
$svc[3] . ':' : '';
358 $out['svc'][$j]['mod'] .= $svc[4] ?
$svc[4] . ':' : '';
359 $out['svc'][$j]['mod'] .= $svc[5] ?
$svc[5] . ':' : '';
360 $out['svc'][$j]['mod'] = preg_replace('/:$/', '', $out['svc'][$j]['mod']);
363 $out['svc'][$j]['chg'] = $seg[2];
364 $out['svc'][$j]['paid'] = $seg[3];
365 $out['svc'][$j]['adj'] = array();
366 // Note: SVC05, if present, indicates the paid units of service.
369 } elseif (strncmp('NM1' . $de, $seg, 4) === 0) {
370 $sar = explode($de, $seg);
371 $id = (isset($sar[1]) && $sar[1]) ?
trim($sar[1]) : "";
374 $out[$ck]['clp'][$i]['patient_lname'] = (isset($sar[3]) && $sar[3]) ?
trim($sar[3]) : "";
375 $out[$ck]['clp'][$i]['patient_fname'] = (isset($sar[4]) && $sar[4]) ?
trim($sar[4]) : "";
376 $out[$ck]['clp'][$i]['patient_mname'] = (isset($sar[5]) && $sar[5]) ?
trim($sar[5]) : "";
377 $out[$ck]['clp'][$i]['patient_member_id'] = (isset($sar[9]) && $sar[9]) ?
trim($sar[9]) : "";
378 } elseif ($id == 'IL') {
379 // IL = Insured or Subscriber
380 $out[$ck]['clp'][$i]['subscriber_lname'] = (isset($sar[3]) && $sar[3]) ?
trim($sar[3]) : "";
381 $out[$ck]['clp'][$i]['subscriber_fname'] = (isset($sar[4]) && $sar[4]) ?
trim($sar[4]) : "";
382 $out[$ck]['clp'][$i]['subscriber_mname'] = (isset($sar[5]) && $sar[5]) ?
trim($sar[5]) : "";
383 $out[$ck]['clp'][$i]['subscriber_member_id'] = (isset($sar[9]) && $sar[9]) ?
trim($sar[9]) : "";
384 } elseif ($id == '82') {
385 // 82 = Rendering Provider
386 $out[$ck]['clp'][$i]['provider_lname'] = (isset($sar[3]) && $sar[3]) ?
trim($sar[3]) : "";
387 $out[$ck]['clp'][$i]['provider_fname'] = (isset($sar[4]) && $sar[4]) ?
trim($sar[4]) : "";
388 $out[$ck]['clp'][$i]['provider_mname'] = (isset($sar[5]) && $sar[5]) ?
trim($sar[5]) : "";
389 $out[$ck]['clp'][$i]['provider_member_id'] = (isset($sar[9]) && $sar[9]) ?
trim($sar[9]) : "";
390 } elseif ($id == 'TT') {
391 //Claim automatic forward case.
392 $out[$ck]['clp'][$i]['crossover'] = 1;
394 } elseif ((strncmp('PER' . $de, $seg, 4) === 0 ) && ($segid == 'PER' && $out['loopid'] == '2100')) {
395 $sar = explode($de, $seg);
396 $out['payer_insurance'] = trim($seg[2]);
397 $out['warnings'] .= 'Claim contact information: ' . $seg[4];
398 } elseif (strncmp('PLB' . $de, $seg, 4) === 0) {
399 $sar = explode($de, $seg);
400 $p = (isset($out[$ck]['plb'])) ?
count($out[$ck]['plb']) : 0;
403 $out[$ck]['plb'][$p] = array();
404 $out[$ck]['plb']['adj'] = array();
405 $out[$ck]['plb'][$p]['provider'] = (isset($sar[1]) && $sar[1]) ?
trim($sar[1]) : "";
406 $out[$ck]['plb'][$p]['fye'] = (isset($sar[2]) && $sar[2]) ?
trim($sar[2]) : "";
407 // PLB02 is provider fiscal year end or CCYY1231
409 $plbar = array_slice($sar, 3);
410 foreach ($plbar as $ky => $plb) {
414 $out[$ck]['clp'][$p]['adj'][$q]['amt'] = $plb;
418 if (strpos($plb, $ds)) {
419 $plb02 = explode($ds, $plb);
420 $out[$ck]['per'][$p]['adj'][$q]['code'] = $plb02[0];
421 $out[$ck]['per'][$p]['adj'][$q]['ref'] = $plb02[1];
423 $out[$ck]['per'][$p]['adj'][$q]['code'] = $plb;
424 $out[$ck]['per'][$p]['adj'][$q]['ref'] = '';
433 $plbar .= (isset($sar[3]) && $sar[3]) ?
trim($sar[3]) : '0';
434 // I am not sure that the assignment is corrent here, but based on the flow, I frame it.
437 if (strncmp('SVC' . $de, $seg, 4) === 0) {
441 $acctng['lx'][$lx01] = array('ts3amt' => 0, 'fee' => 0, 'clmpmt' => 0, 'clmadj' => 0, 'prvadj' => 0, 'ptrsp' => 0);
443 $acctng['pmt'] = $bpr02;
446 // try a little accounting
448 if ($acctng['pmt'] == ($acctng['clmpmt'] +
$acctng['prvadj'])) {
451 $bal = 'Not Balanced';
454 $pmt_html .= "<tr class='pmt'><td colspan=4>Accounting " . text($bal) . "</td></tr>" . PHP_EOL
;
455 $pmt_html .= "<tr class='pmt'><td>Fee " . text($acctng['fee']) . "</td><td>Adj " . text($acctng['clmadj']) . "</td><td>PtRsp " . text($acctng['ptrsp']) . "</td></tr>" . PHP_EOL
;
456 $pmt_html .= "<tr class='pmt'><td>PMT " . text($acctng['pmt']) . "</td><td>CLP " . text($acctng['clmpmt']) . "</td><td>PLB " . text($acctng['prvadj']) . "</td></tr>" . PHP_EOL
;