more billing fixes, support for era posting date override, support for modifiers...
[openemr.git] / library / Claim.class.php
blob5afd4a191cfbbc919727f951bf42c0c5156af8bf
1 <?php
2 // Copyright (C) 2007 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 require_once(dirname(__FILE__) . "/classes/Address.class.php");
10 require_once(dirname(__FILE__) . "/classes/InsuranceCompany.class.php");
11 require_once(dirname(__FILE__) . "/sql-ledger.inc");
12 require_once(dirname(__FILE__) . "/invoice_summary.inc.php");
14 // This enforces the X12 Basic Character Set. Page A2.
16 function x12clean($str) {
17 return preg_replace('/[^A-Z0-9!"\\&\'()+,\\-.\\/;?= ]/', '', strtoupper($str));
20 class Claim {
22 var $pid; // patient id
23 var $encounter_id; // encounter id
24 var $procs; // array of procedure rows from billing table
25 var $x12_partner; // row from x12_partners table
26 var $encounter; // row from form_encounter table
27 var $facility; // row from facility table
28 var $billing_facility; // row from facility table
29 var $provider; // row from users table (rendering provider)
30 var $referrer; // row from users table (referring provider)
31 var $insurance_numbers; // row from insurance_numbers table for current payer
32 var $patient_data; // row from patient_data table
33 var $billing_options; // row from form_misc_billing_options table
34 var $invoice; // result from get_invoice_summary()
35 var $payers; // array of arrays, for all payers
37 function loadPayerInfo(&$billrow) {
38 global $sl_err;
40 // Create the $payers array. This contains data for all insurances
41 // with the current one always at index 0, and the others in payment
42 // order starting at index 1.
44 $this->payers = array();
45 $this->payers[0] = array();
46 $dres = sqlStatement("SELECT * FROM insurance_data WHERE " .
47 "pid = '{$this->pid}' AND provider != '' " .
48 "ORDER BY type");
49 while ($drow = sqlFetchArray($dres)) {
50 $ins = ($drow['provider'] == $billrow['payer_id']) ?
51 0 : count($this->payers);
52 $crow = sqlQuery("SELECT * FROM insurance_companies WHERE " .
53 "id = '" . $drow['provider'] . "'");
54 $orow = new InsuranceCompany($drow['provider']);
55 $this->payers[$ins] = array();
56 $this->payers[$ins]['data'] = $drow;
57 $this->payers[$ins]['company'] = $crow;
58 $this->payers[$ins]['object'] = $orow;
61 $this->using_modifiers = true;
63 // Get payment and adjustment details if there are any previous payers.
65 $this->invoice = array();
66 if ($this->payerSequence() != 'P') {
67 SLConnect();
68 $arres = SLQuery("select id from ar where invnumber = " .
69 "'{$this->pid}.{$this->encounter_id}'");
70 if ($sl_err) die($sl_err);
71 $arrow = SLGetRow($arres, 0);
72 if ($arrow) {
73 $this->invoice = get_invoice_summary($arrow['id'], true);
75 SLClose();
76 // Secondary claims might not have modifiers in SQL-Ledger data.
77 // In that case, note that we should not try to match on them.
78 $this->using_modifiers = false;
79 foreach ($this->invoice as $key => $trash) {
80 if (strpos($key, ':')) $this->using_modifiers = true;
85 // Constructor. Loads relevant database information.
87 function Claim($pid, $encounter_id) {
88 $this->pid = $pid;
89 $this->encounter_id = $encounter_id;
90 $this->procs = array();
92 // Sort by procedure timestamp in order to get some consistency. In particular
93 // we determine the provider from the first procedure in this array.
94 $sql = "SELECT * FROM billing WHERE " .
95 "encounter = '{$this->encounter_id}' AND pid = '{$this->pid}' AND " .
96 "(code_type = 'CPT4' OR code_type = 'HCPCS') AND " .
97 "activity = '1' ORDER BY date, id";
98 $res = sqlStatement($sql);
99 while ($row = sqlFetchArray($res)) {
100 if (!$row['units']) $row['units'] = 1;
101 // Load prior payer data at the first opportunity in order to get
102 // the using_modifiers flag that is referenced below.
103 if (empty($this->procs)) $this->loadPayerInfo($row);
104 // Consolidate duplicate procedures.
105 foreach ($this->procs as $key => $trash) {
106 if ($this->procs[$key]['code'] == $row['code'] &&
107 ($this->procs[$key]['modifier'] == $row['modifier'] ||
108 !$this->using_modifiers))
110 $this->procs[$key]['units'] += $row['units'];
111 $this->procs[$key]['fee'] += $row['fee'];
112 continue 2; // skip to next table row
115 $this->procs[] = $row;
118 $sql = "SELECT * FROM x12_partners WHERE " .
119 "id = '" . $this->procs[0]['x12_partner_id'] . "'";
120 $this->x12_partner = sqlQuery($sql);
122 $sql = "SELECT * FROM form_encounter WHERE " .
123 "pid = '{$this->pid}' AND " .
124 "encounter = '{$this->encounter_id}'";
125 $this->encounter = sqlQuery($sql);
127 $sql = "SELECT * FROM facility WHERE " .
128 "name = '" . addslashes($this->encounter['facility']) . "' " .
129 "ORDER BY id LIMIT 1";
130 $this->facility = sqlQuery($sql);
132 $sql = "SELECT * FROM users WHERE " .
133 "id = '" . $this->procs[0]['provider_id'] . "'";
134 $this->provider = sqlQuery($sql);
136 $sql = "SELECT * FROM facility " .
137 "ORDER BY billing_location DESC, id ASC LIMIT 1";
138 $this->billing_facility = sqlQuery($sql);
140 $sql = "SELECT * FROM insurance_numbers WHERE " .
141 "(insurance_company_id = '" . $this->procs[0]['payer_id'] .
142 "' OR insurance_company_id is NULL) AND " .
143 "provider_id = '" . $this->provider['id'] .
144 "' order by insurance_company_id DESC LIMIT 1";
145 $this->insurance_numbers = sqlQuery($sql);
147 $sql = "SELECT * FROM patient_data WHERE " .
148 "pid = '{$this->pid}' " .
149 "ORDER BY id LIMIT 1";
150 $this->patient_data = sqlQuery($sql);
152 $sql = "SELECT fpa.* FROM forms JOIN form_misc_billing_options AS fpa " .
153 "ON fpa.id = forms.form_id WHERE " .
154 "forms.encounter = '{$this->encounter_id}' AND " .
155 "forms.pid = '{$this->pid}' AND " .
156 "forms.formdir = 'misc_billing_options' " .
157 "ORDER BY forms.date";
158 $this->billing_options = sqlQuery($sql);
160 $sql = "SELECT * FROM users WHERE " .
161 "id = '" . $this->patient_data['providerID'] . "'";
162 $this->referrer = sqlQuery($sql);
163 if (!$this->referrer) $this->referrer = array();
165 } // end constructor
167 // Return an array of adjustments from the designated payer for the
168 // designated procedure key (might be procedure:modifier), or for the claim
169 // level. For each adjustment give date, group code, reason code, amount.
171 function payerAdjustments($ins, $code='Claim') {
172 $aadj = array();
173 $inslabel = ($this->payerSequence($ins) == 'S') ? 'Ins2' : 'Ins1';
175 // If we have no modifiers stored in SQL-Ledger for this claim,
176 // then we cannot use a modifier passed in with the key.
177 $tmp = strpos($code, ':');
178 if ($tmp && !$this->using_modifiers) $code = substr($code, 0, $tmp);
180 // For payments, source always starts with "Ins" or "Pt".
181 // Nonzero adjustment reason examples:
182 // Ins1 adjust code 42 (Charges exceed ... (obsolete))
183 // Ins1 adjust code 45 (Charges exceed your contracted/ legislated fee arrangement)
184 // Ins1 adjust code 97 (Payment is included in the allowance for another service/procedure)
185 // Ins1 adjust code A2 (Contractual adjustment)
186 // Ins adjust Ins1
187 // adjust code 45
188 // Zero adjustment reason examples:
189 // Co-pay: 25.00
190 // Coinsurance: 11.46 (code 2)
191 // To deductible: 0.22 (code 1)
192 // To copay (this seems to be meaningless)
194 if (!empty($this->invoice[$code])) {
195 foreach ($this->invoice[$code]['dtl'] as $key => $value) {
196 $date = str_replace('-', '', trim(substr($key, 0, 10)));
197 if ($date && $value['pmt'] == 0) {
198 $rsn = $value['rsn'];
199 $chg = 0 - $value['chg']; // adjustments are negative charges
201 $gcode = 'CO'; // default group code = contractual obligation
202 $rcode = '45'; // default reason code = max fee exceeded (code 42 is obsolete)
204 if (preg_match("/Ins adjust $inslabel/i", $rsn, $tmp)) {
206 else if (preg_match("/$inslabel adjust code (\S+)/i", $rsn, $tmp)) {
207 $rcode = $tmp[1];
209 else if (preg_match("/$inslabel/i", $rsn, $tmp)) {
210 // Nothing to do.
212 else if ($inslabel == 'Ins1') {
213 if (preg_match("/\$adjust code (\S+)/i", $rsn, $tmp)) {
214 $rcode = $tmp[1];
216 else if ($chg) {
217 // Nothing to do.
219 else if (preg_match("/Co-pay: (\S+)/i", $rsn, $tmp) ||
220 preg_match("/Coinsurance: (\S+)/i", $rsn, $tmp)) {
221 $gcode = 'PR';
222 $rcode = '2';
223 $chg = $tmp[1];
225 else if (preg_match("/To deductible: (\S+)/i", $rsn, $tmp)) {
226 $gcode = 'PR';
227 $rcode = '1';
228 $chg = $tmp[1];
230 else {
231 continue; // there is no adjustment amount anywhere
234 else {
235 continue; // we are not Ins1 and there is no chg so forget it
238 $aadj[] = array($date, $gcode, $rcode, sprintf('%.2f', $chg));
240 } // end if
241 } // end foreach
242 } // end if
244 return $aadj;
247 // Return date, total payments and total adjustments from the designated
248 // prior payer. If $code is specified then only that procedure key is
249 // selected, otherwise it's for the whole claim.
251 function payerTotals($ins, $code='') {
252 // If we have no modifiers stored in SQL-Ledger for this claim,
253 // then we cannot use a modifier passed in with the key.
254 $tmp = strpos($code, ':');
255 if ($tmp && !$this->using_modifiers) $code = substr($code, 0, $tmp);
257 $inslabel = ($this->payerSequence($ins) == 'S') ? 'Ins2' : 'Ins1';
258 $paytotal = 0;
259 $adjtotal = 0;
260 $date = '';
261 foreach($this->invoice as $codekey => $codeval) {
262 if ($code && $codekey != $code) continue;
263 foreach ($codeval['dtl'] as $key => $value) {
264 if (preg_match("/$inslabel/i", $value['src'], $tmp)) {
265 if (!$date) $date = str_replace('-', '', trim(substr($key, 0, 10)));
266 $paytotal += $value['pmt'];
269 $aarr = $this->payerAdjustments($ins, $codekey);
270 foreach ($aarr as $a) {
271 $adjtotal += $a[3];
272 if (!$date) $date = $a[0];
275 return array($date, sprintf('%.2f', $paytotal), sprintf('%.2f', $adjtotal));
278 // Return the amount already paid by the patient.
280 function patientPaidAmount() {
281 $amount = 0;
282 foreach($this->invoice as $codekey => $codeval) {
283 foreach ($codeval['dtl'] as $key => $value) {
284 if (!preg_match("/Ins/i", $value['src'], $tmp)) {
285 $amount += $value['pmt'];
289 return sprintf('%.2f', $amount);
292 // Return invoice total, including adjustments but not payments.
294 function invoiceTotal() {
295 $amount = 0;
296 foreach($this->invoice as $codekey => $codeval) {
297 $amount += $codeval['chg'];
299 return sprintf('%.2f', $amount);
302 // Number of procedures in this claim.
303 function procCount() {
304 return count($this->procs);
307 // Number of payers for this claim. Ranges from 1 to 3.
308 function payerCount() {
309 return count($this->payers);
312 function x12gsversionstring() {
313 return x12clean(trim($this->x12_partner['x12_version']));
316 function x12gssenderid() {
317 $tmp = $this->x12_partner['x12_sender_id'];
318 while (strlen($tmp) < 15) $tmp .= " ";
319 return $tmp;
322 function x12gsreceiverid() {
323 $tmp = $this->x12_partner['x12_receiver_id'];
324 while (strlen($tmp) < 15) $tmp .= " ";
325 return $tmp;
328 function cliaCode() {
329 return x12clean(trim($this->facility['domain_identifier']));
332 function billingFacilityName() {
333 return x12clean(trim($this->billing_facility['name']));
336 function billingFacilityStreet() {
337 return x12clean(trim($this->billing_facility['street']));
340 function billingFacilityCity() {
341 return x12clean(trim($this->billing_facility['city']));
344 function billingFacilityState() {
345 return x12clean(trim($this->billing_facility['state']));
348 function billingFacilityZip() {
349 return x12clean(trim($this->billing_facility['postal_code']));
352 function billingFacilityETIN() {
353 return x12clean(trim(str_replace('-', '', $this->billing_facility['federal_ein'])));
356 function billingFacilityNPI() {
357 return x12clean(trim($this->billing_facility['facility_npi']));
360 function billingContactName() {
361 return x12clean(trim($this->billing_facility['attn']));
364 function billingContactPhone() {
365 if (preg_match("/([2-9]\d\d)\D*(\d\d\d)\D*(\d\d\d\d)/",
366 $this->billing_facility['phone'], $tmp))
368 return $tmp[1] . $tmp[2] . $tmp[3];
370 return '';
373 function facilityName() {
374 return x12clean(trim($this->facility['name']));
377 function facilityStreet() {
378 return x12clean(trim($this->facility['street']));
381 function facilityCity() {
382 return x12clean(trim($this->facility['city']));
385 function facilityState() {
386 return x12clean(trim($this->facility['state']));
389 function facilityZip() {
390 return x12clean(trim($this->facility['postal_code']));
393 function facilityETIN() {
394 return x12clean(trim(str_replace('-', '', $this->facility['federal_ein'])));
397 function facilityNPI() {
398 return x12clean(trim($this->facility['facility_npi']));
401 function facilityPOS() {
402 return x12clean(trim($this->facility['pos_code']));
405 function clearingHouseName() {
406 return x12clean(trim($this->x12_partner['name']));
409 function clearingHouseETIN() {
410 return x12clean(trim(str_replace('-', '', $this->x12_partner['id_number'])));
413 function providerNumberType() {
414 return $this->insurance_numbers['provider_number_type'];
417 function providerNumber() {
418 return x12clean(trim(str_replace('-', '', $this->insurance_numbers['provider_number'])));
421 // Returns 'P', 'S' or 'T'.
423 function payerSequence($ins=0) {
424 return strtoupper(substr($this->payers[$ins]['data']['type'], 0, 1));
427 // Returns the HIPAA code of the patient-to-subscriber relationship.
429 function insuredRelationship($ins=0) {
430 $tmp = strtolower($this->payers[$ins]['data']['subscriber_relationship']);
431 if ($tmp == 'self' ) return '18';
432 if ($tmp == 'spouse') return '01';
433 if ($tmp == 'child' ) return '19';
434 if ($tmp == 'other' ) return 'G8';
435 return $tmp; // should not happen
438 function insuredTypeCode($ins=0) {
439 if ($this->claimType($ins) == 'MB' && $this->payerSequence($ins) != 'P')
440 return '12'; // medicare secondary working aged beneficiary or
441 // spouse with employer group health plan
442 return '';
445 // Is the patient also the subscriber?
447 function isSelfOfInsured($ins=0) {
448 $tmp = strtolower($this->payers[$ins]['data']['subscriber_relationship']);
449 return ($tmp == 'self');
452 function groupNumber($ins=0) {
453 return x12clean(trim($this->payers[$ins]['data']['group_number']));
456 function groupName($ins=0) {
457 return x12clean(trim($this->payers[$ins]['data']['subscriber_employer']));
460 function claimType($ins=0) {
461 return $this->payers[$ins]['object']->get_freeb_claim_type();
464 function insuredLastName($ins=0) {
465 return x12clean(trim($this->payers[$ins]['data']['subscriber_lname']));
468 function insuredFirstName($ins=0) {
469 return x12clean(trim($this->payers[$ins]['data']['subscriber_fname']));
472 function insuredMiddleName($ins=0) {
473 return x12clean(trim($this->payers[$ins]['data']['subscriber_mname']));
476 function policyNumber($ins=0) { // "ID"
477 return x12clean(trim($this->payers[$ins]['data']['policy_number']));
480 function insuredStreet($ins=0) {
481 return x12clean(trim($this->payers[$ins]['data']['subscriber_street']));
484 function insuredCity($ins=0) {
485 return x12clean(trim($this->payers[$ins]['data']['subscriber_city']));
488 function insuredState($ins=0) {
489 return x12clean(trim($this->payers[$ins]['data']['subscriber_state']));
492 function insuredZip($ins=0) {
493 return x12clean(trim($this->payers[$ins]['data']['subscriber_postal_code']));
496 function insuredDOB($ins=0) {
497 return str_replace('-', '', $this->payers[$ins]['data']['subscriber_DOB']);
500 function insuredSex($ins=0) {
501 return strtoupper(substr($this->payers[$ins]['data']['subscriber_sex'], 0, 1));
504 function payerName($ins=0) {
505 return x12clean(trim($this->payers[$ins]['company']['name']));
508 function payerStreet($ins=0) {
509 $tmp = $this->payers[$ins]['object'];
510 $tmp = $tmp->get_address();
511 return x12clean(trim($tmp->get_line1()));
514 function payerCity($ins=0) {
515 $tmp = $this->payers[$ins]['object'];
516 $tmp = $tmp->get_address();
517 return x12clean(trim($tmp->get_city()));
520 function payerState($ins=0) {
521 $tmp = $this->payers[$ins]['object'];
522 $tmp = $tmp->get_address();
523 return x12clean(trim($tmp->get_state()));
526 function payerZip($ins=0) {
527 $tmp = $this->payers[$ins]['object'];
528 $tmp = $tmp->get_address();
529 return x12clean(trim($tmp->get_zip()));
532 function payerID($ins=0) {
533 return x12clean(trim($this->payers[$ins]['company']['cms_id']));
536 function patientLastName() {
537 return x12clean(trim($this->patient_data['lname']));
540 function patientFirstName() {
541 return x12clean(trim($this->patient_data['fname']));
544 function patientMiddleName() {
545 return x12clean(trim($this->patient_data['mname']));
548 function patientStreet() {
549 return x12clean(trim($this->patient_data['street']));
552 function patientCity() {
553 return x12clean(trim($this->patient_data['city']));
556 function patientState() {
557 return x12clean(trim($this->patient_data['state']));
560 function patientZip() {
561 return x12clean(trim($this->patient_data['postal_code']));
564 function patientDOB() {
565 return str_replace('-', '', $this->patient_data['DOB']);
568 function patientSex() {
569 return strtoupper(substr($this->patient_data['sex'], 0, 1));
572 function cptCode($prockey) {
573 return x12clean(trim($this->procs[$prockey]['code']));
576 function cptModifier($prockey) {
577 return x12clean(trim($this->procs[$prockey]['modifier']));
580 // Returns the procedure code, followed by ":modifier" if there is one.
581 function cptKey($prockey) {
582 $tmp = $this->cptModifier($prockey);
583 return $this->cptCode($prockey) . ($tmp ? ":$tmp" : "");
586 function cptCharges($prockey) {
587 return x12clean(trim($this->procs[$prockey]['fee']));
590 function cptUnits($prockey) {
591 if (empty($this->procs[$prockey]['units'])) return '1';
592 return x12clean(trim($this->procs[$prockey]['units']));
595 // NDC drug ID.
596 function cptNDCID($prockey) {
597 $ndcinfo = $this->procs[$prockey]['ndc_info'];
598 if (preg_match('/^N4(\S+)\s+(\S\S)(.*)/', $ndcinfo, $tmp)) {
599 $ndc = $tmp[1];
600 if (preg_match('/^(\d+)-(\d+)-(\d+)$/', $ndc, $tmp)) {
601 return sprintf('%05d-%04d-%02d', $tmp[1], $tmp[2], $tmp[3]);
603 return x12clean($ndc); // format is bad but return it anyway
605 return '';
608 // NDC drug unit of measure code.
609 function cptNDCUOM($prockey) {
610 $ndcinfo = $this->procs[$prockey]['ndc_info'];
611 if (preg_match('/^N4(\S+)\s+(\S\S)(.*)/', $ndcinfo, $tmp))
612 return x12clean($tmp[2]);
613 return '';
616 // NDC drug number of units.
617 function cptNDCQuantity($prockey) {
618 $ndcinfo = $this->procs[$prockey]['ndc_info'];
619 if (preg_match('/^N4(\S+)\s+(\S\S)(.*)/', $ndcinfo, $tmp))
620 return x12clean($tmp[3]);
621 return '';
624 function onsetDate() {
625 return str_replace('-', '', substr($this->encounter['onset_date'], 0, 10));
628 function serviceDate() {
629 return str_replace('-', '', substr($this->encounter['date'], 0, 10));
632 function priorAuth() {
633 return x12clean(trim($this->billing_options['prior_auth_number']));
636 // Returns an array of unique primary diagnoses. Periods are stripped.
637 function diagArray() {
638 $da = array();
639 foreach ($this->procs as $row) {
640 $tmp = explode(':', $row['justify']);
641 if (count($tmp)) {
642 $diag = str_replace('.', '', $tmp[0]);
643 $da[$diag] = $diag;
646 return $da;
649 // Compute the 1-relative index in diagArray for the given procedure.
650 function diagIndex($prockey) {
651 $da = $this->diagArray();
652 $tmp = explode(':', $this->procs[$prockey]['justify']);
653 if (empty($tmp)) return '';
654 $diag = str_replace('.', '', $tmp[0]);
655 $i = 0;
656 foreach ($da as $value) {
657 ++$i;
658 if ($value == $diag) return $i;
660 return '';
663 function providerLastName() {
664 return x12clean(trim($this->provider['lname']));
667 function providerFirstName() {
668 return x12clean(trim($this->provider['fname']));
671 function providerMiddleName() {
672 return x12clean(trim($this->provider['mname']));
675 function providerNPI() {
676 return x12clean(trim($this->provider['npi']));
679 function providerUPIN() {
680 return x12clean(trim($this->provider['upin']));
683 function providerSSN() {
684 return x12clean(trim(str_replace('-', '', $this->provider['federaltaxid'])));
687 function referrerLastName() {
688 return x12clean(trim($this->referrer['lname']));
691 function referrerFirstName() {
692 return x12clean(trim($this->referrer['fname']));
695 function referrerMiddleName() {
696 return x12clean(trim($this->referrer['mname']));
699 function referrerNPI() {
700 return x12clean(trim($this->referrer['npi']));
703 function referrerUPIN() {
704 return x12clean(trim($this->referrer['upin']));
707 function referrerSSN() {
708 return x12clean(trim(str_replace('-', '', $this->referrer['federaltaxid'])));