Bug Fix - default pos code (#6953)
[openemr.git] / src / Services / EncounterService.php
blob9401d5281750b8f4574b50e7300e81e7106bc612
1 <?php
3 /**
4 * EncounterService
6 * @package OpenEMR
7 * @link http://www.open-emr.org
8 * @author Matthew Vita <matthewvita48@gmail.com>
9 * @author Jerry Padgett <sjpadgett@gmail.com>
10 * @author Brady Miller <brady.g.miller@gmail.com>
11 * @copyright Copyright (c) 2018 Matthew Vita <matthewvita48@gmail.com>
12 * @copyright Copyright (c) 2018 Jerry Padgett <sjpadgett@gmail.com>
13 * @copyright Copyright (c) 2018 Brady Miller <brady.g.miller@gmail.com>
14 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
17 namespace OpenEMR\Services;
19 use OpenEMR\Common\Acl\AclMain;
20 use OpenEMR\Common\Database\QueryUtils;
21 use OpenEMR\Common\Database\SqlQueryException;
22 use OpenEMR\Common\Logging\SystemLogger;
23 use OpenEMR\Common\Uuid\UuidRegistry;
24 use OpenEMR\Services\Search\{
25 DateSearchField,
26 FhirSearchWhereClauseBuilder,
27 SearchFieldException,
28 TokenSearchField,
29 TokenSearchValue
31 use OpenEMR\Validators\EncounterValidator;
32 use OpenEMR\Validators\ProcessingResult;
33 use Particle\Validator\Validator;
35 require_once dirname(__FILE__) . "/../../library/forms.inc.php";
36 require_once dirname(__FILE__) . "/../../library/encounter.inc.php";
38 class EncounterService extends BaseService
40 /**
41 * @var EncounterValidator
43 private $encounterValidator;
45 public const ENCOUNTER_TABLE = "form_encounter";
46 private const PATIENT_TABLE = "patient_data";
47 private const PROVIDER_TABLE = "users";
48 private const FACILITY_TABLE = "facility";
50 /**
51 * Default class_code from list_options. Defaults to outpatient ambulatory care.
53 const DEFAULT_CLASS_CODE = 'AMB';
55 /**
56 * Default constructor.
58 public function __construct()
60 parent::__construct('form_encounter');
61 UuidRegistry::createMissingUuidsForTables([self::ENCOUNTER_TABLE, self::PATIENT_TABLE, self::PROVIDER_TABLE,
62 self::FACILITY_TABLE]);
63 $this->encounterValidator = new EncounterValidator();
66 /**
67 * Returns a list of encounters matching the encounter identifier.
69 * @param $eid The encounter identifier of particular encounter
70 * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid.
71 * @return ProcessingResult which contains validation messages, internal error messages, and the data
72 * payload.
74 public function getEncounterById($eid, $puuidBind = null)
76 $search = ['eid' => new TokenSearchField('eid', [new TokenSearchValue($eid)])];
77 return $this->search($search, true, $puuidBind);
80 /**
81 * Returns a list of encounters matching the encounter identifier.
83 * @param $euuid The encounter identifier of particular encounter
84 * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid.
85 * @return ProcessingResult which contains validation messages, internal error messages, and the data
86 * payload.
88 public function getEncounter($euuid, $puuidBind = null)
90 $search = ['euuid' => new TokenSearchField('euuid', [new TokenSearchValue($euuid, null, true)])];
91 return $this->search($search, true, $puuidBind);
94 /**
95 * Returns an encounter matching the patient and encounter identifier.
97 * @param $pid The legacy identifier of particular patient
98 * @param $encounter_id The identifier of a particular encounter
99 * @return array first row of encounter data
101 public function getOneByPidEid($pid, $encounter_id)
103 $encounterResult = $this->search(['pid' => $pid, 'eid' => $encounter_id], $options = ['limit' => '1']);
104 if ($encounterResult->hasData()) {
105 return $encounterResult->getData()[0];
107 return [];
110 public function getUuidFields(): array
112 return ['provider_uuid', 'facility_uuid', 'euuid', 'puuid', 'billing_facility_uuid'
113 , 'facility_location_uuid', 'billing_location_uuid', 'referrer_uuid'];
117 * Given a patient pid return the most recent patient encounter for that patient
118 * @param $pid The unique public id (pid) for the patient.
119 * @return array|null Returns the encounter if found, null otherwise
121 public function getMostRecentEncounterForPatient($pid): ?array
123 $pid = new TokenSearchField('pid', [new TokenSearchValue($pid, null)]);
124 // we discovered that most queries were ordering by encounter id which may NOT be the most recent encounter as
125 // an older historical encounter may be entered after a more recent encounter, so ordering be encounter id screws
126 // this up.
127 $result = $this->search(['pid' => $pid], true, '', ['limit' => 1, 'order' => '`date` DESC']);
128 if ($result->hasData()) {
129 return array_pop($result->getData());
131 return null;
135 * Returns a list of encounters matching optional search criteria.
136 * Search criteria is conveyed by array where key = field/column name, value = field value.
137 * If no search criteria is provided, all records are returned.
139 * @param array $search search array parameters
140 * @param bool $isAndCondition specifies if AND condition is used for multiple criteria. Defaults to true.
141 * @param string $puuidBindValue - Optional puuid to only allow visibility of the patient with this puuid.
142 * @param array $options - Optional array of sql clauses like LIMIT, ORDER, etc
143 * @return bool|ProcessingResult|true|null ProcessingResult which contains validation messages, internal error messages, and the data
144 * payload.
146 public function search($search = array(), $isAndCondition = true, $puuidBindValue = '', $options = array())
148 $limit = $options['limit'] ?? null;
149 $sqlBindArray = array();
150 $processingResult = new ProcessingResult();
152 // Validating and Converting _id to UUID byte
153 if (isset($search['uuid'])) {
154 $isValidEncounter = $this->encounterValidator->validateId(
155 'uuid',
156 self::ENCOUNTER_TABLE,
157 $search['uuid'],
158 true
160 if ($isValidEncounter !== true) {
161 return $isValidEncounter;
163 $search['uuid'] = UuidRegistry::uuidToBytes($search['uuid']);
165 // passed in uuid string to bind patient via their uuid.
166 // confusing ...
167 if (!empty($puuidBindValue)) {
168 // code to support patient binding
169 $isValidPatient = $this->encounterValidator->validateId('uuid', self::PATIENT_TABLE, $puuidBindValue, true);
170 if ($isValidPatient !== true) {
171 return $isValidPatient;
173 $pid = $this->getIdByUuid(UuidRegistry::uuidToBytes($puuidBindValue), self::PATIENT_TABLE, "pid");
174 if (empty($pid)) {
175 $processingResult->setValidationMessages("Invalid pid");
176 return $processingResult;
178 $search['puuid'] = new TokenSearchField('puuid', [new TokenSearchValue($puuidBindValue, null, true)]);
181 $sql = "SELECT fe.eid,
182 fe.euuid,
183 fe.date,
184 fe.reason,
185 fe.onset_date,
186 fe.sensitivity,
187 fe.billing_note,
188 fe.pc_catid,
189 fe.last_level_billed,
190 fe.last_level_closed,
191 fe.last_stmt_date,
192 fe.stmt_count,
193 fe.supervisor_id,
194 fe.invoice_refno,
195 fe.referral_source,
196 fe.billing_facility,
197 fe.external_id,
198 fe.pos_code,
199 fe.class_code,
200 class.notes as class_title,
201 opc.pc_catname,
203 patient.pid,
204 patient.puuid,
205 facilities.facility_id,
206 facilities.facility_uuid,
207 facilities.facility_name,
208 facilities.facility_location_uuid,
210 fa.billing_facility_id,
211 fa.billing_facility_uuid,
212 fa.billing_facility_name,
213 fa.billing_location_uuid,
215 fe.provider_id,
216 fe.referring_provider_id,
217 providers.provider_uuid,
218 providers.provider_username,
219 referrers.referrer_uuid,
220 referrers.referrer_username,
221 fe.discharge_disposition,
222 discharge_list.discharge_disposition_text
225 FROM (
226 select
227 encounter as eid,
228 uuid as euuid,
229 `date`,
230 reason,
231 onset_date,
232 sensitivity,
233 billing_note,
234 pc_catid,
235 last_level_billed,
236 last_level_closed,
237 last_stmt_date,
238 stmt_count,
239 provider_id,
240 supervisor_id,
241 invoice_refno,
242 referral_source,
243 billing_facility,
244 external_id,
245 pos_code,
246 class_code,
247 facility_id,
248 discharge_disposition,
249 pid as encounter_pid,
250 referring_provider_id
251 FROM form_encounter
252 ) fe
253 LEFT JOIN openemr_postcalendar_categories as opc
254 ON opc.pc_catid = fe.pc_catid
255 LEFT JOIN list_options as class ON class.option_id = fe.class_code
256 LEFT JOIN (
257 select
258 facility.id AS billing_facility_id
259 ,facility.uuid AS billing_facility_uuid
260 ,facility.`name` AS billing_facility_name
261 ,locations.uuid AS billing_location_uuid
262 from facility
263 LEFT JOIN uuid_mapping AS locations
264 ON locations.target_uuid = facility.uuid AND locations.resource='Location'
265 ) fa ON fa.billing_facility_id = fe.billing_facility
266 LEFT JOIN (
267 select
269 ,uuid AS puuid
270 FROM patient_data
271 ) patient ON fe.encounter_pid = patient.pid
272 LEFT JOIN (
273 select
274 id AS provider_provider_id
275 ,uuid AS provider_uuid
276 ,`username` AS provider_username
277 FROM users
278 WHERE
279 npi IS NOT NULL and npi != ''
280 ) providers ON fe.provider_id = providers.provider_provider_id
281 LEFT JOIN (
282 select
283 id AS referring_provider_id
284 ,uuid AS referrer_uuid
285 ,`username` AS referrer_username
286 FROM users
287 WHERE
288 npi IS NOT NULL and npi != ''
289 ) referrers ON fe.referring_provider_id = referrers.referring_provider_id
290 LEFT JOIN (
291 select
292 facility.id AS facility_id
293 ,facility.uuid AS facility_uuid
294 ,facility.`name` AS facility_name
295 ,`locations`.`uuid` AS facility_location_uuid
296 from facility
297 LEFT JOIN uuid_mapping AS locations
298 ON locations.target_uuid = facility.uuid AND locations.resource='Location'
299 ) facilities ON facilities.facility_id = fe.facility_id
300 LEFT JOIN (
301 select option_id AS discharge_option_id
302 ,title AS discharge_disposition_text
303 FROM list_options
304 WHERE list_id = 'discharge-disposition'
305 ) discharge_list ON fe.discharge_disposition = discharge_list.discharge_option_id";
307 try {
308 $whereFragment = FhirSearchWhereClauseBuilder::build($search, $isAndCondition);
309 $sql .= $whereFragment->getFragment();
311 if (empty($options['order'])) {
312 $sql .= " ORDER BY fe.eid DESC";
313 } else {
314 $sql .= " ORDER BY " . $options['order'];
318 if (is_int($limit) && $limit > 0) {
319 $sql .= " LIMIT " . $limit;
322 $records = QueryUtils::fetchRecords($sql, $whereFragment->getBoundValues());
324 if (!empty($records)) {
325 foreach ($records as $row) {
326 $resultRecord = $this->createResultRecordFromDatabaseResult($row);
327 $processingResult->addData($resultRecord);
330 } catch (SqlQueryException $exception) {
331 // we shouldn't hit a query exception
332 (new SystemLogger())->error($exception->getMessage(), ['trace' => $exception->getTraceAsString()]);
333 $processingResult->addInternalError("Error selecting data from database");
334 } catch (SearchFieldException $exception) {
335 (new SystemLogger())->error($exception->getMessage(), ['trace' => $exception->getTraceAsString(), 'field' => $exception->getField()]);
336 $processingResult->setValidationMessages([$exception->getField() => $exception->getMessage()]);
339 return $processingResult;
343 * Inserts a new Encounter record.
345 * @param $puuid The patient identifier of particular encounter
346 * @param $data The encounter fields (array) to insert.
347 * @return ProcessingResult which contains validation messages, internal error messages, and the data
348 * payload.
350 public function insertEncounter($puuid, $data)
352 $processingResult = new ProcessingResult();
353 $processingResult = $this->encounterValidator->validate(
354 array_merge($data, ["puuid" => $puuid]),
355 EncounterValidator::DATABASE_INSERT_CONTEXT
358 if (!$processingResult->isValid()) {
359 return $processingResult;
362 $encounter = generate_id();
363 $data['encounter'] = $encounter;
364 $data['uuid'] = UuidRegistry::getRegistryForTable(self::ENCOUNTER_TABLE)->createUuid();
365 if (empty($data['date'])) {
366 $data['date'] = date("Y-m-d");
368 $puuidBytes = UuidRegistry::uuidToBytes($puuid);
369 $data['pid'] = $this->getIdByUuid($puuidBytes, self::PATIENT_TABLE, "pid");
370 $query = $this->buildInsertColumns($data);
371 $sql = " INSERT INTO form_encounter SET ";
372 $sql .= $query['set'];
374 $results = sqlInsert(
375 $sql,
376 $query['bind']
379 addForm(
380 $encounter,
381 "New Patient Encounter",
382 $results,
383 "newpatient",
384 $data['pid'],
385 $data["provider_id"],
386 $data["date"],
387 $data['user'],
388 $data['group'],
389 $data['referring_provider_id']
392 if ($results) {
393 $processingResult->addData(array(
394 'encounter' => $encounter,
395 'uuid' => UuidRegistry::uuidToString($data['uuid']),
397 } else {
398 $processingResult->addProcessingError("error processing SQL Insert");
401 return $processingResult;
405 * Updates an existing Encounter record.
407 * @param $puuid The patient identifier of particular encounter.
408 * @param $euuid - The Encounter identifier used for update.
409 * @param $data - The updated Encounter data fields
410 * @return ProcessingResult which contains validation messages, internal error messages, and the data
411 * payload.
413 public function updateEncounter($puuid, $euuid, $data)
415 $processingResult = new ProcessingResult();
416 $processingResult = $this->encounterValidator->validate(
417 array_merge($data, ["puuid" => $puuid, "euuid" => $euuid]),
418 EncounterValidator::DATABASE_UPDATE_CONTEXT
421 if (!$processingResult->isValid()) {
422 return $processingResult;
425 $puuidBytes = UuidRegistry::uuidToBytes($puuid);
426 $euuidBytes = UuidRegistry::uuidToBytes($euuid);
427 $pid = $this->getIdByUuid($puuidBytes, self::PATIENT_TABLE, "pid");
428 $encounter = $this->getIdByUuid($euuidBytes, self::ENCOUNTER_TABLE, "encounter");
430 $facilityService = new FacilityService();
431 $facilityresult = $facilityService->getById($data["facility_id"]);
432 $facility = $facilityresult['name'];
433 $result = sqlQuery("SELECT sensitivity FROM form_encounter WHERE encounter = ?", array($encounter));
434 if ($result['sensitivity'] && !AclMain::aclCheckCore('sensitivities', $result['sensitivity'])) {
435 return "You are not authorized to see this encounter.";
438 // See view.php to allow or disallow updates of the encounter date.
439 if (!AclMain::aclCheckCore('encounters', 'date_a')) {
440 unset($data["date"]);
443 $data['facility'] = $facility;
445 $query = $this->buildUpdateColumns($data);
446 $sql = " UPDATE form_encounter SET ";
447 $sql .= $query['set'];
448 $sql .= " WHERE encounter = ?";
449 $sql .= " AND pid = ?";
451 array_push($query['bind'], $encounter);
452 array_push($query['bind'], $pid);
453 $results = sqlStatement(
454 $sql,
455 $query['bind']
458 if ($results) {
459 $processingResult = $this->getEncounter($euuid, $puuid);
460 } else {
461 $processingResult->addProcessingError("error processing SQL Update");
464 return $processingResult;
467 public function insertSoapNote($pid, $eid, $data)
469 $soapSql = " INSERT INTO form_soap SET";
470 $soapSql .= " date=NOW(),";
471 $soapSql .= " activity=1,";
472 $soapSql .= " pid=?,";
473 $soapSql .= " subjective=?,";
474 $soapSql .= " objective=?,";
475 $soapSql .= " assessment=?,";
476 $soapSql .= " plan=?";
478 $soapResults = sqlInsert(
479 $soapSql,
480 array(
481 $pid,
482 $data["subjective"],
483 $data["objective"],
484 $data["assessment"],
485 $data["plan"]
489 if (!$soapResults) {
490 return false;
493 $formSql = "INSERT INTO forms SET";
494 $formSql .= " date=NOW(),";
495 $formSql .= " encounter=?,";
496 $formSql .= " form_name='SOAP',";
497 $formSql .= " authorized='1',";
498 $formSql .= " form_id=?,";
499 $formSql .= " pid=?,";
500 $formSql .= " formdir='soap'";
502 $formResults = sqlInsert(
503 $formSql,
504 array(
505 $eid,
506 $soapResults,
507 $pid
511 return array($soapResults, $formResults);
514 public function updateSoapNote($pid, $eid, $sid, $data)
516 $sql = " UPDATE form_soap SET";
517 $sql .= " date=NOW(),";
518 $sql .= " activity=1,";
519 $sql .= " pid=?,";
520 $sql .= " subjective=?,";
521 $sql .= " objective=?,";
522 $sql .= " assessment=?,";
523 $sql .= " plan=?";
524 $sql .= " where id=?";
526 return sqlStatement(
527 $sql,
528 array(
529 $pid,
530 $data["subjective"],
531 $data["objective"],
532 $data["assessment"],
533 $data["plan"],
534 $sid
539 public function updateVital($pid, $eid, $vid, $data)
541 $data['date'] = date("Y-m-d H:i:s");
542 $data['activity'] = 1;
543 $data['id'] = $vid;
544 $data['pid'] = $pid;
545 $data['eid'] = $eid;
547 $vitalsService = new VitalsService();
548 $updatedRecords = $vitalsService->save($data);
549 return $updatedRecords;
552 public function insertVital($pid, $eid, $data)
554 $data['eid'] = $eid;
555 $data['authorized'] = '1';
556 $data['pid'] = $pid;
557 $vitalsService = new VitalsService();
558 $savedVitals = $vitalsService->save($data);
560 // need to grab the form record here, not sure why people need this but sure, why not, since the old method returned
561 // it we will keep the functionality.
562 $vitalsFormId = $savedVitals['id'];
563 $formId = intval(QueryUtils::fetchSingleValue('select id FROM forms WHERE form_id = ? ', 'id', [$vitalsFormId]));
564 return [$vitalsFormId, $formId];
567 public function getVitals($pid, $eid)
569 $vitalsService = new VitalsService();
570 $vitals = $vitalsService->getVitalsForPatientEncounter($pid, $eid) ?? [];
571 return $vitals;
574 public function getVital($pid, $eid, $vid)
576 $vitalsService = new VitalsService();
577 $vitals = $vitalsService->getVitalsForForm($vid);
578 if (!empty($vitals) && $vitals['eid'] == $eid && $vitals['pid'] == $pid) {
579 return $vitals;
581 return null;
584 public function getSoapNotes($pid, $eid)
586 $sql = " SELECT fs.*";
587 $sql .= " FROM forms fo";
588 $sql .= " JOIN form_soap fs on fs.id = fo.form_id";
589 $sql .= " WHERE fo.encounter = ?";
590 $sql .= " AND fs.pid = ?";
592 $statementResults = sqlStatement($sql, array($eid, $pid));
594 $results = array();
595 while ($row = sqlFetchArray($statementResults)) {
596 array_push($results, $row);
599 return $results;
602 public function getSoapNote($pid, $eid, $sid)
604 $sql = " SELECT fs.*";
605 $sql .= " FROM forms fo";
606 $sql .= " JOIN form_soap fs on fs.id = fo.form_id";
607 $sql .= " WHERE fo.encounter = ?";
608 $sql .= " AND fs.id = ?";
609 $sql .= " AND fs.pid = ?";
611 return sqlQuery($sql, array($eid, $sid, $pid));
614 public function validateSoapNote($soapNote)
616 $validator = new Validator();
618 $validator->optional('subjective')->lengthBetween(2, 65535);
619 $validator->optional('objective')->lengthBetween(2, 65535);
620 $validator->optional('assessment')->lengthBetween(2, 65535);
621 $validator->optional('plan')->lengthBetween(2, 65535);
623 return $validator->validate($soapNote);
626 public function validateVital($vital)
628 $validator = new Validator();
630 $validator->optional('temp_method')->lengthBetween(1, 255);
631 $validator->optional('note')->lengthBetween(1, 255);
632 $validator->optional('BMI_status')->lengthBetween(1, 255);
633 $validator->optional('bps')->numeric();
634 $validator->optional('bpd')->numeric();
635 $validator->optional('weight')->numeric();
636 $validator->optional('height')->numeric();
637 $validator->optional('temperature')->numeric();
638 $validator->optional('pulse')->numeric();
639 $validator->optional('respiration')->numeric();
640 $validator->optional('BMI')->numeric();
641 $validator->optional('waist_circ')->numeric();
642 $validator->optional('head_circ')->numeric();
643 $validator->optional('oxygen_saturation')->numeric();
645 return $validator->validate($vital);
648 public function getEncountersForPatientByPid($pid)
650 $encounterResult = $this->search(['pid' => $pid]);
651 if ($encounterResult->hasData()) {
652 return $encounterResult->getData();
654 return [];
658 * The result of this function returns the format needed by the frontend with the window.left_nav.setPatientEncounter function
659 * @param $pid
660 * @return array
662 public function getPatientEncounterListWithCategories($pid)
664 $encounters = $this->getEncountersForPatientByPid($pid);
666 $encounterList = [
667 'ids' => []
668 ,'dates' => []
669 ,'categories' => []
671 foreach ($encounters as $index => $encounter) {
672 $encounterList['ids'][$index] = $encounter['eid'];
673 $encounterList['dates'][$index] = date("Y-m-d", strtotime($encounter['date']));
674 $encounterList['categories'][$index] = $encounter['pc_catname'];
676 return $encounterList;
680 * Returns the sensitivity level for the encounter matching the patient and encounter identifier.
682 * @param $pid The legacy identifier of particular patient
683 * @param $encounter_id The identifier of a particular encounter
684 * @return string sensitivity_level of first row of encounter data
686 public function getSensitivity($pid, $encounter_id)
688 $encounterResult = $this->search(['pid' => $pid, 'eid' => $encounter_id], $options = ['limit' => '1']);
689 if ($encounterResult->hasData()) {
690 return $encounterResult->getData()[0]['sensitivity'];
692 return [];
696 * Returns the referring provider for the encounter matching the patient and encounter identifier.
698 * @param $pid The legacy identifier of particular patient
699 * @param $encounter_id The identifier of a particular encounter
700 * @return string referring provider of first row of encounter data (it's an id from the users table)
702 public function getReferringProviderID($pid, $encounter_id)
704 $encounterResult = $this->search(['pid' => $pid, 'eid' => $encounter_id], $options = ['limit' => '1']);
705 if ($encounterResult->hasData()) {
706 return $encounterResult->getData()[0]['referring_provider_id'] ?? '';
708 return [];
712 * Return an array of encounters within a date range
714 * @param $start_date Any encounter starting on this date
715 * @param $end_date Any encounter ending on this date
716 * @return Array Encounter data payload.
718 public function getEncountersByDateRange($startDate, $endDate)
720 $dateField = new DateSearchField('date', ['ge' . $startDate, 'le' . $endDate], DateSearchField::DATE_TYPE_DATE, $isAnd = true);
721 $encounterResult = $this->search(['date' => $dateField]);
722 if ($encounterResult->hasData()) {
723 $result = $encounterResult->getData();
724 return $result;
726 return [];
730 * Returns the default POS code that is set in the facility table.
732 * @param $facility_id
733 * @return mixed
735 public function getPosCode($facility_id)
737 $sql = "SELECT pos_code FROM facility WHERE id = ?";
738 $result = sqlQuery($sql, [$facility_id]);
739 return $result['pos_code'];