psr fix to prior commit
[openemr.git] / library / edihistory / test_edih_835_accounting.php
blobbec99cbc5c287cf59f28c1593373507a8ed05dfd
1 <?php
2 /*
3 * test_edih_835_accounting.php
4 *
5 * Copyright 2016 Kevin McCormick <kevin@kt61p>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20 * MA 02110-1301, USA.
25 function edih_835_accounting($segments, $delimiters)
27 // accounting information is in
28 // BPR TRN CLP SVC PLB
29 /*****
32 *$out['check_number'] = trim($seg[2]); TRN
33 *$out['payer_tax_id'] = substr($seg[3], 1); // 9 digits
34 *$out['payer_id'] = trim($seg[4]);
35 *$out['production_date'] = trim($seg[2]); DTM 405
36 *$out['payer_name'] = trim($seg[2]); N1 loop 1000A
37 * $out['payer_street'] = trim($seg[1]); N3
38 * $out['payer_city'] = trim($seg[1]); N4
39 * $out['payer_state'] = trim($seg[2]);
40 * $out['payer_zip'] = trim($seg[3]);
41 * $out['payee_name'] = trim($seg[2]); N1 loop 1000B
42 *$out['payee_street'] = trim($seg[1]);
43 * $out['payee_city'] = trim($seg[1]);
44 * $out['payee_state'] = trim($seg[2]);
45 *$out['payee_zip'] = trim($seg[3]);
46 * CLP segment
47 * // Clear some stuff to start the new claim:
48 $out['subscriber_lname'] = '';
49 $out['subscriber_fname'] = '';
50 $out['subscriber_mname'] = '';
51 $out['subscriber_member_id'] = '';
52 $out['crossover']=0;
53 $out['svc'] = array();
54 * $out['our_claim_id'] = trim($seg[1]);
55 *$out['claim_status_code'] = trim($seg[2]);
56 *$out['amount_charged'] = trim($seg[3]);
57 * $out['amount_approved'] = trim($seg[4]);
58 * $out['amount_patient'] = trim($seg[5]); // pt responsibility, copay + deductible
59 * $out['payer_claim_id'] = trim($seg[7]); // payer's claim number
61 * else if ($segid == 'CAS' && $out['loopid'] == '2100') {
62 // This is a claim-level adjustment and should be unusual.
63 // Handle it by creating a dummy zero-charge service item and
64 // then populating the adjustments into it. See also code in
65 // parse_era_2100() which will later plug in a payment reversal
66 // amount that offsets these adjustments.
67 $i = 0; // if present, the dummy service item will be first.
68 if (!$out['svc'][$i]) {
69 $out['svc'][$i] = array();
70 $out['svc'][$i]['code'] = 'Claim';
71 $out['svc'][$i]['mod'] = '';
72 $out['svc'][$i]['chg'] = '0';
73 $out['svc'][$i]['paid'] = '0';
74 $out['svc'][$i]['adj'] = array();
76 for ($k = 2; $k < 20; $k += 3) {
77 if (!$seg[$k]) break;
78 $j = count($out['svc'][$i]['adj']);
79 $out['svc'][$i]['adj'][$j] = array();
80 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
81 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
82 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
86 * // QC = Patient
87 else if ($segid == 'NM1' && $seg[1] == 'QC' && $out['loopid'] == '2100') {
88 $out['patient_lname'] = trim($seg[3]);
89 $out['patient_fname'] = trim($seg[4]);
90 $out['patient_mname'] = trim($seg[5]);
91 $out['patient_member_id'] = trim($seg[9]);
93 // IL = Insured or Subscriber
94 else if ($segid == 'NM1' && $seg[1] == 'IL' && $out['loopid'] == '2100') {
95 $out['subscriber_lname'] = trim($seg[3]);
96 $out['subscriber_fname'] = trim($seg[4]);
97 $out['subscriber_mname'] = trim($seg[5]);
98 $out['subscriber_member_id'] = trim($seg[9]);
100 // 82 = Rendering Provider
101 else if ($segid == 'NM1' && $seg[1] == '82' && $out['loopid'] == '2100') {
102 $out['provider_lname'] = trim($seg[3]);
103 $out['provider_fname'] = trim($seg[4]);
104 $out['provider_mname'] = trim($seg[5]);
105 $out['provider_member_id'] = trim($seg[9]);
107 else if ($segid == 'NM1' && $seg[1] == 'TT' && $out['loopid'] == '2100') {
108 $out['crossover'] = 1;//Claim automatic forward case.
112 * else if ($segid == 'REF' && $seg[1] == '1W' && $out['loopid'] == '2100') {
113 $out['claim_comment'] = trim($seg[2]);
116 * else if ($segid == 'DTM' && $seg[1] == '050' && $out['loopid'] == '2100') {
117 $out['claim_date'] = trim($seg[2]); // yyyymmdd
120 * else if ($segid == 'PER' && $out['loopid'] == '2100') {
122 $out['payer_insurance'] = trim($seg[2]);
123 $out['warnings'] .= 'Claim contact information: ' .
124 $seg[4] . "\n";
127 * else if ($segid == 'SVC') {
128 if (! $out['loopid']) return 'Unexpected SVC segment';
129 * $out['loopid'] = '2110';
130 if ($seg[6]) {
131 // SVC06 if present is our original procedure code that they are changing.
132 // We will not put their crap in our invoice, but rather log a note and
133 // treat it as adjustments to our originally submitted coding.
134 $svc = explode($delimiter3, $seg[6]);
135 $tmp = explode($delimiter3, $seg[1]);
136 $out['warnings'] .= "Payer is restating our procedure " . $svc[1] .
137 " as " . $tmp[1] . ".\n";
138 } else {
139 $svc = explode($delimiter3, $seg[1]);
141 if ($svc[0] != 'HC') return 'SVC segment has unexpected qualifier';
142 // TBD: Other qualifiers are possible; see IG pages 140-141.
143 $i = count($out['svc']);
144 $out['svc'][$i] = array();
146 * // It seems some payers append the modifier with no separator!
147 if (strlen($svc[1]) == 7 && empty($svc[2])) {
148 $out['svc'][$i]['code'] = substr($svc[1], 0, 5);
149 $out['svc'][$i]['mod'] = substr($svc[1], 5);
150 } else {
151 $out['svc'][$i]['code'] = $svc[1];
152 $out['svc'][$i]['mod'] = $svc[2] ? $svc[2] . ':' : '';
153 $out['svc'][$i]['mod'] .= $svc[3] ? $svc[3] . ':' : '';
154 $out['svc'][$i]['mod'] .= $svc[4] ? $svc[4] . ':' : '';
155 $out['svc'][$i]['mod'] .= $svc[5] ? $svc[5] . ':' : '';
156 $out['svc'][$i]['mod'] = preg_replace('/:$/','',$out['svc'][$i]['mod']);
158 $out['svc'][$i]['chg'] = $seg[2];
159 $out['svc'][$i]['paid'] = $seg[3];
160 $out['svc'][$i]['adj'] = array();
161 // Note: SVC05, if present, indicates the paid units of service.
162 // It defaults to 1.
164 * // DTM01 identifies the type of service date:
165 // 472 = a single date of service
166 // 150 = service period start
167 // 151 = service period end
168 else if ($segid == 'DTM' && $out['loopid'] == '2110') {
169 $out['dos'] = trim($seg[2]); // yyyymmdd
171 else if ($segid == 'CAS' && $out['loopid'] == '2110') {
172 $i = count($out['svc']) - 1;
173 for ($k = 2; $k < 20; $k += 3) {
174 if (!$seg[$k]) break;
175 if ($seg[1] == 'CO' && $seg[$k+1] < 0) {
176 $out['warnings'] .= "Negative Contractual Obligation adjustment " .
177 "seems wrong. Inverting, but should be checked!\n";
178 $seg[$k+1] = 0 - $seg[$k+1];
180 $j = count($out['svc'][$i]['adj']);
181 $out['svc'][$i]['adj'][$j] = array();
182 $out['svc'][$i]['adj'][$j]['group_code'] = $seg[1];
183 $out['svc'][$i]['adj'][$j]['reason_code'] = $seg[$k];
184 $out['svc'][$i]['adj'][$j]['amount'] = $seg[$k+1];
185 // Note: $seg[$k+2] is "quantity". A value here indicates a change to
186 // the number of units of service. We're ignoring that for now.
189 *else if ($segid == 'LQ' && $seg[1] == 'HE' && $out['loopid'] == '2110') {
190 $i = count($out['svc']) - 1;
191 $out['svc'][$i]['remark'] = $seg[2];
195 * else if ($segid == 'PLB') {
196 // Provider-level adjustments are a General Ledger thing and should not
197 // alter the A/R for the claim, so we just report them as notes.
198 for ($k = 3; $k < 15; $k += 2) {
199 if (!$seg[$k]) break;
200 $out['warnings'] .= 'PROVIDER LEVEL ADJUSTMENT (not claim-specific): $' .
201 sprintf('%.2f', $seg[$k+1]) . " with reason code " . $seg[$k] . "\n";
202 // Note: For PLB adjustment reason codes see IG pages 165-170.
205 else if ($segid == 'SE') {
206 parse_era_2100($out, $cb);
207 $out['loopid'] = '';
208 if ($out['st_control_number'] != trim($seg[2])) {
209 return 'Ending transaction set control number mismatch';
211 if (($out['st_segment_count'] + 1) != trim($seg[1])) {
212 return 'Ending transaction set segment count mismatch';
221 if (is_array($segments) && count($segments)) {
222 $acct = array();
223 } else {
224 csv_edihist_log("edih_835_accounting: invalid segments argument");
225 return "835 accounting: invalid segments argument";
228 foreach ($segments as $seg) {
229 if (strncmp('GS'.$de, $seg, 3) === 0) {
230 $sar = explode($de, $seg);
231 $gs_date = (isset($sar[4]) && $sar[4]) ? trim($sar[4]) : '';
234 if (strncmp('BPR'.$de, $seg, 4) === 0) {
235 $sar = explode($de, $seg);
236 $check_amount = (isset($sar[2]) && $sar[2]) ? trim($sar[2]) : '';
237 $check_date = (isset($sar[16]) && $sar[16]) ? trim($sar[16]) : '';
240 if (strncmp('TRN'.$de, $seg, 4) === 0) {
241 $sar = explode($de, $seg);
242 $ck = (isset($sar[2]) && $sar[2]) ? trim($sar[2]) : count($out);
243 $out[$ck]['gs_date'] = $gs_date;
244 $out[$ck]['check_amount'] = $check_amount;
245 $out[$ck]['check_date'] = $check_date;
246 $out[$ck]['check_number'] = (isset($sar[2]) && $sar[2]) ? trim($sar[2]) : '';
249 if (strncmp('LX'.$de, $seg, 3) === 0) {
252 if (strncmp('CLP'.$de, $seg, 4) === 0) {
253 $sar = explode($de, $seg);
254 $loopid = '2100';
256 $i = (isset($out[$ck]['clp'])) ? count($out[$ck]['clp']) : 0;
258 $out[$ck]['loopid'] = '2100';
259 $out[$ck]['warnings'] = '';
260 // Clear some stuff to start the new claim:
261 $out[$ck]['clp'][$i]['subscriber_lname'] = '';
262 $out[$ck]['clp'][$i]['subscriber_fname'] = '';
263 $out[$ck]['clp'][$i]['subscriber_mname'] = '';
264 $out[$ck]['clp'][$i]['subscriber_member_id'] = '';
265 $out[$ck]['clp'][$i]['crossover']=0;
266 $out[$ck]['clp'][$i]['svc'] = array();
268 // This is the poorly-named "Patient Account Number". For 837p
269 // it comes from CLM01 which we populated as pid-diagid-procid,
270 // where diagid and procid are id values from the billing table.
271 // For HCFA 1500 claims it comes from field 26 which we
272 // populated with our familiar pid-encounter billing key.
274 // The 835 spec calls this the "provider-assigned claim control
275 // number" and notes that it is specifically intended for
276 // identifying the claim in the provider's database.
277 $out[$ck]['clp'][$i]['our_claim_id'] = (isset($sar[1]) && $sar[1]) ? trim($sar[1]) : "";
279 $out[$ck]['clp'][$i]['claim_status_code'] = (isset($sar[2]) && $sar[2]) ? trim($sar[2]) : "";
280 $out[$ck]['clp'][$i]['amount_charged'] = (isset($sar[3]) && $sar[3]) ? trim($sar[3]) : "";
281 $out[$ck]['clp'][$i]['amount_approved'] = (isset($sar[4]) && $sar[4]) ? trim($sar[4]) : "";
282 $out[$ck]['clp'][$i]['amount_patient'] = (isset($sar[5]) && $sar[5]) ? trim($sar[5]) : ""; // pt responsibility, copay + deductible
283 $out[$ck]['clp'][$i]['payer_claim_id'] = (isset($sar[7]) && $sar[7]) ? trim($sar[7]) : ""; // payer's claim number
286 if (strncmp('CAS'.$de, $seg, 4) === 0) {
287 $sar = explode($de, $seg);
288 if ($loop == '2100') {
290 // This is a claim-level adjustment and should be unusual.
291 // Handle it by creating a dummy zero-charge service item and
292 // then populating the adjustments into it. See also code in
293 // parse_era_2100() which will later plug in a payment reversal
294 // amount that offsets these adjustments.
295 $j = 0; // if present, the dummy service item will be first.
296 if (!$out['svc'][$j]) {
297 $out[$ck]['clp'][$i]['svc'][$j] = array();
298 $out[$ck]['clp'][$i]['svc'][$j]['code'] = 'Claim';
299 $out[$ck]['clp'][$i]['svc'][$j]['mod'] = '';
300 $out[$ck]['clp'][$i]['svc'][$j]['chg'] = '0';
301 $out[$ck]['clp'][$i]['svc'][$j]['paid'] = '0';
302 $out[$ck]['clp'][$i]['svc'][$j]['adj'] = array();
305 for ($k = 2; $k < 20; $k += 3) {
306 if (!isset($sar[$k])) {
307 break;
310 $k = count($out['svc'][$j]['adj']);
311 $out[$ck]['clp'][$i]['svc'][$j]['adj'][$k] = array();
312 $out[$ck]['clp'][$i]['svc'][$j]['adj'][$k]['group_code'] = $sar[1];
313 $out[$ck]['clp'][$i]['svc'][$j]['adj'][$k]['reason_code'] = $sar[$k];
314 $out[$ck]['clp'][$i]['svc'][$j]['adj'][$k]['amount'] = $sar[$k+1];
316 } elseif ($loopid == '2110') {
317 $sar = explode($de, $seg);
318 $j = count($out[$ck]['clp'][$i]['svc']);
319 $out[$ck]['clp'][$i]['svc'][$j] = array();
320 if (! $out['loopid']) {
321 return 'Unexpected SVC segment';
325 if (isset($sar[6]) && $sar[6]) {
326 // SVC06 if present is our original procedure code that they are changing.
327 // We will not put their crap in our invoice, but rather log a note and
328 // treat it as adjustments to our originally submitted coding.
329 $svc = explode($ds, $sar[6]);
330 $tmp = (isset($sar[1]) && $sar[1]) ? explode($ds, $sar[1]): "";
331 $out[$ck]['clp'][$i]['warnings'] .= "Submitted procedure modified " . $svc[1] .
332 " as " . $tmp[1] . ".\n";
333 } else {
334 $svc = explode($delimiter3, $seg[1]);
337 if ($svc[0] != 'HC') {
338 return 'SVC segment has unexpected qualifier';
341 // TBD: Other qualifiers are possible; see IG pages 140-141.
342 $j = count($out[$ck]['clp'][$i]['svc']);
343 $out['svc'][$j] = array();
344 // It seems some payers append the modifier with no separator!
345 if (strlen($svc[1]) == 7 && empty($svc[2])) {
346 $out['svc'][$j]['code'] = substr($svc[1], 0, 5);
347 $out['svc'][$j]['mod'] = substr($svc[1], 5);
348 } else {
349 $out['svc'][$j]['code'] = $svc[1];
350 $out['svc'][$j]['mod'] = $svc[2] ? $svc[2] . ':' : '';
351 $out['svc'][$j]['mod'] .= $svc[3] ? $svc[3] . ':' : '';
352 $out['svc'][$j]['mod'] .= $svc[4] ? $svc[4] . ':' : '';
353 $out['svc'][$j]['mod'] .= $svc[5] ? $svc[5] . ':' : '';
354 $out['svc'][$j]['mod'] = preg_replace('/:$/', '', $out['svc'][$j]['mod']);
357 $out['svc'][$j]['chg'] = $seg[2];
358 $out['svc'][$j]['paid'] = $seg[3];
359 $out['svc'][$j]['adj'] = array();
360 // Note: SVC05, if present, indicates the paid units of service.
361 // It defaults to 1.
363 } elseif (strncmp('NM1'.$de, $seg, 4) === 0) {
364 $sar = explode($de, $seg);
365 $id = (isset($sar[1]) && $sar[1]) ? trim($sar[1]) : "";
366 if ($id == 'QC') {
367 // QC Patient
368 $out[$ck]['clp'][$i]['patient_lname'] = (isset($sar[3]) && $sar[3]) ? trim($sar[3]) : "";
369 $out[$ck]['clp'][$i]['patient_fname'] = (isset($sar[4]) && $sar[4]) ? trim($sar[4]) : "";
370 $out[$ck]['clp'][$i]['patient_mname'] = (isset($sar[5]) && $sar[5]) ? trim($sar[5]) : "";
371 $out[$ck]['clp'][$i]['patient_member_id'] = (isset($sar[9]) && $sar[9]) ? trim($sar[9]) : "";
372 } elseif ($id == 'IL') {
373 // IL = Insured or Subscriber
374 $out[$ck]['clp'][$i]['subscriber_lname'] = (isset($sar[3]) && $sar[3]) ? trim($sar[3]) : "";
375 $out[$ck]['clp'][$i]['subscriber_fname'] = (isset($sar[4]) && $sar[4]) ? trim($sar[4]) : "";
376 $out[$ck]['clp'][$i]['subscriber_mname'] = (isset($sar[5]) && $sar[5]) ? trim($sar[5]) : "";
377 $out[$ck]['clp'][$i]['subscriber_member_id'] = (isset($sar[9]) && $sar[9]) ? trim($sar[9]) : "";
378 } elseif ($id == '82') {
379 // 82 = Rendering Provider
380 $out[$ck]['clp'][$i]['provider_lname'] = (isset($sar[3]) && $sar[3]) ? trim($sar[3]) : "";
381 $out[$ck]['clp'][$i]['provider_fname'] = (isset($sar[4]) && $sar[4]) ? trim($sar[4]) : "";
382 $out[$ck]['clp'][$i]['provider_mname'] = (isset($sar[5]) && $sar[5]) ? trim($sar[5]) : "";
383 $out[$ck]['clp'][$i]['provider_member_id'] = (isset($sar[9]) && $sar[9]) ? trim($sar[9]) : "";
384 } elseif ($id =='TT') {
385 //Claim automatic forward case.
386 $out[$ck]['clp'][$i]['crossover'] = 1;
388 } elseif ((strncmp('PER'.$de, $seg, 4) === 0 ) && ($segid == 'PER' && $out['loopid'] == '2100')) {
389 $sar = explode($de, $seg);
390 $out['payer_insurance'] = trim($seg[2]);
391 $out['warnings'] .= 'Claim contact information: '.$seg[4];
392 } elseif (strncmp('PLB'.$de, $seg, 4) === 0) {
393 $sar = explode($de, $seg);
394 $p = (isset($out[$ck]['plb'])) ? count($out[$ck]['plb']) : 0;
395 $q = 0;
397 $out[$ck]['plb'][$p] = array();
398 $out[$ck]['plb']['adj'] = array();
399 $out[$ck]['plb'][$p]['provider'] = (isset($sar[1]) && $sar[1]) ? trim($sar[1]) : "";
400 $out[$ck]['plb'][$p]['fye'] = (isset($sar[2]) && $sar[2]) ? trim($sar[2]) : "";
401 // PLB02 is provider fiscal year end or CCYY1231
403 $plbar = array_slice($sar, 3);
404 foreach ($plbar as $ky => $plb) {
405 switch ($ky % 2) {
406 // PLB04 06 08 ...
407 case 0:
408 $out[$ck]['clp'][$p]['adj'][$q]['amt'] = $plb;
409 break;
410 // PLB03 05 07 ...
411 case 1:
412 if (strpos($plb, $ds)) {
413 $plb02 = explode($ds, $plb);
414 $out[$ck]['per'][$p]['adj'][$q]['code'] = $plb02[0];
415 $out[$ck]['per'][$p]['adj'][$q]['ref'] = $plb02[1];
416 } else {
417 $out[$ck]['per'][$p]['adj'][$q]['code'] = $plb;
418 $out[$ck]['per'][$p]['adj'][$q]['ref'] = '';
421 $q++;
422 break;
427 $plbar .= (isset($sar[3]) && $sar[3]) ? trim($sar[3]) : '0';
428 // I am not sure that the assignment is corrent here, but based on the flow, I frame it.
431 if (strncmp('SVC'.$de, $seg, 4) === 0) {
432 $loopid = '2110';
436 $acctng['lx'][$lx01] = array('ts3amt'=>0, 'fee'=>0, 'clmpmt'=>0, 'clmadj'=>0, 'prvadj'=>0, 'ptrsp'=>0);
437 if ($chk) {
438 $acctng['pmt'] = $bpr02;
441 // try a little accounting
442 if ($chk) {
443 if ($acctng['pmt'] == ($acctng['clmpmt'] + $acctng['prvadj'])) {
444 $bal = 'Balanced';
445 } else {
446 $bal = 'Not Balanced';
449 $pmt_html .= "<tr class='pmt'><td colspan=4>Accounting $bal</td></tr>".PHP_EOL;
450 $pmt_html .= "<tr class='pmt'><td>Fee {$acctng['fee']}</td><td>Adj {$acctng['clmadj']}</td><td>PtRsp {$acctng['ptrsp']}</td></tr>".PHP_EOL;
451 $pmt_html .= "<tr class='pmt'><td>PMT {$acctng['pmt']}</td><td>CLP {$acctng['clmpmt']}</td><td>PLB {$acctng['prvadj']}</td></tr>".PHP_EOL;