updated US CDC website link to current immunization VIS page (#7855)
[openemr.git] / src / Services / InsuranceService.php
blob5192b24a8bf5c20e424cf5ed6883795450165112
1 <?php
3 /**
4 * InsuranceService - Service class for patient insurance policy (coverage) data
6 * @package OpenEMR
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\{
24 CompositeSearchField,
25 DateSearchField,
26 FhirSearchWhereClauseBuilder,
27 SearchModifier,
28 TokenSearchField,
29 TokenSearchValue,
31 use OpenEMR\Validators\{
32 CoverageValidator,
33 ProcessingResult,
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";
41 /**
42 * @var CoverageValidator $coverageValidator
44 private $coverageValidator;
47 /**
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`.*,
77 `puuid`,
78 `insureruuid`
79 FROM `insurance_data`
80 LEFT JOIN (
81 SELECT
82 `uuid` AS `insureruuid`,
83 `id` AS `insurerid`
84 FROM `insurance_companies`
85 ) `insurance_company_data` ON `insurance_data`.`provider` = `insurance_company_data`.`insurerid`
86 LEFT JOIN (
87 SELECT
88 `pid` AS `patient_data_pid`,
89 `uuid` AS `puuid`
90 FROM `patient_data`
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) {
119 return $isValid;
121 $uuidBytes = UuidRegistry::uuidToBytes($uuid);
122 $sql = "SELECT * FROM insurance_data WHERE uuid=? ";
124 $sqlResult = sqlQuery($sql, array($uuidBytes));
125 if ($sqlResult) {
126 $sqlResult['uuid'] = UuidRegistry::uuidToString($sqlResult['uuid']);
127 $processingResult->addData($sqlResult);
128 } else {
129 $processingResult->addInternalError("error processing SQL");
131 return $processingResult;
135 * @deprecated use search instead
136 * @param $search
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(
147 'uuid',
148 self::PATIENT_TABLE,
149 $search['pid'],
150 true
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(
161 'uuid',
162 self::INSURANCE_TABLE,
163 $search['provider'],
164 true
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(
176 'uuid',
177 self::COVERAGE_TABLE,
178 $search['id'],
179 true
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)) {
190 $sql .= ' WHERE ';
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 = '')
218 if (!empty($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=?,";
256 $sql .= " copay=?,";
257 $sql .= " date=?,";
258 $sql .= " date_end=?,";
259 $sql .= " subscriber_sex=?,";
260 $sql .= " accept_assignment=?,";
261 $sql .= " policy_type=?,";
262 $sql .= " 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(
272 $sql,
273 array(
274 $data["provider"],
275 $data["plan_name"],
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"],
296 $data["copay"],
297 $data["date"],
298 empty($data["date_end"]) ? null : $data["date_end"],
299 $data["subscriber_sex"],
300 $data["accept_assignment"],
301 $data["policy_type"],
302 $data['type'],
303 $uuid
306 if ($results) {
307 $serviceSavePostEvent = new ServiceSaveEvent($this, $data);
308 $this->getEventDispatcher()->dispatch($serviceSavePostEvent, ServiceSaveEvent::EVENT_POST_SAVE);
309 $processingResult = $this->getOne($data['uuid']);
310 } else {
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 ";
326 $sql .= " uuid=?,";
327 $sql .= " type=?,";
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=?,";
350 $sql .= " copay=?,";
351 $sql .= " date=?,";
352 $sql .= " pid=?,";
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(
362 $sql,
363 array(
364 $data['uuid'],
365 $data['type'],
366 $data["provider"],
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"] ?? '',
389 $data["date"] ?? '',
390 $data['pid'],
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);
407 } else {
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,
439 'type' => $type,
440 'date' => $composite,
443 if ($insuranceDataResult->hasData()) {
444 $result = $insuranceDataResult->getData();
445 } else {
446 $result = [];
449 return $result;
452 public function getPoliciesOrganizedByTypeForPatientPid($pid)
454 $insurancePolicies = $this->search(['pid' => $pid]);
455 $result = [];
456 foreach ($insurancePolicies->getData() as $insurancePolicy) {
457 if (empty($insurancePolicy['type'])) {
458 $result[$insurancePolicy['type']] = [];
459 continue;
461 $result[$insurancePolicy['type']][] = $insurancePolicy;
463 $organizedResults = [];
464 foreach ($result as $key => $policies) {
465 if (count($policies) > 0) {
466 reset($policies);
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();
489 try {
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;
531 } else {
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;
558 $result = [
559 'src' => $srcInsurance
560 ,'target' => $targetInsurance
562 $processingResult->addData($result);
563 } catch (\Exception $e) {
564 $processingResult->addInternalError($e->getMessage());
565 } finally {
566 try {
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;