4 * InsuranceService - Service class for patient insurance policy (coverage) data
7 * @link http://www.open-emr.org
8 * @author Matthew Vita <matthewvita48@gmail.com>
9 * @author Brady Miller <brady.g.miller@gmail.com>
10 * @author Stephen Nielson <snielson@discoverandchange.com>
11 * @copyright Copyright (c) 2018 Matthew Vita <matthewvita48@gmail.com>
12 * @copyright Copyright (c) 2018 Brady Miller <brady.g.miller@gmail.com>
13 * @copyright Copyright (c) 2024 Care Management Solutions, Inc. <stephen.waite@cmsvt.com>
14 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
17 namespace OpenEMR\Services
;
19 use OpenEMR\Common\Database\QueryUtils
;
20 use OpenEMR\Common\Logging\SystemLogger
;
21 use OpenEMR\Common\Uuid\UuidRegistry
;
22 use OpenEMR\Events\Services\ServiceSaveEvent
;
23 use OpenEMR\Services\Search\
{
26 FhirSearchWhereClauseBuilder
,
31 use OpenEMR\Validators\
{
36 class InsuranceService
extends BaseService
38 private const COVERAGE_TABLE
= "insurance_data";
39 private const PATIENT_TABLE
= "patient_data";
40 private const INSURANCE_TABLE
= "insurance_companies";
42 * @var CoverageValidator $coverageValidator
44 private $coverageValidator;
48 * Default constructor.
50 public function __construct()
52 parent
::__construct(self
::COVERAGE_TABLE
);
53 // TODO: we need to migrate the addresses in these tables into the Address table
54 UuidRegistry
::createMissingUuidsForTables([self
::COVERAGE_TABLE
, self
::PATIENT_TABLE
, self
::INSURANCE_TABLE
]);
55 $this->coverageValidator
= new CoverageValidator();
58 public function getUuidFields(): array
60 return ['uuid', 'puuid', 'insureruuid'];
63 public function validate($data)
65 return $this->coverageValidator
->validate($data);
68 public function getOneByPid($id, $type)
70 $sql = "SELECT * FROM insurance_data WHERE pid=? AND type=?";
71 return sqlQuery($sql, array($id, $type));
74 public function search($search, $isAndCondition = true)
76 $sql = "SELECT `insurance_data`.*,
82 `uuid` AS `insureruuid`,
84 FROM `insurance_companies`
85 ) `insurance_company_data` ON `insurance_data`.`provider` = `insurance_company_data`.`insurerid`
88 `pid` AS `patient_data_pid`,
91 ) `patient_data` ON `insurance_data`.`pid` = `patient_data`.`patient_data_pid` ";
93 $whereClause = FhirSearchWhereClauseBuilder
::build($search, $isAndCondition);
95 $orderClause = " ORDER BY `patient_data_pid` ASC,`type` ASC"
96 // sort by 1 first then 0
97 . ", (`date_end` is null or `date_end` > NOW()) DESC"
98 . ", (`date_end` IS NOT NULL AND `date_end` > NOW()) DESC"
99 . ", `date` DESC, `date_end` DESC, `policy_number` ASC";
101 $sql .= $whereClause->getFragment() . $orderClause;
102 $sqlBindArray = $whereClause->getBoundValues();
103 $statementResults = QueryUtils
::sqlStatementThrowException($sql, $sqlBindArray);
105 $processingResult = new ProcessingResult();
106 while ($row = sqlFetchArray($statementResults)) {
107 $resultRecord = $this->createResultRecordFromDatabaseResult($row);
108 $processingResult->addData($resultRecord);
110 return $processingResult;
113 public function getOne($uuid)
116 $processingResult = new ProcessingResult();
117 $isValid = $this->coverageValidator
->validateId('uuid', self
::COVERAGE_TABLE
, $uuid, true);
118 if ($isValid !== true) {
121 $uuidBytes = UuidRegistry
::uuidToBytes($uuid);
122 $sql = "SELECT * FROM insurance_data WHERE uuid=? ";
124 $sqlResult = sqlQuery($sql, array($uuidBytes));
126 $sqlResult['uuid'] = UuidRegistry
::uuidToString($sqlResult['uuid']);
127 $processingResult->addData($sqlResult);
129 $processingResult->addInternalError("error processing SQL");
131 return $processingResult;
135 * @deprecated use search instead
137 * @param $isAndCondition
138 * @return ProcessingResult|true
140 public function getAll($search = array(), $isAndCondition = true)
143 // Validating and Converting Patient UUID to PID
144 // Validating and Converting UUID to ID
145 if (isset($search['pid'])) {
146 $isValidcondition = $this->coverageValidator
->validateId(
152 if ($isValidcondition !== true) {
153 return $isValidcondition;
155 $puuidBytes = UuidRegistry
::uuidToBytes($search['pid']);
156 $search['pid'] = $this->getIdByUuid($puuidBytes, self
::PATIENT_TABLE
, "pid");
158 // Validating and Converting Payor UUID to provider
159 if (isset($search['provider'])) {
160 $isValidcondition = $this->coverageValidator
->validateId(
162 self
::INSURANCE_TABLE
,
166 if ($isValidcondition !== true) {
167 return $isValidcondition;
169 $uuidBytes = UuidRegistry
::uuidToBytes($search['provider']);
170 $search['provider'] = $this->getIdByUuid($uuidBytes, self
::INSURANCE_TABLE
, "provider");
173 // Validating and Converting UUID to ID
174 if (isset($search['id'])) {
175 $isValidcondition = $this->coverageValidator
->validateId(
177 self
::COVERAGE_TABLE
,
181 if ($isValidcondition !== true) {
182 return $isValidcondition;
184 $uuidBytes = UuidRegistry
::uuidToBytes($search['id']);
185 $search['id'] = $this->getIdByUuid($uuidBytes, self
::COVERAGE_TABLE
, "id");
187 $sqlBindArray = array();
188 $sql = "SELECT * FROM insurance_data ";
189 if (!empty($search)) {
191 $whereClauses = array();
192 foreach ($search as $fieldName => $fieldValue) {
193 array_push($whereClauses, $fieldName . ' = ?');
194 array_push($sqlBindArray, $fieldValue);
196 $sqlCondition = ($isAndCondition == true) ?
'AND' : 'OR';
197 $sql .= implode(' ' . $sqlCondition . ' ', $whereClauses);
199 $statementResults = sqlStatement($sql, $sqlBindArray);
201 $processingResult = new ProcessingResult();
202 while ($row = sqlFetchArray($statementResults)) {
203 $row['uuid'] = UuidRegistry
::uuidToString($row['uuid']);
204 $patientuuidBytes = $this->getUuidById($row['pid'], self
::PATIENT_TABLE
, "id");
205 $row['puuid'] = UuidRegistry
::uuidToString($patientuuidBytes);
206 $insureruuidBytes = $this->getUuidById($row['provider'], self
::INSURANCE_TABLE
, "id");
207 //When No provider data is available
208 if (strlen($insureruuidBytes) > 0) {
209 $row['insureruuid'] = UuidRegistry
::uuidToString($insureruuidBytes);
210 $processingResult->addData($row);
213 return $processingResult;
216 public function doesInsuranceTypeHaveEntry($pid, $type = '')
219 return sqlQuery("Select `id` From `insurance_data` Where pid = ? And type = ?", [$pid, $type])['id'] ??
null;
221 return $this->getOne($pid, $type) !== false;
224 public function update($data)
226 $validationResult = $this->coverageValidator
->validate($data, CoverageValidator
::DATABASE_UPDATE_CONTEXT
);
227 if (!$validationResult->isValid()) {
228 return $validationResult;
231 $processingResult = new ProcessingResult();
233 $sql = " UPDATE insurance_data SET ";
234 $sql .= " provider=?,";
235 $sql .= " plan_name=?,";
236 $sql .= " policy_number=?,";
237 $sql .= " group_number=?,";
238 $sql .= " subscriber_lname=?,";
239 $sql .= " subscriber_mname=?,";
240 $sql .= " subscriber_fname=?,";
241 $sql .= " subscriber_relationship=?,";
242 $sql .= " subscriber_ss=?,";
243 $sql .= " subscriber_DOB=?,";
244 $sql .= " subscriber_street=?,";
245 $sql .= " subscriber_postal_code=?,";
246 $sql .= " subscriber_city=?,";
247 $sql .= " subscriber_state=?,";
248 $sql .= " subscriber_country=?,";
249 $sql .= " subscriber_phone=?,";
250 $sql .= " subscriber_employer=?,";
251 $sql .= " subscriber_employer_street=?,";
252 $sql .= " subscriber_employer_postal_code=?,";
253 $sql .= " subscriber_employer_state=?,";
254 $sql .= " subscriber_employer_country=?,";
255 $sql .= " subscriber_employer_city=?,";
258 $sql .= " date_end=?,";
259 $sql .= " subscriber_sex=?,";
260 $sql .= " accept_assignment=?,";
261 $sql .= " policy_type=?,";
263 $sql .= " WHERE uuid = ? ";
265 $serviceSaveEvent = new ServiceSaveEvent($this, $data);
266 $this->getEventDispatcher()->dispatch($serviceSaveEvent, ServiceSaveEvent
::EVENT_PRE_SAVE
);
267 $data = $serviceSaveEvent->getSaveData();
268 $uuid = UuidRegistry
::uuidToBytes($data['uuid']);
271 $results = sqlStatement(
276 $data["policy_number"],
277 $data["group_number"],
278 $data["subscriber_lname"],
279 $data["subscriber_mname"],
280 $data["subscriber_fname"],
281 $data["subscriber_relationship"],
282 $data["subscriber_ss"],
283 $data["subscriber_DOB"],
284 $data["subscriber_street"],
285 $data["subscriber_postal_code"],
286 $data["subscriber_city"],
287 $data["subscriber_state"],
288 $data["subscriber_country"],
289 $data["subscriber_phone"],
290 $data["subscriber_employer"],
291 $data["subscriber_employer_street"],
292 $data["subscriber_employer_postal_code"],
293 $data["subscriber_employer_state"],
294 $data["subscriber_employer_country"],
295 $data["subscriber_employer_city"],
298 empty($data["date_end"]) ?
null : $data["date_end"],
299 $data["subscriber_sex"],
300 $data["accept_assignment"],
301 $data["policy_type"],
307 $serviceSavePostEvent = new ServiceSaveEvent($this, $data);
308 $this->getEventDispatcher()->dispatch($serviceSavePostEvent, ServiceSaveEvent
::EVENT_POST_SAVE
);
309 $processingResult = $this->getOne($data['uuid']);
311 $processingResult->addProcessingError("error processing SQL Update");
313 return $processingResult;
316 public function insert($data): ProcessingResult
318 $validationResult = $this->coverageValidator
->validate($data, CoverageValidator
::DATABASE_INSERT_CONTEXT
);
319 if (!$validationResult->isValid()) {
320 return $validationResult;
323 $data['uuid'] = UuidRegistry
::getRegistryForTable(self
::COVERAGE_TABLE
)->createUuid();
325 $sql = " INSERT INTO insurance_data SET ";
328 $sql .= " provider=?,";
329 $sql .= " plan_name=?,";
330 $sql .= " policy_number=?,";
331 $sql .= " group_number=?,";
332 $sql .= " subscriber_lname=?,";
333 $sql .= " subscriber_mname=?,";
334 $sql .= " subscriber_fname=?,";
335 $sql .= " subscriber_relationship=?,";
336 $sql .= " subscriber_ss=?,";
337 $sql .= " subscriber_DOB=?,";
338 $sql .= " subscriber_street=?,";
339 $sql .= " subscriber_postal_code=?,";
340 $sql .= " subscriber_city=?,";
341 $sql .= " subscriber_state=?,";
342 $sql .= " subscriber_country=?,";
343 $sql .= " subscriber_phone=?,";
344 $sql .= " subscriber_employer=?,";
345 $sql .= " subscriber_employer_street=?,";
346 $sql .= " subscriber_employer_postal_code=?,";
347 $sql .= " subscriber_employer_state=?,";
348 $sql .= " subscriber_employer_country=?,";
349 $sql .= " subscriber_employer_city=?,";
353 $sql .= " subscriber_sex=?,";
354 $sql .= " accept_assignment=?,";
355 $sql .= " policy_type=?";
357 $serviceSaveEvent = new ServiceSaveEvent($this, $data);
358 $dispatchedEvent = $this->getEventDispatcher()->dispatch($serviceSaveEvent, ServiceSaveEvent
::EVENT_PRE_SAVE
);
359 $data = $dispatchedEvent->getSaveData();
361 $insuranceDataId = sqlInsert(
367 $data["plan_name"] ??
'',
368 $data["policy_number"] ??
'',
369 $data["group_number"] ??
'',
370 $data["subscriber_lname"] ??
'',
371 $data["subscriber_mname"] ??
'',
372 $data["subscriber_fname"] ??
'',
373 $data["subscriber_relationship"] ??
'',
374 $data["subscriber_ss"] ??
'',
375 $data["subscriber_DOB"] ??
'',
376 $data["subscriber_street"] ??
'',
377 $data["subscriber_postal_code"] ??
'',
378 $data["subscriber_city"] ??
'',
379 $data["subscriber_state"] ??
'',
380 $data["subscriber_country"] ??
'',
381 $data["subscriber_phone"] ??
'',
382 $data["subscriber_employer"] ??
'',
383 $data["subscriber_employer_street"] ??
'',
384 $data["subscriber_employer_postal_code"] ??
'',
385 $data["subscriber_employer_state"] ??
'',
386 $data["subscriber_employer_country"] ??
'',
387 $data["subscriber_employer_city"] ??
'',
388 $data["copay"] ??
'',
391 $data["subscriber_sex"] ??
'',
392 $data["accept_assignment"] ??
'',
393 $data["policy_type"] ??
''
396 // I prefer exceptions... but we will try to match other service handler formats for consistency
397 $processingResult = new ProcessingResult();
398 $stringUuid = UuidRegistry
::uuidToString($data['uuid']);
399 if ($insuranceDataId) {
400 $data['id'] = $insuranceDataId;
401 $processingResult->addData([
402 'id' => $insuranceDataId
403 ,'uuid' => $stringUuid
405 $this->getEventDispatcher()->dispatch($serviceSaveEvent, ServiceSaveEvent
::EVENT_POST_SAVE
);
406 $processingResult = $this->getOne($stringUuid);
408 $processingResult->addProcessingError("error processing SQL Update");
411 return $processingResult;
415 * Return an array of encounters within a date range
417 * @param $start_date Any encounter starting on this date
418 * @param $end_date Any encounter ending on this date
419 * @return Array Encounter data payload.
421 public function getPidsForPayerByEffectiveDate($provider, $type, $startDate, $endDate)
423 // most common case of null in 'date' field aka effective date which signifies is only insurance of that type
424 // TBD: add another token for 'date_end' field
425 $dateMissing = new TokenSearchField('date', [new TokenSearchValue(null)]);
426 $dateMissing->setModifier(SearchModifier
::MISSING
);
428 // search for encounters by passed in start and end dates
429 $dateField = new DateSearchField('date', ['ge' . $startDate, 'le' . $endDate], DateSearchField
::DATE_TYPE_DATE
);
431 // set up composite search with false signifying an OR condition for the effective date
432 $composite = new CompositeSearchField('date', [], false);
433 $composite->addChild($dateMissing);
434 $composite->addChild($dateField);
436 $insuranceDataResult = $this->search(
438 'provider' => $provider,
440 'date' => $composite,
443 if ($insuranceDataResult->hasData()) {
444 $result = $insuranceDataResult->getData();
452 public function getPoliciesOrganizedByTypeForPatientPid($pid)
454 $insurancePolicies = $this->search(['pid' => $pid]);
456 foreach ($insurancePolicies->getData() as $insurancePolicy) {
457 if (empty($insurancePolicy['type'])) {
458 $result[$insurancePolicy['type']] = [];
461 $result[$insurancePolicy['type']][] = $insurancePolicy;
463 $organizedResults = [];
464 foreach ($result as $key => $policies) {
465 if (count($policies) > 0) {
467 $current = current($policies);
468 $history = array_splice($policies, 1);
470 $organizedResults[$key] = [
471 'current' => $current
472 ,'history' => $history // we want in descending order
476 return $organizedResults;
479 public function swapInsurance($pid, string $targetType, string $insuranceUuid)
481 $transactionCommitted = false;
482 $validateData = ['pid' => $pid, 'type' => $targetType, 'uuid' => $insuranceUuid];
483 $validationResult = $this->coverageValidator
->validate($validateData, CoverageValidator
::DATABASE_SWAP_CONTEXT
);
484 if (!$validationResult->isValid()) {
485 return $validationResult;
487 $processingResult = new ProcessingResult();
490 QueryUtils
::startTransaction();
492 $targetUuid = QueryUtils
::fetchSingleValue("SELECT uuid FROM insurance_data WHERE pid = ? AND type = ? ORDER BY (date IS NULL) ASC, date DESC", 'uuid', [$pid, $targetType]);
493 $targetInsurance = null;
494 if (!empty($targetUuid)) {
495 $targetResult = $this->getOne(UuidRegistry
::uuidToString($targetUuid));
496 if ($targetResult->hasData()) {
497 $targetInsurance = $targetResult->getFirstDataResult();
498 if (!$this->coverageValidator
->validate($targetInsurance, CoverageValidator
::DATABASE_UPDATE_CONTEXT
)->isValid()) {
499 $processingResult->setValidationMessages(
501 'type' => ['Record::TARGET_INSURANCE_UPDATE_PROHIBITED' => xl('Target insurance could not be saved as it was missing data required for database updates')]
504 // note the finally clause will rollback the transaction
505 return $processingResult;
509 $srcResult = $this->getOne($insuranceUuid);
510 if (!$srcResult->hasData()) {
511 // shouldn't happen as the validator should have caught this
512 throw new \
InvalidArgumentException("Could not find insurance policy with uuid: $insuranceUuid");
514 $srcInsurance = $srcResult->getFirstDataResult();
515 if (!$this->coverageValidator
->validate($srcInsurance, CoverageValidator
::DATABASE_UPDATE_CONTEXT
)->isValid()) {
516 $processingResult->setValidationMessages(
518 'type' => ['Record::SOURCE_INSURANCE_UPDATE_PROHIBITED' => xl('Source insurance could not be saved as it was missing data required for database updates')]
521 // note the finally clause will rollback the transaction
522 return $processingResult;
525 // we need to look at changing up the date of the current insurance policy
526 $resetStartDate = null;
527 if (!empty($targetInsurance)) {
528 $resetStartDate = $targetInsurance['date'];
529 if ($resetStartDate != $srcInsurance['date']) {
530 $resetStartDate = null;
532 // set to the largest possible date we can which should not conflict with any possible date
533 // I don't like this but due to the pid-type-date db constraint we have to set a temporary date so we don't conflict
534 // with the current insurance policy, the chances of conflict are infitisemally small.
535 // If OpenEMR is still around in 7K+ years, someone should have fixed this by then.
536 // again since its all wrapped in a transaction, this date should never permanently save
537 $targetInsurance["date"] = "9999-12-31";
539 $targetInsurance['type'] = $srcInsurance['type'];
540 $updateResult = $this->update($targetInsurance);
541 if ($updateResult->hasErrors()) {
542 throw new \
InvalidArgumentException("Failed to update insurance policy with uuid: $insuranceUuid");
546 // we have to do this in multiple steps due to the way the db constraing on the type and date are set
547 $srcInsurance['type'] = $targetType;
548 $this->update($srcInsurance);
550 if (!empty($targetInsurance)) {
551 if (!empty($resetStartDate)) {
552 $targetInsurance["date"] = $resetStartDate;
554 $this->update($targetInsurance);
556 QueryUtils
::commitTransaction();
557 $transactionCommitted = true;
559 'src' => $srcInsurance
560 ,'target' => $targetInsurance
562 $processingResult->addData($result);
563 } catch (\Exception
$e) {
564 $processingResult->addInternalError($e->getMessage());
567 if (!$transactionCommitted) {
568 QueryUtils
::rollbackTransaction();
570 } catch (\Exception
$e) {
571 (new SystemLogger())->errorLogCaller(
572 "Failed to rollback transaction " . $e->getMessage(),
573 ['type' => $targetType, 'insuranceUuid' => $insuranceUuid, 'pid' => $pid]
577 return $processingResult;