From 48527473b6f1f19c564ebf95a677d23410e051be Mon Sep 17 00:00:00 2001 From: Stephen Nielson Date: Wed, 9 Oct 2024 20:56:28 -0400 Subject: [PATCH] Bug openemr fix 7746 export since delta fix (#7754) * lastModifiedDate export support. _since parameter was not working properly. Need to expose on all of the FHIR services the _lastModifiedDate search parameter and verify all the endpoints are functioning and performant. The export can specify a _since parameter and apparently the original implementation only supported _since from the beginning of the system implementation, and no actual date filtering. Anyone wanting to do a delta would not have their export data function properly. This is in reference to issue #7746 * Fixed _since implementation in bulk export. The _since date filterer was broken and was only allowing dates from the beginning of time for the _since filter. This was preventing any kind of bulk export using the delta of a system. Had to add everywhere the _lastModified parameter. Resources implemented here are - DiagnosticReport - ClinicalNotes - AllergyIntolerance - CarePlan - CareTeam - Coverage - Device - DocumentReference - Encounter - Goal - Group (this was tricky, it uses the patient last updated date to determine group membership) - Immunization - Patient - Location - ValueSet - CareTeam, - Practitioner, - PractitionerRole - Condition - DiagnosticReport - MedicationRequest - Observation - Organization - Procedure - Provenance Had to add date fields to the facility and user table in order to handle their location data since the tables are denormalized using uuid_mapping for the date. Needed to configure list service and appointments to handle searching on list options and appointment categories w/ last updated status so people can grab delta on if the value set has changed. Really need better support for published versions, etc. But it is what it is for now. Added db last_updated fields (and where missing date_create) fields for - Insurance companies - user&facility roles IE PractitionerRole endpoint - calendar categories - list options - added _lastUpdated to form_clinical_notes NOTE if you have a LOT of vital records the upgrade may take some time. Need to make sure we mention this in the patch. Added last_updated column to vitals form to track modifications to the form. Improved vitals query performance by making it use an index on the forms_encounter. * Fix FHIR _lastUpdated social history Made it so the smoking status w/ social history works w/ the _lastUpdated field. * _lastUpdated for Organization resource Added to table procedure_providers the following columns - date_created - procedure_providers Implemented _lastUpdated for Organization resources which includes facility, insurance, and procedure providers. * _lastUpdated and FHIR fix for Medication Had an issue in the Medication resource where drugs added via the pharmacy/dispensary module would not actual show their valid RXCUI system if there was no code system available. Made _lastUpdated work for filtering on medications. Added last_updated and date_created columns to the medication service. * Add helpful message for internal data loads * FHIR patient _lastUpdated, deceasedDate fix Made the _lastUpdated work by adding a last_updated field to patient_data. Hopefully people won't have maxed out their patient_data table record for MySQL as we absolutely need to track the most recent patient_data values from an API perspective so we have to make this change. Also discovered that deceasedDate was throwing errors in the API due to a bad column value. Fixed the issue so deceased boolean flag is now reporting correctly. * FHIR _lastUpdated search for Procedure resource Made the fhir procedure service work with the _lastUpdated parameter for both surgeries and openemr procedures. Fixed issue with unix_timestamp conversion on the prescription service. * Fix style issues. * _lastUpdated Person resource implementation Made it so the _lastUpdated search parameter works with the FHIR Person resource. * FHIR _lastUpdated database changes added date_created to users and facility table. Added all of the date_created and last_modified columns to the fresh database installs. * FHIR Provenance _lastUpdated support Made it so the provenance _lastUpdated gets passed down to all of the sub resources when working with the provenance piece. * FHIR appointment,group bug fixes Group was only showing the most recently changed patients when _lastUpdated is used instead of showing the entire resource. Appointment was not using the correct search field when doing an export. Style fixes. * Safer error messages if oauth2 key invalid * Update api documentation * Fix date_created, missing columns in new db Date_created does not need on update clause. Missed two database columns in the new database. * Fix surrogate key provenance issue. Provenance was incorrectly pushing out surrogate key values based on lastUpdated value fields. Now that lastUpdated is actually reflecting on the actual resources instead of just the latest timestamp, we were incorrectly getting surrogate keys if they the resource was updated earlier than 2022 (V1 key value). * Fix insert statements on calendar categories * Provide better error messages on installer class Error messages of actual SQL causing the installer to fail was being surpressed. Made it so the error messages show instead of just fatal death w/o any more information. --- _rest_config.php | 25 +- _rest_routes.inc.php | 261 ++++++++++++++++++ interface/super/load_codes.php | 5 +- library/classes/Installer.class.php | 23 +- sql/7_0_2-to-7_0_3_upgrade.sql | 70 ++++- sql/database.sql | 65 +++-- src/FHIR/Export/ExportJob.php | 12 + .../AuthorizationController.php | 6 +- .../FhirOperationExportRestController.php | 23 +- src/Services/AppointmentService.php | 17 +- src/Services/CareTeamService.php | 3 + src/Services/ClinicalNotesService.php | 4 + src/Services/ConditionService.php | 5 +- src/Services/DeviceService.php | 4 +- src/Services/DrugService.php | 64 +++-- ...irDiagnosticReportClinicalNotesService.php | 19 +- .../FhirDiagnosticReportLaboratoryService.php | 22 +- .../FhirClinicalNotesService.php | 19 +- .../FhirPatientDocumentReferenceService.php | 19 +- .../FHIR/FhirAllergyIntoleranceService.php | 12 +- src/Services/FHIR/FhirAppointmentService.php | 7 +- src/Services/FHIR/FhirCarePlanService.php | 14 +- src/Services/FHIR/FhirCareTeamService.php | 13 +- src/Services/FHIR/FhirConditionService.php | 13 +- src/Services/FHIR/FhirCoverageService.php | 18 +- src/Services/FHIR/FhirDeviceService.php | 17 +- .../FHIR/FhirDiagnosticReportService.php | 9 + .../FHIR/FhirDocumentReferenceService.php | 9 + src/Services/FHIR/FhirEncounterService.php | 8 +- src/Services/FHIR/FhirGoalService.php | 16 +- src/Services/FHIR/FhirGroupService.php | 7 + src/Services/FHIR/FhirImmunizationService.php | 12 +- src/Services/FHIR/FhirLocationService.php | 14 +- .../FHIR/FhirMedicationRequestService.php | 12 +- src/Services/FHIR/FhirMedicationService.php | 12 +- src/Services/FHIR/FhirObservationService.php | 6 + src/Services/FHIR/FhirOrganizationService.php | 8 +- src/Services/FHIR/FhirPatientService.php | 15 +- src/Services/FHIR/FhirPersonService.php | 14 +- .../FHIR/FhirPractitionerRoleService.php | 19 +- src/Services/FHIR/FhirPractitionerService.php | 16 +- src/Services/FHIR/FhirProcedureService.php | 6 + src/Services/FHIR/FhirProvenanceService.php | 115 +++----- src/Services/FHIR/FhirValueSetService.php | 163 ++++++----- .../Group/FhirPatientProviderGroupService.php | 13 +- .../FHIR/IFhirExportableResourceService.php | 10 + .../FhirObservationLaboratoryService.php | 12 +- .../FhirObservationSocialHistoryService.php | 12 +- .../FhirObservationVitalsService.php | 14 +- .../FhirOrganizationFacilityService.php | 20 +- .../FhirOrganizationInsuranceService.php | 20 +- ...irOrganizationProcedureProviderService.php | 20 +- .../FhirProcedureOEProcedureService.php | 14 +- .../Procedure/FhirProcedureSurgeryService.php | 12 +- .../FhirBulkExportDomainResourceTrait.php | 13 + src/Services/FHIR/UtilsService.php | 14 + src/Services/GroupService.php | 25 +- src/Services/ImmunizationService.php | 5 +- src/Services/InsuranceCompanyService.php | 4 +- src/Services/ListService.php | 50 ++++ src/Services/LocationService.php | 9 +- src/Services/PractitionerRoleService.php | 15 +- src/Services/PrescriptionService.php | 20 +- src/Services/ProcedureProviderService.php | 2 + src/Services/Search/DateSearchField.php | 2 +- .../Search/FHIRSearchFieldFactory.php | 14 +- src/Services/SurgeryService.php | 4 +- src/Services/UserService.php | 8 +- src/Services/Utils/DateFormatterUtils.php | 9 + src/Services/VitalsService.php | 8 +- swagger/openemr-api.yaml | 205 +++++++++++++- 71 files changed, 1480 insertions(+), 295 deletions(-) diff --git a/_rest_config.php b/_rest_config.php index c1f652a315d..d931bb969e9 100644 --- a/_rest_config.php +++ b/_rest_config.php @@ -202,19 +202,30 @@ public static function verifyAccessToken() $logger = new SystemLogger(); $response = self::createServerResponse(); $request = self::createServerRequest(); - $server = new ResourceServer( - new AccessTokenRepository(), - self::$publicKey - ); try { + // if we there's a key problem need to catch the exception + $server = new ResourceServer( + new AccessTokenRepository(), + self::$publicKey + ); $raw = $server->validateAuthenticatedRequest($request); } catch (OAuthServerException $exception) { $logger->error("RestConfig->verifyAccessToken() OAuthServerException", ["message" => $exception->getMessage()]); return $exception->generateHttpResponse($response); } catch (\Exception $exception) { - $logger->error("RestConfig->verifyAccessToken() Exception", ["message" => $exception->getMessage()]); - return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) - ->generateHttpResponse($response); + if ($exception instanceof LogicException) { + $logger->error( + "RestConfig->verifyAccessToken() LogicException, likely oauth2 public key is missing, corrupted, or misconfigured", + ["message" => $exception->getMessage()] + ); + return (new OAuthServerException("Invalid access token", 0, 'invalid_token', 401)) + ->generateHttpResponse($response); + } else { + $logger->error("RestConfig->verifyAccessToken() Exception", ["message" => $exception->getMessage()]); + // do NOT reveal what happened at the server level if we have a server exception + return (new OAuthServerException("Server Error", 0, 'unknown_error', 500)) + ->generateHttpResponse($response); + } } return $raw; diff --git a/_rest_routes.inc.php b/_rest_routes.inc.php index b2792744a90..d3fa1ef324e 100644 --- a/_rest_routes.inc.php +++ b/_rest_routes.inc.php @@ -7622,6 +7622,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -7814,6 +7823,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -7945,6 +7963,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -8113,6 +8140,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -8196,6 +8232,15 @@ * type="string" * ) * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), * @OA\Response( * response="200", * description="Standard Response", @@ -8305,6 +8350,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -8484,6 +8538,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -8642,6 +8705,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -8802,6 +8874,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -9011,6 +9092,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -9338,6 +9428,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -9537,6 +9636,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -9697,6 +9805,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -9861,6 +9978,24 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -10014,6 +10149,15 @@ * type="string" * ) * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), * @OA\Response( * response="200", * description="Standard Response", @@ -10133,6 +10277,24 @@ * path="/fhir/Medication", * description="Returns a list of Medication resources.", * tags={"fhir"}, + * @OA\Parameter( + * name="_id", + * in="query", + * description="The uuid for the Medication resource.", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), * @OA\Response( * response="200", * description="Standard Response", @@ -10270,6 +10432,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -10458,6 +10629,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -10684,6 +10864,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="name", * in="query", * description="The name of the Organization resource.", @@ -11491,6 +11680,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="identifier", * in="query", * description="The identifier of the Patient resource.", @@ -11860,6 +12058,24 @@ * description="Returns a list of Person resources.", * tags={"fhir"}, * @OA\Parameter( + * name="_id", + * in="query", + * description="The uuid for the Person resource.", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="name", * in="query", * description="The name of the Person resource.", @@ -12138,6 +12354,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="name", * in="query", * description="The name of the Practitioner resource.", @@ -12569,6 +12794,24 @@ * description="Returns a list of PractitionerRole resources.", * tags={"fhir"}, * @OA\Parameter( + * name="_id", + * in="query", + * description="The uuid for the PractitionerRole resource.", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="specialty", * in="query", * description="The specialty of the PractitionerRole resource.", @@ -12727,6 +12970,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -13051,6 +13303,15 @@ * type="string" * ) * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), * @OA\Response( * response="200", * description="Standard Response", diff --git a/interface/super/load_codes.php b/interface/super/load_codes.php index 9bea7da6e07..89d122b13d1 100644 --- a/interface/super/load_codes.php +++ b/interface/super/load_codes.php @@ -226,7 +226,10 @@ that (zipped or not). You may do the same with the weekly updates, but for those uncheck the "" checkbox above.

- +

+ +

+
diff --git a/library/classes/Installer.class.php b/library/classes/Installer.class.php index 319d14dad11..3bb2eb0ac7b 100644 --- a/library/classes/Installer.class.php +++ b/library/classes/Installer.class.php @@ -1346,14 +1346,23 @@ private function execute_sql($sql, $showError = true) $this->user_database_connection(); } - $results = mysqli_query($this->dbh, $sql); - if ($results) { - return $results; - } else { + try { + $results = mysqli_query($this->dbh, $sql); + if ($results) { + return $results; + } else { + if ($showError) { + $error_mes = mysqli_error($this->dbh); + $this->error_message = "unable to execute SQL: '$sql' due to: " . $error_mes; + error_log("ERROR IN OPENEMR INSTALL: Unable to execute SQL: " . htmlspecialchars($sql, ENT_QUOTES) . " due to: " . htmlspecialchars($error_mes, ENT_QUOTES)); + } + return false; + } + // this exception only occurs if MYSQLI_REPORT_STRICT is enabled (see https://www.php.net/manual/en/mysqli.query.php) + } catch (\mysqli_sql_exception $exception) { if ($showError) { - $error_mes = mysqli_error($this->dbh); - $this->error_message = "unable to execute SQL: '$sql' due to: " . $error_mes; - error_log("ERROR IN OPENEMR INSTALL: Unable to execute SQL: " . htmlspecialchars($sql, ENT_QUOTES) . " due to: " . htmlspecialchars($error_mes, ENT_QUOTES)); + $this->error_message = "unable to execute SQL: '$sql' due to: " . $exception->getMessage(); + error_log("ERROR IN OPENEMR INSTALL: Unable to execute SQL: " . htmlspecialchars($sql, ENT_QUOTES) . " due to: " . htmlspecialchars($exception->getMessage(), ENT_QUOTES)); } return false; } diff --git a/sql/7_0_2-to-7_0_3_upgrade.sql b/sql/7_0_2-to-7_0_3_upgrade.sql index 40f7ffb72cb..3cf5447325d 100644 --- a/sql/7_0_2-to-7_0_3_upgrade.sql +++ b/sql/7_0_2-to-7_0_3_upgrade.sql @@ -134,4 +134,72 @@ INSERT INTO `supported_external_dataloads` (`load_type`, `load_source`, `load_re #IfNotRow4D supported_external_dataloads load_type ICD10 load_source CMS load_release_date 2024-10-01 load_filename Zip File 3 2025 ICD-10-PCS Codes File.zip INSERT INTO `supported_external_dataloads` (`load_type`, `load_source`, `load_release_date`, `load_filename`, `load_checksum`) VALUES ('ICD10', 'CMS', '2024-10-01', 'Zip File 3 2025 ICD-10-PCS Codes File.zip', 'a47ceb9a09fcc475fec19cee6526a335'); -#EndIf \ No newline at end of file +#EndIf + +#IfMissingColumn users date_created +ALTER TABLE `users` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn users last_updated +ALTER TABLE `users` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn facility date_created +ALTER TABLE `facility` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn facility last_updated +ALTER TABLE `facility` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn insurance_companies date_created +ALTER TABLE `insurance_companies` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn insurance_companies last_updated +ALTER TABLE `insurance_companies` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn facility_user_ids date_created +ALTER TABLE `facility_user_ids` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn facility_user_ids last_updated +ALTER TABLE `facility_user_ids` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn openemr_postcalendar_categories pc_last_updated +ALTER TABLE `openemr_postcalendar_categories` ADD `pc_last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn list_options last_updated +ALTER TABLE `list_options` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn form_clinical_notes last_updated +ALTER TABLE `form_clinical_notes` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn form_vitals last_updated +ALTER TABLE `form_vitals` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn procedure_providers date_created +ALTER TABLE `procedure_providers` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn procedure_providers last_updated +ALTER TABLE `procedure_providers` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn drugs date_created +ALTER TABLE `drugs` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn drugs last_updated +ALTER TABLE `drugs` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn patient_data last_updated +ALTER TABLE `patient_data` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf diff --git a/sql/database.sql b/sql/database.sql index 1fd6da2f49f..6954d015db1 100644 --- a/sql/database.sql +++ b/sql/database.sql @@ -1485,6 +1485,8 @@ CREATE TABLE `drugs` ( `drug_code` varchar(25) NULL, `consumable` tinyint(1) NOT NULL DEFAULT 0 COMMENT '1 = will not show on the fee sheet', `dispensable` tinyint(1) NOT NULL DEFAULT 1 COMMENT '0 = pharmacy elsewhere, 1 = dispensed here', + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`drug_id`), UNIQUE KEY `uuid` (`uuid`) ) ENGINE=InnoDB AUTO_INCREMENT=1; @@ -1737,6 +1739,8 @@ CREATE TABLE `facility` ( `info` TEXT, `weno_id` VARCHAR(10) DEFAULT NULL, `inactive` tinyint(1) NOT NULL DEFAULT '0', + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uuid` (`uuid`), PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4; @@ -1745,7 +1749,7 @@ CREATE TABLE `facility` ( -- Inserting data for table `facility` -- -INSERT INTO `facility` VALUES (3, NULL, 'Your Clinic Name Here', '000-000-0000', '000-000-0000', '', '', '', '', '', '', NULL, NULL, 1, 1, 1, NULL, '', '', '', '', '', '','#99FFFF','0', '', '1', '', '', '', '', '', '', '', '', NULL, 0); +INSERT INTO `facility` VALUES (3, NULL, 'Your Clinic Name Here', '000-000-0000', '000-000-0000', '', '', '', '', '', '', NULL, NULL, 1, 1, 1, NULL, '', '', '', '', '', '','#99FFFF','0', '', '1', '', '', '', '', '', '', '', '', NULL, 0, NOW(), NOW()); -- -------------------------------------------------------- @@ -1761,6 +1765,8 @@ CREATE TABLE `facility_user_ids` ( `uuid` binary(16) DEFAULT NULL, `field_id` varchar(31) NOT NULL COMMENT 'references layout_options.field_id', `field_value` TEXT, + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `uid` (`uid`,`facility_id`,`field_id`), KEY `uuid` (`uuid`) @@ -1839,6 +1845,7 @@ CREATE TABLE `form_clinical_notes` ( `clinical_notes_type` varchar(100) DEFAULT NULL, `clinical_notes_category` varchar(100) DEFAULT NULL, `note_related_to` text, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uuid` (`uuid`) ) ENGINE=InnoDB; @@ -2293,6 +2300,7 @@ CREATE TABLE `form_vitals` ( `ped_bmi` DECIMAL(6,2) default '0.00', `ped_head_circ` DECIMAL(6,2) default '0.00', `inhaled_oxygen_concentration` DECIMAL(6,2) DEFAULT '0.00', + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `pid` (`pid`), UNIQUE KEY `uuid` (`uuid`) @@ -3137,6 +3145,8 @@ CREATE TABLE `insurance_companies` ( `eligibility_id` VARCHAR(32) default NULL, `x12_default_eligibility_id` INT(11) default NULL, `cqm_sop` int DEFAULT NULL COMMENT 'HL7 Source of Payment for eCQMs', + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uuid` (`uuid`) ) ENGINE=InnoDB; @@ -3715,6 +3725,7 @@ CREATE TABLE `list_options` ( `subtype` varchar(31) NOT NULL DEFAULT '', `edit_options` tinyint(1) NOT NULL DEFAULT '1', `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`list_id`,`option_id`) ) ENGINE=InnoDB; @@ -7289,6 +7300,7 @@ CREATE TABLE `openemr_postcalendar_categories` ( `pc_active` tinyint(1) NOT NULL default 1, `pc_seq` int(11) NOT NULL default '0', `aco_spec` VARCHAR(63) NOT NULL default 'encounters|notes', + `pc_last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`pc_catid`), UNIQUE KEY (`pc_constant_id`), KEY `basic_cat` (`pc_catname`,`pc_catcolor`) @@ -7298,21 +7310,37 @@ CREATE TABLE `openemr_postcalendar_categories` ( -- Inserting data for table `openemr_postcalendar_categories` -- -INSERT INTO `openemr_postcalendar_categories` VALUES (1,'no_show', 'No Show', '#dee2e6', 'Reserved to define when an event did not occur as specified.', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 0, 0, 0, 0, 0, 0,1,1,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (2,'in_office', 'In Office', '#cce5ff', 'Reserved todefine when a provider may haveavailable appointments after.', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 1, 3, 2, 0, 0, 1,1,2,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (3,'out_of_office', 'Out Of Office', '#fdb172', 'Reserved to define when a provider may not have available appointments after.', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 1, 3, 2, 0, 0, 1,1,3,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (4,'vacation', 'Vacation', '#e9ecef', 'Reserved for use to define Scheduled Vacation Time', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 0, 0, 0, 1, 0, 1,1,4,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (5,'office_visit', 'Office Visit', '#ffecb4', 'Normal Office Visit', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,5,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (6,'holidays','Holidays','#8663ba','Clinic holiday',0,NULL,'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}',0,86400,1,3,2,0,0,2,1,6,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (7,'closed','Closed','#2374ab','Clinic closed',0,NULL,'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}',0,86400,1,3,2,0,0,2,1,7,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (8,'lunch', 'Lunch', '#ffd351', 'Lunch', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 3600, 0, 3, 2, 0, 0, 1,1,8,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (9,'established_patient', 'Established Patient', '#93d3a2', '', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0, 0,1,9,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (10,'new_patient','New Patient', '#a2d9e2', '', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 1800, 0, 0, 0, 0, 0, 0,1,10,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (11,'reserved','Reserved','#b02a37','Reserved',1,NULL,'a:5:{s:17:\"event_repeat_freq\";s:1:\"1\";s:22:\"event_repeat_freq_type\";s:1:\"4\";s:19:\"event_repeat_on_num\";s:1:\"1\";s:19:\"event_repeat_on_day\";s:1:\"0\";s:20:\"event_repeat_on_freq\";s:1:\"0\";}',0,900,0,3,2,0,0, 1,1,11,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (12,'health_and_behavioral_assessment', 'Health and Behavioral Assessment', '#ced4da', 'Health and Behavioral Assessment', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,12,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (13,'preventive_care_services', 'Preventive Care Services', '#d3c6ec', 'Preventive Care Services', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,13,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (14,'ophthalmological_services', 'Ophthalmological Services', '#febe89', 'Ophthalmological Services', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,14,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (15,'group_therapy', 'Group Therapy' , '#adb5bd' , 'Group Therapy', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 3600, 0, 0, 0, 0, 0, 3, 1, 15,'encounters|notes'); + +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (1,'no_show', 'No Show', '#dee2e6', 'Reserved to define when an event did not occur as specified.', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 0, 0, 0, 0, 0, 0,1,1,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (2,'in_office', 'In Office', '#cce5ff', 'Reserved todefine when a provider may haveavailable appointments after.', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 1, 3, 2, 0, 0, 1,1,2,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (3,'out_of_office', 'Out Of Office', '#fdb172', 'Reserved to define when a provider may not have available appointments after.', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 1, 3, 2, 0, 0, 1,1,3,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (4,'vacation', 'Vacation', '#e9ecef', 'Reserved for use to define Scheduled Vacation Time', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 0, 0, 0, 1, 0, 1,1,4,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (5,'office_visit', 'Office Visit', '#ffecb4', 'Normal Office Visit', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,5,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (6,'holidays','Holidays','#8663ba','Clinic holiday',0,NULL,'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}',0,86400,1,3,2,0,0,2,1,6,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (7,'closed','Closed','#2374ab','Clinic closed',0,NULL,'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}',0,86400,1,3,2,0,0,2,1,7,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (8,'lunch', 'Lunch', '#ffd351', 'Lunch', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 3600, 0, 3, 2, 0, 0, 1,1,8,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (9,'established_patient', 'Established Patient', '#93d3a2', '', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0, 0,1,9,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (10,'new_patient','New Patient', '#a2d9e2', '', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 1800, 0, 0, 0, 0, 0, 0,1,10,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (11,'reserved','Reserved','#b02a37','Reserved',1,NULL,'a:5:{s:17:\"event_repeat_freq\";s:1:\"1\";s:22:\"event_repeat_freq_type\";s:1:\"4\";s:19:\"event_repeat_on_num\";s:1:\"1\";s:19:\"event_repeat_on_day\";s:1:\"0\";s:20:\"event_repeat_on_freq\";s:1:\"0\";}',0,900,0,3,2,0,0, 1,1,11,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (12,'health_and_behavioral_assessment', 'Health and Behavioral Assessment', '#ced4da', 'Health and Behavioral Assessment', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,12,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (13,'preventive_care_services', 'Preventive Care Services', '#d3c6ec', 'Preventive Care Services', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,13,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (14,'ophthalmological_services', 'Ophthalmological Services', '#febe89', 'Ophthalmological Services', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,14,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (15,'group_therapy', 'Group Therapy' , '#adb5bd' , 'Group Therapy', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 3600, 0, 0, 0, 0, 0, 3, 1, 15,'encounters|notes'); -- -------------------------------------------------------- @@ -7521,6 +7549,7 @@ CREATE TABLE `patient_data` ( `updated_by` BIGINT(20) DEFAULT NULL COMMENT 'users.id the user that last modified this record', `preferred_name` TINYTEXT, `nationality_country` TINYTEXT, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `pid` (`pid`), UNIQUE KEY `uuid` (`uuid`), KEY `id` (`id`) @@ -8895,6 +8924,8 @@ CREATE TABLE `users` ( `supervisor_id` int(11) NOT NULL DEFAULT '0', `billing_facility` TEXT, `billing_facility_id` INT(11) NOT NULL DEFAULT '0', + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uuid` (`uuid`), KEY `abook_type` (`abook_type`) @@ -9343,6 +9374,8 @@ CREATE TABLE `procedure_providers` ( `lab_director` bigint(20) NOT NULL DEFAULT '0', `active` tinyint(1) NOT NULL DEFAULT '1', `type` varchar(31) DEFAULT NULL, + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`ppid`), UNIQUE KEY `uuid` (`uuid`) ) ENGINE=InnoDB; diff --git a/src/FHIR/Export/ExportJob.php b/src/FHIR/Export/ExportJob.php index 0c771ca61e1..87eeec842f7 100644 --- a/src/FHIR/Export/ExportJob.php +++ b/src/FHIR/Export/ExportJob.php @@ -22,6 +22,8 @@ use http\Exception\InvalidArgumentException; use OpenEMR\Common\Uuid\UuidRegistry; +use OpenEMR\Services\Search\SearchComparator; +use OpenEMR\Services\Utils\DateFormatterUtils; class ExportJob { @@ -210,6 +212,16 @@ public function getResourceIncludeTime(): \DateTime return $this->resourceIncludeTime; } + public function getResourceIncludeSearchParamValue() + { + return SearchComparator::GREATER_THAN_OR_EQUAL_TO . $this->getResourceIncludeISO8601Date(); + } + + public function getResourceIncludeISO8601Date(): string + { + return DateFormatterUtils::getFormattedISO8601DateFromDateTime($this->resourceIncludeTime); + } + /** * @param \DateTime $resourceIncludeTime */ diff --git a/src/RestControllers/AuthorizationController.php b/src/RestControllers/AuthorizationController.php index ce7c95e79cc..b0c4349358d 100644 --- a/src/RestControllers/AuthorizationController.php +++ b/src/RestControllers/AuthorizationController.php @@ -90,6 +90,9 @@ class AuthorizationController public const GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials'; public const OFFLINE_ACCESS_SCOPE = 'offline_access'; + // https://hl7.org/fhir/uv/bulkdata/authorization/index.html#issuing-access-tokens Spec states 5 min max + public const GRANT_TYPE_ACCESS_CODE_TTL = "PT300S"; // 5 minutes + public $authBaseUrl; public $authBaseFullUrl; public $siteId; @@ -642,8 +645,7 @@ public function getAuthorizationServer($includeAuthGrantRefreshToken = true): Au $client_credentials->setHttpClient(new Client()); // set our guzzle client here $authServer->enableGrantType( $client_credentials, - // https://hl7.org/fhir/uv/bulkdata/authorization/index.html#issuing-access-tokens Spec states 5 min max - new \DateInterval('PT300S') + new \DateInterval(self::GRANT_TYPE_ACCESS_CODE_TTL) ); } diff --git a/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php b/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php index f1377855296..78bfae45d41 100644 --- a/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php +++ b/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php @@ -23,6 +23,8 @@ use OpenEMR\Services\FHIR\IFhirExportableResourceService; use OpenEMR\Services\FHIR\Utils\FhirServiceLocator; use OpenEMR\Services\FHIR\UtilsService; +use OpenEMR\Services\Search\DateSearchField; +use OpenEMR\Services\Search\SearchFieldComparableValue; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; use Ramsey\Uuid\Uuid; @@ -104,7 +106,11 @@ public function processExport($exportParams, $exportType, $acceptHeader, $prefer } $outputFormat = $exportParams['_outputFormat'] ?? ExportJob::OUTPUT_FORMAT_FHIR_NDJSON; - $since = $exportParams['_since'] ?? new \DateTime(date("Y-m-d H:i:s", 0)); // since epoch time + if (!empty($exportParams['_since'])) { + $since = $this->parseFHIRInstant($exportParams['_since']); + } else { + $since = new \DateTime(date(\DateTimeInterface::ATOM, 0)); // since epoch time + } $type = $exportParams['type'] ?? ''; $groupId = $exportParams['groupId'] ?? null; $resources = !empty($type) ? explode(",", $type) : []; @@ -624,4 +630,19 @@ private function getPatientUuidsForGroup($groupId) } return $patientUuids; } + + private function parseFHIRInstant(string $_since) + { + // concievably they could send us a date that is not an actual INSTANCE of a date, but we'll just convert it + // to a regular date anyways, if the format is invalid DateSearchField will error out. + $dateField = new DateSearchField("_since", [$_since], DateSearchField::DATE_TYPE_DATETIME); + $values = $dateField->getValues(); + $comparable = reset($values); + $value = $comparable->getValue(); + if ($value instanceof \DatePeriod) { + return $value->getStartDate(); + } else { + throw new \InvalidArgumentException("Invalid date format for _since parameter"); + } + } } diff --git a/src/Services/AppointmentService.php b/src/Services/AppointmentService.php index c6d9e62b584..1e7776877ba 100644 --- a/src/Services/AppointmentService.php +++ b/src/Services/AppointmentService.php @@ -475,7 +475,7 @@ public function deleteAppointmentRecord($eid) */ public function getCalendarCategories() { - $sql = "SELECT pc_catid, pc_constant_id, pc_catname, pc_cattype,aco_spec FROM openemr_postcalendar_categories " + $sql = "SELECT pc_catid, pc_constant_id, pc_catname, pc_cattype,aco_spec, pc_last_updated FROM openemr_postcalendar_categories " . " WHERE pc_active = 1 ORDER BY pc_seq"; return QueryUtils::fetchRecords($sql); } @@ -656,4 +656,19 @@ public function getOneCalendarCategory($cat_id) $sql = "SELECT * FROM openemr_postcalendar_categories WHERE pc_catid = ?"; return QueryUtils::fetchRecords($sql, [$cat_id]); } + + public function searchCalendarCategories(array $oeSearchParameters) + { + $sql = "SELECT * FROM openemr_postcalendar_categories "; + $whereClause = FhirSearchWhereClauseBuilder::build($oeSearchParameters, true); + $sql .= $whereClause->getFragment(); + $sqlBindArray = $whereClause->getBoundValues(); + $records = QueryUtils::fetchRecords($sql, $sqlBindArray); + $processingResult = new ProcessingResult(); + if (!empty($records)) { + $processingResult->setData($records); + } + // TODO: look at handling offset and limit here + return $processingResult; + } } diff --git a/src/Services/CareTeamService.php b/src/Services/CareTeamService.php index e3d44d0729d..4c1e9d56cd2 100644 --- a/src/Services/CareTeamService.php +++ b/src/Services/CareTeamService.php @@ -50,6 +50,7 @@ public function search($search, $isAndCondition = true) careteam_mapping.care_team_provider as providers, careteam_mapping.care_team_facility as facilities, careteam_mapping.care_team_status, + careteam_mapping.date, care_team_status_title FROM ( SELECT @@ -58,6 +59,7 @@ public function search($search, $isAndCondition = true) ,patient_data.care_team_provider ,patient_data.care_team_facility ,patient_data.care_team_status + ,patient_data.date FROM uuid_mapping -- we join on this to make sure we've got data integrity since we don't actually use foreign keys right now @@ -72,6 +74,7 @@ public function search($search, $isAndCondition = true) ,patient_history.care_team_provider ,patient_history.care_team_facility ,'inactive' AS care_team_status + ,patient_history.date FROM patient_history JOIN patient_data ON patient_history.pid = patient_data.pid diff --git a/src/Services/ClinicalNotesService.php b/src/Services/ClinicalNotesService.php index 1c6c4345a69..9c61653bafd 100644 --- a/src/Services/ClinicalNotesService.php +++ b/src/Services/ClinicalNotesService.php @@ -74,6 +74,8 @@ public function search($search, $isAndCondition = true) ,notes.clinical_notes_type ,notes.note_related_to ,notes.clinical_notes_category + ,notes.last_updated + ,forms.date_created ,lo_category.category_code ,lo_category.category_title ,patients.pid @@ -100,6 +102,7 @@ public function search($search, $isAndCondition = true) ,note_related_to ,clinical_notes_category ,form_id + ,last_updated ,user FROM form_clinical_notes @@ -109,6 +112,7 @@ public function search($search, $isAndCondition = true) id AS form_id, encounter ,pid AS form_pid + ,`date` AS date_created FROM forms ) forms ON forms.form_id = notes.form_id diff --git a/src/Services/ConditionService.php b/src/Services/ConditionService.php index a3fd1bf37f6..a1423c36823 100644 --- a/src/Services/ConditionService.php +++ b/src/Services/ConditionService.php @@ -48,6 +48,7 @@ public function search($search, $isAndCondition = true) patient.puuid, patient.patient_uuid, condition_ids.condition_uuid, + condition_ids.last_updated_time, verification.title as verification_title ,provider.provider_id ,provider.provider_npi @@ -55,7 +56,7 @@ public function search($search, $isAndCondition = true) ,provider.provider_username FROM lists INNER JOIN ( - SELECT lists.uuid AS condition_uuid FROM lists + SELECT lists.uuid AS condition_uuid, lists.modifydate as last_updated_time FROM lists ) condition_ids ON lists.uuid = condition_ids.condition_uuid LEFT JOIN list_options as verification ON verification.option_id = lists.verification and verification.list_id = 'condition-verification' RIGHT JOIN ( @@ -68,7 +69,7 @@ public function search($search, $isAndCondition = true) LEFT JOIN issue_encounter as issue ON issue.list_id =lists.id LEFT JOIN form_encounter as encounter ON encounter.encounter =issue.encounter LEFT JOIN ( - select + select id AS provider_id ,uuid AS provider_uuid ,npi AS provider_npi diff --git a/src/Services/DeviceService.php b/src/Services/DeviceService.php index b2e5540868a..e8bf46077af 100644 --- a/src/Services/DeviceService.php +++ b/src/Services/DeviceService.php @@ -38,7 +38,7 @@ public function search($search, $isAndCondition = true) ( SELECT `udi`, - `uuid`, `date`, `title`,`udi_data`, `begdate`, `diagnosis`, `user`, `pid` + `uuid`, `date`, `title`,`udi_data`, `begdate`, `diagnosis`, `user`, `pid`,modifydate FROM lists WHERE `type` = 'medical_device' ) l JOIN ( @@ -46,7 +46,7 @@ public function search($search, $isAndCondition = true) from patient_data ) patients ON l.pid = patients.pid LEFT JOIN ( - select + select id AS provider_id ,npi AS provider_npi ,uuid AS provider_uuid diff --git a/src/Services/DrugService.php b/src/Services/DrugService.php index ee72ba86132..34ff7567fda 100644 --- a/src/Services/DrugService.php +++ b/src/Services/DrugService.php @@ -94,29 +94,49 @@ public function getOne($uuid) public function search($search, $isAndCondition = true) { - $sql = "SELECT drugs.drug_id, - uuid, - name, - ndc_number, - form, - size, - unit, - route, - related_code, - active, - drug_code, + $sql = "SELECT + drug_table.drug_id, + drug_table.uuid, + drug_table.name, + drug_table.ndc_number, + drug_table.form, + drug_table.size, + drug_table.unit, + drug_table.route, + drug_table.related_code, + drug_table.active, + drug_table.drug_code, IF(drug_prescriptions.rxnorm_drugcode!='' ,drug_prescriptions.rxnorm_drugcode - ,IF(drug_code IS NULL, '', concat('RXCUI:',drug_code)) + ,IF(drug_table.drug_code IS NULL, '', drug_table.drug_code) ) AS 'rxnorm_drugcode', drug_inventory.manufacturer, drug_inventory.lot_number, - drug_inventory.expiration - FROM drugs + drug_inventory.expiration, + drug_table.drug_last_updated, + drug_table.drug_date_created + FROM ( + select + drug_id, + uuid, + name, + ndc_number, + form, + size, + unit, + route, + related_code, + active, + drug_code, + last_updated AS drug_last_updated, + date_created AS drug_date_created + FROM + drugs + ) drug_table LEFT JOIN drug_inventory - ON drugs.drug_id = drug_inventory.drug_id + ON drug_table.drug_id = drug_inventory.drug_id LEFT JOIN ( - select + select uuid AS prescription_uuid ,rxnorm_drugcode ,drug_id @@ -124,7 +144,7 @@ public function search($search, $isAndCondition = true) FROM prescriptions ) drug_prescriptions - ON drug_prescriptions.drug_id = drugs.drug_id + ON drug_prescriptions.drug_id = drug_table.drug_id LEFT JOIN ( select uuid AS puuid ,pid @@ -161,7 +181,14 @@ protected function createResultRecordFromDatabaseResult($row) $record = parent::createResultRecordFromDatabaseResult($row); if ($record['rxnorm_drugcode'] != "") { - $codes = $this->addCoding($row['rxnorm_drugcode']); + // removed the RXCUI concatenation out of the db query and into the code here + // some parts of OpenEMR adds the RXCUI designation in the drug_code such as the inventory/dispensary module + // and this causes the FHIR medication resource to not get the actual RXCUI code. + if ($row['drug_code'] == $record['rxnorm_drugcode'] && strpos($row['drug_code'], ':') === false) { + $codes = $this->addCoding("RXCUI:" . $row['drug_code']); + } else { + $codes = $this->addCoding($row['rxnorm_drugcode']); + } $updatedCodes = []; foreach ($codes as $code => $codeValues) { if (empty($codeValues['description'])) { @@ -173,6 +200,7 @@ protected function createResultRecordFromDatabaseResult($row) $record['drug_code'] = $updatedCodes; } + // TODO: @adunsulag this looks odd... why modify the original row...? look at removing this. if ($row['rxnorm_drugcode'] != "") { $row['drug_code'] = $this->addCoding($row['drug_code']); } diff --git a/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportClinicalNotesService.php b/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportClinicalNotesService.php index 2ac98de8718..55da1778b69 100644 --- a/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportClinicalNotesService.php +++ b/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportClinicalNotesService.php @@ -31,6 +31,7 @@ use OpenEMR\Services\FHIR\UtilsService; use OpenEMR\Services\ListService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\SearchModifier; use OpenEMR\Services\Search\ServiceField; @@ -65,9 +66,15 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category_code']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + public function supportsCategory($category) { $loincCategory = "LOINC:" . $category; @@ -85,10 +92,14 @@ public function supportsCode($code) public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $report = new FHIRDiagnosticReport(); - $meta = new FHIRMeta(); - $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $report->setMeta($meta); + $fhirMeta = new FHIRMeta(); + $fhirMeta->setVersionId('1'); + if (!empty($dataRecord['last_updated'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $report->setMeta($fhirMeta); $id = new FHIRId(); $id->setValue($dataRecord['uuid']); diff --git a/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportLaboratoryService.php b/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportLaboratoryService.php index 393e8bc696e..ddfcb5c581c 100644 --- a/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportLaboratoryService.php +++ b/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportLaboratoryService.php @@ -29,6 +29,7 @@ use OpenEMR\Services\FHIR\UtilsService; use OpenEMR\Services\ProcedureService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\SearchModifier; use OpenEMR\Services\Search\ServiceField; @@ -71,9 +72,15 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['report_date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('report_uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['report_date']); + } + public function supportsCategory($category) { return $category === self::LAB_CATEGORY; @@ -91,14 +98,15 @@ public function supportsCode($code) public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $report = new FHIRDiagnosticReport(); - $meta = new FHIRMeta(); - $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $report->setMeta($meta); - - $dataRecordReport = array_pop($dataRecord['reports']); - + $fhirMeta = new FHIRMeta(); + $fhirMeta->setVersionId('1'); + if (!empty($dataRecordReport['date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecordReport['date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $report->setMeta($fhirMeta); $id = new FHIRId(); $id->setValue($dataRecordReport['uuid']); diff --git a/src/Services/FHIR/DocumentReference/FhirClinicalNotesService.php b/src/Services/FHIR/DocumentReference/FhirClinicalNotesService.php index 2b71c4183dc..dcd70bfd5cb 100644 --- a/src/Services/FHIR/DocumentReference/FhirClinicalNotesService.php +++ b/src/Services/FHIR/DocumentReference/FhirClinicalNotesService.php @@ -36,6 +36,7 @@ use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\FHIR\UtilsService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\SearchModifier; use OpenEMR\Services\Search\ServiceField; @@ -85,16 +86,26 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATE, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $docReference = new FHIRDocumentReference(); - $meta = new FHIRMeta(); - $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $docReference->setMeta($meta); + $fhirMeta = new FHIRMeta(); + $fhirMeta->setVersionId('1'); + if (!empty($dataRecord['date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $docReference->setMeta($fhirMeta); $id = new FHIRId(); $id->setValue($dataRecord['uuid']); diff --git a/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php b/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php index 485b0f8b3ca..9271d883d9d 100644 --- a/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php +++ b/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php @@ -31,6 +31,7 @@ use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\FHIR\UtilsService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\SearchModifier; use OpenEMR\Services\Search\ServiceField; @@ -74,9 +75,15 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + protected function searchForOpenEMRRecords($openEMRSearchParameters): ProcessingResult { if (isset($openEMRSearchParameters['category'])) { @@ -99,10 +106,14 @@ protected function searchForOpenEMRRecords($openEMRSearchParameters): Processing public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $docReference = new FHIRDocumentReference(); - $meta = new FHIRMeta(); - $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $docReference->setMeta($meta); + $fhirMeta = new FHIRMeta(); + $fhirMeta->setVersionId('1'); + if (!empty($dataRecord['date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $docReference->setMeta($fhirMeta); $id = new FHIRId(); $id->setValue($dataRecord['uuid']); diff --git a/src/Services/FHIR/FhirAllergyIntoleranceService.php b/src/Services/FHIR/FhirAllergyIntoleranceService.php index 3fdc04ad0ff..717710fc722 100644 --- a/src/Services/FHIR/FhirAllergyIntoleranceService.php +++ b/src/Services/FHIR/FhirAllergyIntoleranceService.php @@ -30,6 +30,7 @@ use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\PractitionerService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\ReferenceSearchValue; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; @@ -76,9 +77,14 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('allergy_uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['modifydate']); + } /** * Parses an OpenEMR allergyIntolerance record, returning the equivalent FHIR AllergyIntolerance Resource @@ -113,7 +119,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $allergyIntoleranceResource = new FHIRAllergyIntolerance(); $fhirMeta = new FHIRMeta(); $fhirMeta->setVersionId("1"); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['modifydate'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $allergyIntoleranceResource->setMeta($fhirMeta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirAppointmentService.php b/src/Services/FHIR/FhirAppointmentService.php index 658c71544b1..2c07df49680 100644 --- a/src/Services/FHIR/FhirAppointmentService.php +++ b/src/Services/FHIR/FhirAppointmentService.php @@ -65,11 +65,16 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('pc_uuid', ServiceField::TYPE_UUID)]), - '_lastUpdated' => new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['pc_time']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATE, ['pc_eventDate']), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['pc_time']); + } + /** * Parses an OpenEMR data record, returning the equivalent FHIR Resource * diff --git a/src/Services/FHIR/FhirCarePlanService.php b/src/Services/FHIR/FhirCarePlanService.php index ff6411e3f6f..1d70b3f2bab 100644 --- a/src/Services/FHIR/FhirCarePlanService.php +++ b/src/Services/FHIR/FhirCarePlanService.php @@ -23,6 +23,7 @@ use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait; use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -57,9 +58,16 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('status', SearchFieldType::TOKEN, ['careplan_category']), // note even though we label this as a uuid, it is a SURROGATE UID because of the nature of CarePlan '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, ['uuid']), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + // TODO: @adunsulag introduce a last_modified date field to the care plan table as we don't track this anywhere + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['creation_date']); + } + /** * Parses an OpenEMR record, returning the equivalent FHIR Resource * @@ -73,7 +81,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $fhirMeta = new FHIRMeta(); $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['creation_date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['creation_date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $carePlanResource->setMeta($fhirMeta); $fhirId = new FHIRId(); diff --git a/src/Services/FHIR/FhirCareTeamService.php b/src/Services/FHIR/FhirCareTeamService.php index 46fcd3d084f..663cae8b8e6 100644 --- a/src/Services/FHIR/FhirCareTeamService.php +++ b/src/Services/FHIR/FhirCareTeamService.php @@ -24,6 +24,7 @@ use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait; use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -67,9 +68,15 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'status' => new FhirSearchParameterDefinition('status', SearchFieldType::TOKEN, ['care_team_status']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Parses an OpenEMR careTeam record, returning the equivalent FHIR CareTeam Resource * @@ -83,7 +90,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $fhirMeta = new FHIRMeta(); $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $careTeamResource->setMeta($fhirMeta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirConditionService.php b/src/Services/FHIR/FhirConditionService.php index 874485b5ab9..09e902751d1 100644 --- a/src/Services/FHIR/FhirConditionService.php +++ b/src/Services/FHIR/FhirConditionService.php @@ -14,6 +14,7 @@ use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait; use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -58,9 +59,15 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('condition_uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated_time']); + } + /** * Parses an OpenEMR condition record, returning the equivalent FHIR Condition Resource * @@ -74,7 +81,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_updated_time'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated_time'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $conditionResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirCoverageService.php b/src/Services/FHIR/FhirCoverageService.php index 2d4483f8f9c..7ecd6030e87 100644 --- a/src/Services/FHIR/FhirCoverageService.php +++ b/src/Services/FHIR/FhirCoverageService.php @@ -6,6 +6,7 @@ use OpenEMR\FHIR\R4\FHIRElement\FHIRCoding; use OpenEMR\FHIR\R4\FHIRElement\FHIRCode; use OpenEMR\FHIR\R4\FHIRElement\FHIRId; +use OpenEMR\FHIR\R4\FHIRElement\FHIRMeta; use OpenEMR\FHIR\R4\FHIRElement\FHIRReference; use OpenEMR\FHIR\R4\FHIRDomainResource\FHIRCoverage; use OpenEMR\Services\FHIR\FhirServiceBase; @@ -13,6 +14,7 @@ use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\InsuranceService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -53,10 +55,16 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), 'payor' => new FhirSearchParameterDefinition('payor', SearchFieldType::TOKEN, ['provider']), - '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]) + '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Parses an OpenEMR Insurance record, returning the equivalent FHIR Coverage Resource * @@ -67,7 +75,13 @@ protected function loadSearchParameters() public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $coverageResource = new FHIRCoverage(); - $meta = array('versionId' => '1', 'lastUpdated' => UtilsService::getDateFormattedAsUTC()); + $meta = new FHIRMeta(); + $meta->setVersionId('1'); + if (!empty($dataRecord['date'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $coverageResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirDeviceService.php b/src/Services/FHIR/FhirDeviceService.php index b9eb9560830..d65b79fa2a7 100644 --- a/src/Services/FHIR/FhirDeviceService.php +++ b/src/Services/FHIR/FhirDeviceService.php @@ -14,12 +14,14 @@ use OpenEMR\FHIR\R4\FHIRDomainResource\FHIRDevice; use OpenEMR\FHIR\R4\FHIRElement\FHIRDateTime; use OpenEMR\FHIR\R4\FHIRElement\FHIRId; +use OpenEMR\FHIR\R4\FHIRElement\FHIRMeta; use OpenEMR\FHIR\R4\FHIRResource\FHIRDevice\FHIRDeviceUdiCarrier; use OpenEMR\Services\DeviceService; use OpenEMR\Services\FHIR\Traits\BulkExportSupportAllOperationsTrait; use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait; use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -52,9 +54,15 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['modifydate']); + } + /** * Parses an OpenEMR data record, returning the equivalent FHIR Resource * @@ -66,7 +74,14 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $device = new FHIRDevice(); - $device->setMeta(UtilsService::createFhirMeta('1', UtilsService::getDateFormattedAsUTC())); + $fhirMeta = new FHIRMeta(); + $fhirMeta->setVersionId('1'); + if (!empty($dataRecord['modifydate'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['modifydate'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $device->setMeta($fhirMeta); $id = new FHIRId(); $id->setValue($dataRecord['uuid']); diff --git a/src/Services/FHIR/FhirDiagnosticReportService.php b/src/Services/FHIR/FhirDiagnosticReportService.php index 8aacf715a01..38097e641b8 100644 --- a/src/Services/FHIR/FhirDiagnosticReportService.php +++ b/src/Services/FHIR/FhirDiagnosticReportService.php @@ -20,6 +20,7 @@ use OpenEMR\Services\FHIR\Traits\MappedServiceCodeTrait; use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldException; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; @@ -50,11 +51,19 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'code' => new FhirSearchParameterDefinition('code', SearchFieldType::TOKEN, ['code']), 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), + // shouldn't be a problem if date and _lastUpdated are provided as it will just be ignored with duplicate WHERE clause conditions + // TODO: @adunsulag test this assumption to make sure it is correct 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. * @param $fhirSearchParameters The FHIR resource search parameters diff --git a/src/Services/FHIR/FhirDocumentReferenceService.php b/src/Services/FHIR/FhirDocumentReferenceService.php index 7c163097d12..78b63e84e7b 100644 --- a/src/Services/FHIR/FhirDocumentReferenceService.php +++ b/src/Services/FHIR/FhirDocumentReferenceService.php @@ -20,6 +20,7 @@ use OpenEMR\Services\FHIR\Traits\MappedServiceCodeTrait; use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldException; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; @@ -54,12 +55,20 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'type' => new FhirSearchParameterDefinition('type', SearchFieldType::TOKEN, ['type']), 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), + // shouldn't be a problem if date and _lastUpdated are provided as it will just be ignored with duplicate WHERE clause conditions + // TODO: @adunsulag test this assumption to make sure it is correct 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), // it will search all the services, but since we are only grabbing a single id this should be relatively fast '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. * @param $fhirSearchParameters The FHIR resource search parameters diff --git a/src/Services/FHIR/FhirEncounterService.php b/src/Services/FHIR/FhirEncounterService.php index 0bfada182ee..5cccdb6ca4a 100644 --- a/src/Services/FHIR/FhirEncounterService.php +++ b/src/Services/FHIR/FhirEncounterService.php @@ -39,6 +39,7 @@ use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -95,10 +96,15 @@ protected function loadSearchParameters() ), 'patient' => $this->getPatientContextSearchField(), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), - '_lastUpdated' => new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_update']) + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_update']); + } + /** * Parses an OpenEMR patient record, returning the equivalent FHIR Patient Resource * https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-us-core-encounter-definitions.html diff --git a/src/Services/FHIR/FhirGoalService.php b/src/Services/FHIR/FhirGoalService.php index fc523b76a3d..2ee3c321b89 100644 --- a/src/Services/FHIR/FhirGoalService.php +++ b/src/Services/FHIR/FhirGoalService.php @@ -25,6 +25,7 @@ use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait; use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -59,15 +60,22 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), // note even though we label this as a uuid, it is a SURROGATE UID because of the nature of how goals are stored '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, ['uuid']), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + // TODO: @adunsulag introduce a last_modified date field to the care plan table as we don't track this anywhere + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['creation_date']); + } + /** * Parses an OpenEMR careTeam record, returning the equivalent FHIR CareTeam Resource * * @param array $dataRecord The source OpenEMR data record * @param boolean $encode Indicates if the returned resource is encoded into a string. Defaults to false. - * @return FHIRCareTeam + * @return FHIRGoal */ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { @@ -75,7 +83,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $fhirMeta = new FHIRMeta(); $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['creation_date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['creation_date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $goal->setMeta($fhirMeta); $fhirId = new FHIRId(); diff --git a/src/Services/FHIR/FhirGroupService.php b/src/Services/FHIR/FhirGroupService.php index ab1b3c7d854..5f258fafb7f 100644 --- a/src/Services/FHIR/FhirGroupService.php +++ b/src/Services/FHIR/FhirGroupService.php @@ -19,6 +19,7 @@ use OpenEMR\Services\FHIR\Traits\MappedServiceTrait; use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldException; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; @@ -48,9 +49,15 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. * @param $fhirSearchParameters The FHIR resource search parameters diff --git a/src/Services/FHIR/FhirImmunizationService.php b/src/Services/FHIR/FhirImmunizationService.php index b00d0a859e8..9f4db5a09ab 100644 --- a/src/Services/FHIR/FhirImmunizationService.php +++ b/src/Services/FHIR/FhirImmunizationService.php @@ -62,9 +62,15 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('uuid', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['update_date']); + } + /** * Parses an OpenEMR immunization record, returning the equivalent FHIR Immunization Resource * @@ -78,7 +84,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['update_date'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['update_date'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $immunizationResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirLocationService.php b/src/Services/FHIR/FhirLocationService.php index f3c23e97931..9778bc06c29 100644 --- a/src/Services/FHIR/FhirLocationService.php +++ b/src/Services/FHIR/FhirLocationService.php @@ -59,10 +59,16 @@ public function __construct() protected function loadSearchParameters() { return [ - '_id' => new FhirSearchParameterDefinition('uuid', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]) + '_id' => new FhirSearchParameterDefinition('uuid', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Parses an OpenEMR location record, returning the equivalent FHIR Location Resource * @@ -76,7 +82,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $locationResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirMedicationRequestService.php b/src/Services/FHIR/FhirMedicationRequestService.php index 0c52b04e44f..674804743af 100644 --- a/src/Services/FHIR/FhirMedicationRequestService.php +++ b/src/Services/FHIR/FhirMedicationRequestService.php @@ -109,9 +109,15 @@ protected function loadSearchParameters() 'intent' => new FhirSearchParameterDefinition('intent', SearchFieldType::TOKEN, ['intent']), 'status' => new FhirSearchParameterDefinition('status', SearchFieldType::TOKEN, ['status']), '_id' => new FhirSearchParameterDefinition('uuid', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date_modified']); + } + /** * Parses an OpenEMR prescription record, returning the equivalent FHIR Patient Resource * @@ -125,7 +131,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['date_modified'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date_modified'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $medRequestResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirMedicationService.php b/src/Services/FHIR/FhirMedicationService.php index 864c1ba6a98..9adbf598125 100644 --- a/src/Services/FHIR/FhirMedicationService.php +++ b/src/Services/FHIR/FhirMedicationService.php @@ -48,9 +48,15 @@ protected function loadSearchParameters() { return [ '_id' => new FhirSearchParameterDefinition('uuid', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['drug_last_updated']); + } + /** * Parses an OpenEMR medication record, returning the equivalent FHIR Medication Resource * @@ -64,7 +70,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['drug_last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['drug_last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $medicationResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirObservationService.php b/src/Services/FHIR/FhirObservationService.php index f3af4cef617..493d390cf7e 100644 --- a/src/Services/FHIR/FhirObservationService.php +++ b/src/Services/FHIR/FhirObservationService.php @@ -76,9 +76,15 @@ protected function loadSearchParameters(): array 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, ['uuid']), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date_modified']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. * @param $fhirSearchParameters The FHIR resource search parameters diff --git a/src/Services/FHIR/FhirOrganizationService.php b/src/Services/FHIR/FhirOrganizationService.php index cba23a2ef86..e600a56d5a3 100644 --- a/src/Services/FHIR/FhirOrganizationService.php +++ b/src/Services/FHIR/FhirOrganizationService.php @@ -81,10 +81,16 @@ public function getSearchParams() 'address-city' => new FhirSearchParameterDefinition('address-city', SearchFieldType::STRING, ['city']), 'address-postalcode' => new FhirSearchParameterDefinition('address-postalcode', SearchFieldType::STRING, ['postal_code', "zip"]), 'address-state' => new FhirSearchParameterDefinition('address-state', SearchFieldType::STRING, ['state']), - 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']) + 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + public function getOne($fhirResourceId, $puuidBind = null): ProcessingResult { return $this->getAll(['_id' => $fhirResourceId], $puuidBind); diff --git a/src/Services/FHIR/FhirPatientService.php b/src/Services/FHIR/FhirPatientService.php index 573fad5869d..cdde9982cdd 100644 --- a/src/Services/FHIR/FhirPatientService.php +++ b/src/Services/FHIR/FhirPatientService.php @@ -90,6 +90,8 @@ class FhirPatientService extends FhirServiceBase implements IFhirExportableResou const FIELD_NAME_GENDER = 'sex'; + private ?array $searchParameters = null; + public function __construct() { parent::__construct(); @@ -143,11 +145,16 @@ protected function loadSearchParameters() 'given' => new FhirSearchParameterDefinition('given', SearchFieldType::STRING, ['fname', 'mname']), 'phone' => new FhirSearchParameterDefinition('phone', SearchFieldType::TOKEN, ['phone_home', 'phone_biz', 'phone_cell']), 'telecom' => new FhirSearchParameterDefinition('telecom', SearchFieldType::TOKEN, ['email','email_direct', 'phone_home', 'phone_biz', 'phone_cell']), - '_lastUpdated' => new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']), + '_lastUpdated' => $this->getLastModifiedSearchField(), 'generalPractitioner' => new FhirSearchParameterDefinition('generalPractitioner', SearchFieldType::REFERENCE, ['provider_uuid']) ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Parses an OpenEMR patient record, returning the equivalent FHIR Patient Resource * @@ -161,8 +168,8 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - if (!empty($dataRecord['date'])) { - $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); } else { $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); } @@ -172,7 +179,7 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $id = new FHIRId(); $id->setValue($dataRecord['uuid']); $patientResource->setId($id); - $patientResource->setDeceasedBoolean($dataRecord[ 'deceasedDate' ] != null); + $patientResource->setDeceasedBoolean($dataRecord[ 'deceased_date' ] != null); $this->parseOpenEMRPatientSummaryText($patientResource, $dataRecord); $this->parseOpenEMRPatientName($patientResource, $dataRecord); diff --git a/src/Services/FHIR/FhirPersonService.php b/src/Services/FHIR/FhirPersonService.php index b5dafac4a4b..025a6fb6d86 100644 --- a/src/Services/FHIR/FhirPersonService.php +++ b/src/Services/FHIR/FhirPersonService.php @@ -67,10 +67,16 @@ protected function loadSearchParameters() 'given' => new FhirSearchParameterDefinition('given', SearchFieldType::STRING, ["fname", "mname"]), 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ["users.title", "fname", "mname", "lname"]), - '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]) + '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Parses an OpenEMR user record, returning the equivalent FHIR Person Resource @@ -85,7 +91,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $person->setMeta($meta); $person->setActive($dataRecord['active'] == "1" ? true : false); diff --git a/src/Services/FHIR/FhirPractitionerRoleService.php b/src/Services/FHIR/FhirPractitionerRoleService.php index 4e75a93db6c..23b72168bc3 100644 --- a/src/Services/FHIR/FhirPractitionerRoleService.php +++ b/src/Services/FHIR/FhirPractitionerRoleService.php @@ -46,10 +46,21 @@ protected function loadSearchParameters() return [ 'specialty' => new FhirSearchParameterDefinition('specialty', SearchFieldType::TOKEN, ['specialty_code']), 'practitioner' => new FhirSearchParameterDefinition('practitioner', SearchFieldType::STRING, ['user_name']), - '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('providers.facility_role_uuid', ServiceField::TYPE_UUID)]) + '_id' => new FhirSearchParameterDefinition( + '_id', + SearchFieldType::TOKEN, + [new ServiceField('providers.facility_role_uuid', ServiceField::TYPE_UUID)] + ), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + // we just go off of role as specialty gets updated at the same time + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['role_last_updated']); + } + /** * Parses an OpenEMR practitionerRole record, returning the equivalent FHIR PractitionerRole Resource * @@ -63,7 +74,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['role_last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['role_last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $practitionerRoleResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirPractitionerService.php b/src/Services/FHIR/FhirPractitionerService.php index 41fc9539cbb..7652ff43e6c 100644 --- a/src/Services/FHIR/FhirPractitionerService.php +++ b/src/Services/FHIR/FhirPractitionerService.php @@ -64,10 +64,18 @@ protected function loadSearchParameters() 'address-state' => new FhirSearchParameterDefinition('address-state', SearchFieldType::STRING, ['state']), 'family' => new FhirSearchParameterDefinition('family', SearchFieldType::STRING, ["lname"]), 'given' => new FhirSearchParameterDefinition('given', SearchFieldType::STRING, ["fname", "mname"]), - 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ["title", "fname", "mname", "lname"]) + 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ["title", "fname", "mname", "lname"]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + // TODO: @adunsulag I don't like specifying full table name here in the search field, but I don't see a way around it + // right now... if we ever need to implement better escaping this is an issue. + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['users.last_updated']); + } + /** * Parses an OpenEMR practitioner record, returning the equivalent FHIR Practitioner Resource @@ -82,7 +90,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $practitionerResource->setMeta($meta); $practitionerResource->setActive($dataRecord['active'] == "1" ? true : false); diff --git a/src/Services/FHIR/FhirProcedureService.php b/src/Services/FHIR/FhirProcedureService.php index 0ed8ce946fa..700d061f34a 100644 --- a/src/Services/FHIR/FhirProcedureService.php +++ b/src/Services/FHIR/FhirProcedureService.php @@ -70,9 +70,15 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['report_date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. diff --git a/src/Services/FHIR/FhirProvenanceService.php b/src/Services/FHIR/FhirProvenanceService.php index d007cf4fdaa..317eab65212 100644 --- a/src/Services/FHIR/FhirProvenanceService.php +++ b/src/Services/FHIR/FhirProvenanceService.php @@ -230,7 +230,7 @@ public function getAll($fhirSearchParameters, $puuidBind = null): ProcessingResu if (!empty($fhirSearchParameters['_id'])) { $fhirSearchResult = $this->getProvenanceRecordsForId($fhirSearchParameters['_id'], $puuidBind); } else { - $fhirSearchResult = $this->getAllProvenanceRecordsFromServices($puuidBind); + $fhirSearchResult = $this->getAllProvenanceRecordsFromServices($fhirSearchParameters, $puuidBind); } } catch (SearchFieldException $exception) { $systemLogger = new SystemLogger(); @@ -242,13 +242,15 @@ public function getAll($fhirSearchParameters, $puuidBind = null): ProcessingResu return $fhirSearchResult; } - private function getAllProvenanceRecordsFromServices($puuidBind = null) + private function getAllProvenanceRecordsFromServices(array $fhirSearchParameters, $puuidBind = null) { $processingResult = new ProcessingResult(); if (empty($this->serviceLocator)) { (new SystemLogger())->errorLogCaller("class was not properly configured with the service locator"); } + $searchParams = $this->filterSupportedSearchParams($fhirSearchParameters); + // we only return provenances for $servicesByResource = $this->serviceLocator->findServices(IResourceUSCIGProfileService::class); @@ -258,13 +260,13 @@ private function getAllProvenanceRecordsFromServices($puuidBind = null) continue; } try { - $this->addAllProvenanceRecordsForService($processingResult, $service, [], $puuidBind); + $this->addAllProvenanceRecordsForService($processingResult, $service, $searchParams, $puuidBind); } catch (SearchFieldException $ex) { $systemLogger = new SystemLogger(); - $systemLogger->error(get_class($this) . "->getAll() exception thrown", ['message' => $exception->getMessage(), - 'field' => $exception->getField(), 'trace' => $exception->getTraceAsString()]); + $systemLogger->error(get_class($this) . "->getAll() exception thrown", ['message' => $ex->getMessage(), + 'field' => $ex->getField(), 'trace' => $ex->getTraceAsString()]); // put our exception information here - $processingResult->setValidationMessages([$exception->getField() => $exception->getMessage()]); + $processingResult->setValidationMessages([$ex->getField() => $ex->getMessage()]); return $processingResult; } catch (Exception $ex) { $systemLogger = new SystemLogger(); @@ -333,57 +335,6 @@ private function getProvenanceRecordsForId($id, $puuidBind) return $processingResult; } - /** - * Searches for OpenEMR records using OpenEMR search parameters - * @param openEMRSearchParameters OpenEMR search fields - * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid. - * @return OpenEMR records - */ - protected function searchForOpenEMRRecords($openEMRSearchParameters): ProcessingResult - { - $patientToken = $openEMRSearchParameters['patient'] ?? new TokenSearchField('patient', []); - $patientBinding = !empty($patientToken->getValues()) ? $patientToken->getValues()[0]->getCode() : null; - /** - * @var TokenSearchField - */ - $id = $openEMRSearchParameters['_id'] ?? new TokenSearchField('_id', []); - $processingResult = new ProcessingResult(); - foreach ($id->getValues() as $value) { - // should be in format of ResourceType/uuid - $code = $value->getCode() ?? ""; - try { - $idParts = explode(":", $code); - $resourceName = array_shift($idParts); - - $innerId = implode(":", $idParts); - $className = RestControllerHelper::FHIR_SERVICES_NAMESPACE . $resourceName . "Service"; - if (class_exists($className)) { - $newServiceClass = new $className(); - if ($newServiceClass instanceof IResourceReadableService) { - $searchParams = [ - '_id' => $innerId - ,'_revinclude' => 'Provenance:target' - ]; - $results = $newServiceClass->getAll($searchParams, $patientBinding); - if ($results->hasData()) { - foreach ($results->getData() as $datum) { - if ($datum instanceof FHIRProvenance) { - $processingResult->addData($datum); - } - } - } else { - $processingResult->addProcessingResult($results); - } - } - } - } catch (\Exception $exception) { - // TODO: @adunsulag log the exception - $processingResult->addInternalError("Server error occurred in returning provenance for _id " . $code); - } - } - return $processingResult; - } - /** * Returns the Canonical URIs for the FHIR resource for each of the US Core Implementation Guide Profiles that the * resource implements. Most resources have only one profile, but several like DiagnosticReport and Observation @@ -416,7 +367,9 @@ public function getSurrogateKeyForResource(FHIRDomainResource $resource) "Resource missing required Meta->lastUpdated field", ['resource' => $resource->getId(), 'type' => $resource->get_fhirElementName()] ); - } else { + // patients were the only ones who actually were tracking a valid last updated date instead of the most + // current timestamp for V1 so we need to check for that, everything else is V2 as last updated wasn't really tracked. + } else if ($resource->get_fhirElementName() === 'Patient') { // we use DATE_ATOM to get an ISO8601 compatible date as DATE_ISO8601 does not actually conform to an ISO8601 date for php legacy purposes $lastUpdated = \DateTime::createFromFormat(DATE_ATOM, $resource->getMeta()->getLastUpdated()); @@ -490,23 +443,43 @@ public function export(ExportStreamWriter $writer, ExportJob $job, $lastResource $searchParams[$searchField->getName()] = implode(",", $patientUuids); } } - - $serviceResult = $service->getAll($searchParams); - // now loop through and grab all of our provenance resources - if ($serviceResult->hasData()) { - foreach ($serviceResult->getData() as $record) { - if (!($record instanceof FHIRDomainResource)) { - throw new ExportException(self::class . " returned records that are not a valid fhir resource type for this class", 0, $lastResourceIdExported); - } - // we only want to write out provenance records - if (!($record instanceof FHIRProvenance)) { - continue; + $searchParams['_lastUpdated'] = $job->getResourceIncludeSearchParamValue(); + try { + $serviceResult = $service->getAll($searchParams); + // now loop through and grab all of our provenance resources + if ($serviceResult->hasData()) { + foreach ($serviceResult->getData() as $record) { + if (!($record instanceof FHIRDomainResource)) { + throw new ExportException(self::class . " returned records that are not a valid fhir resource type for this class", 0, $lastResourceIdExported); + } + // we only want to write out provenance records + if (!($record instanceof FHIRProvenance)) { + continue; + } + $writer->append($record); + $lastResourceIdExported = $record->getId(); } - $writer->append($record); - $lastResourceIdExported = $record->getId(); } + } catch (SearchFieldException $exception) { + $message = $exception->getMessage() . " Search Field " . $exception->getField(); + throw new ExportException($message, 0, $lastResourceIdExported); } } } } + + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + // nothing to really do here as we handle it internally in the export operation + return null; + } + + private function filterSupportedSearchParams(array $fhirSearchParameters) + { + $supportedParams = []; + if (isset($fhirSearchParameters['_lastUpdated'])) { + $supportedParams['_lastUpdated'] = $fhirSearchParameters['_lastUpdated']; + } + return $supportedParams; + } } diff --git a/src/Services/FHIR/FhirValueSetService.php b/src/Services/FHIR/FhirValueSetService.php index 31f26b4df7f..d9659511b73 100644 --- a/src/Services/FHIR/FhirValueSetService.php +++ b/src/Services/FHIR/FhirValueSetService.php @@ -85,13 +85,13 @@ class FhirValueSetService extends FhirServiceBase implements IResourceUSCIGProfi */ - const USCGI_PROFILE_URI = 'http://hl7.org/fhir/StructureDefinition/shareablevalueset'; const APPOINTMENT_TYPE = 'appointment-type'; public function __construct() { parent::__construct(); + // TODO: @adunsulag we need to look at adding a mapping service here in order to get our value sets out. $this->appointmentService = new AppointmentService(); $this->listOptionService = new ListService(); } @@ -103,9 +103,20 @@ protected function loadSearchParameters() { return [ '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('id', ServiceField::TYPE_STRING)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['sublist_updated_date', 'last_updated']); + } + + private function getLastModifiedSearchFieldForAppointmentCategories() + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['pc_last_updated']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. * @param $fhirSearchParameters The FHIR resource search parameters @@ -115,74 +126,14 @@ public function getAll($fhirSearchParameters, $puuidBind = null): ProcessingResu { $fhirSearchResult = new ProcessingResult(); try { - if ( - !isset($fhirSearchParameters[ '_id' ]) - // could be array (AND) or comma-delimited string value (OR) - // check array first but should only be len 1 ("AND", becuase cannot be 2 simultaneous) - || ( is_array($fhirSearchParameters[ '_id' ]) - && count($fhirSearchParameters[ '_id' ]) == 1 - && $fhirSearchParameters[ '_id' ][ 0 ] == self::APPOINTMENT_TYPE ) - // and string which could be comma-delimiter OR of exploded values - || ( !is_array($fhirSearchParameters[ '_id' ]) - && in_array(self::APPOINTMENT_TYPE, explode(",", $fhirSearchParameters[ '_id' ])) ) - ) { - $calendarCategories = $this->appointmentService->getCalendarCategories(); - $valueSet = new FHIRValueSet(); - $valueSet->setId(self::APPOINTMENT_TYPE); - $compose = new FHIRValueSetCompose(); - $include = new FHIRValueSetInclude(); - foreach ($calendarCategories as $category) { - if ($category["pc_cattype"] != 0) { - continue; // only cat_type==0 - } - $concept = new FHIRValueSetConcept(); - $code = new FHIRCode(); - $code->setValue($category[ "pc_constant_id"]); - $concept->setCode($code); - $concept->setDisplay($category[ "pc_catname" ]); - $include->addConcept($concept); - } - $compose->addInclude($include); - $valueSet->setCompose($compose); - $fhirSearchResult->addData($valueSet); + // we don't really deal with provenance for ValueSet pieces so we will ignore this property + if (isset($fhirSearchParameters['_revinclude'])) { + unset($fhirSearchParameters['_revinclude']); } - // Now the same for list_options selected in $listNames - $list_ids = $this->listOptionService->getListIds(); - foreach ($list_ids as $listName) { - if ( - isset($fhirSearchParameters[ '_id' ]) - // could be array (AND) or comma-delimited string value (OR) - // check array first but should only be len 1 ("AND", becuase cannot be 2 simultaneous) - && ( ( is_array($fhirSearchParameters[ '_id' ]) - && count($fhirSearchParameters[ '_id' ]) == 1 - && $fhirSearchParameters[ '_id' ][ 0 ] != $listName ) - // and string which could be comma-delimiter OR of exploded values - || ( !is_array($fhirSearchParameters[ '_id' ]) - && !in_array($listName, explode(",", $fhirSearchParameters[ '_id' ])) ) ) - ) { - continue; - } - $options = $this->listOptionService->getOptionsByListName($listName); // does not return title - if (count($options) == 0) { - continue; - } - $valueSet = new FHIRValueSet(); - $valueSet->setId($listName); - $compose = new FHIRValueSetCompose(); - $include = new FHIRValueSetInclude(); - foreach ($options as $option) { - $concept = new FHIRValueSetConcept(); - $code = new FHIRCode(); - $code->setValue($option[ "option_id"]); - $concept->setCode($code); - $concept->setDisplay($option[ "title" ]); - $include->addConcept($concept); - } - $compose->addInclude($include); - $valueSet->setCompose($compose); - $fhirSearchResult->addData($valueSet); - } + $this->addAppointmentCategoriesValueSetForSearch($fhirSearchResult, $fhirSearchParameters); + + $this->addListOptionsValueSetsForSearch($fhirSearchResult, $fhirSearchParameters, $puuidBind); } catch (SearchFieldException $exception) { (new SystemLogger())->errorLogCaller("search exception thrown", ['message' => $exception->getMessage(), 'field' => $exception->getField()]); @@ -204,4 +155,82 @@ function getProfileURIs(): array { return [self::USCGI_PROFILE_URI]; } + + private function addAppointmentCategoriesValueSetForSearch(ProcessingResult $fhirSearchResult, array $fhirSearchParameters, string $puuidBind = null) + { + $this->getSearchFieldFactory()->setSearchFieldDefinition('_lastUpdated', $this->getLastModifiedSearchFieldForAppointmentCategories()); + $oeSearchParameters = $this->createOpenEMRSearchParameters($fhirSearchParameters, $puuidBind); + if ( + !isset($oeSearchParameters['_id']) + // could be array (AND) or comma-delimited string value (OR) + // check array first but should only be len 1 ("AND", becuase cannot be 2 simultaneous) + || $oeSearchParameters['_id']->hasCodeValue(self::APPOINTMENT_TYPE) + ) { + if (!isset($oeSearchParameters['_id'])) { + // if we have any match on categories we want to return everything... hate the double db call + // but rather than mess with a complex query we will just do it this way + $processingResult = $this->appointmentService->searchCalendarCategories($oeSearchParameters); + // nothing to do here as we have no categories matching so we return + if (!$processingResult->hasData()) { + return $fhirSearchResult; + } + } + $calendarCategories = $this->appointmentService->getCalendarCategories(); + $valueSet = new FHIRValueSet(); + $valueSet->setId(self::APPOINTMENT_TYPE); + $compose = new FHIRValueSetCompose(); + $include = new FHIRValueSetInclude(); + foreach ($calendarCategories as $category) { + if ($category["pc_cattype"] != 0) { + continue; // only cat_type==0 + } + $concept = new FHIRValueSetConcept(); + $code = new FHIRCode(); + $code->setValue($category["pc_constant_id"]); + $concept->setCode($code); + $concept->setDisplay($category["pc_catname"]); + $include->addConcept($concept); + } + $compose->addInclude($include); + $valueSet->setCompose($compose); + $fhirSearchResult->addData($valueSet); + } + return $fhirSearchResult; + } + + private function addListOptionsValueSetsForSearch(ProcessingResult $fhirSearchResult, array $fhirSearchParameters, ?string $puuidBind = null) + { + $this->getSearchFieldFactory()->setSearchFieldDefinition('_lastUpdated', $this->getLastModifiedSearchField()); + $oeSearchParameters = $this->createOpenEMRSearchParameters($fhirSearchParameters, $puuidBind); + + // Now the same for list_options selected in $listNames + $listsResult = $this->listOptionService->searchLists($oeSearchParameters); + if (!$listsResult->hasData()) { + $fhirSearchResult->addProcessingResult($listsResult); + return $fhirSearchResult; + } + foreach ($listsResult->getData() as $listRecord) { + $listName = $listRecord["option_id"]; + $options = $this->listOptionService->getOptionsByListName($listName); // does not return title + if (count($options) == 0) { + continue; + } + $valueSet = new FHIRValueSet(); + $valueSet->setId($listName); + $compose = new FHIRValueSetCompose(); + $include = new FHIRValueSetInclude(); + foreach ($options as $option) { + $concept = new FHIRValueSetConcept(); + $code = new FHIRCode(); + $code->setValue($option["option_id"]); + $concept->setCode($code); + $concept->setDisplay($option["title"]); + $include->addConcept($concept); + } + $compose->addInclude($include); + $valueSet->setCompose($compose); + $fhirSearchResult->addData($valueSet); + } + return $fhirSearchResult; + } } diff --git a/src/Services/FHIR/Group/FhirPatientProviderGroupService.php b/src/Services/FHIR/Group/FhirPatientProviderGroupService.php index 38d559edf5a..d66565e3b9b 100644 --- a/src/Services/FHIR/Group/FhirPatientProviderGroupService.php +++ b/src/Services/FHIR/Group/FhirPatientProviderGroupService.php @@ -20,6 +20,7 @@ use OpenEMR\Services\FHIR\UtilsService; use OpenEMR\Services\GroupService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -45,9 +46,15 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['patient_last_updated']); + } + protected function searchForOpenEMRRecords($openEMRSearchParameters): ProcessingResult { return $this->service->searchPatientProviderGroups($openEMRSearchParameters); @@ -58,7 +65,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $fhirGroup = new FHIRGroup(); $fhirMeta = new FHIRMeta(); $fhirMeta->setVersionId("1"); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_modified_date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_modified_date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $fhirGroup->setMeta($fhirMeta); $fhirGroup->setId($dataRecord['uuid']); diff --git a/src/Services/FHIR/IFhirExportableResourceService.php b/src/Services/FHIR/IFhirExportableResourceService.php index a008468fcfb..6c56871261c 100644 --- a/src/Services/FHIR/IFhirExportableResourceService.php +++ b/src/Services/FHIR/IFhirExportableResourceService.php @@ -18,6 +18,8 @@ use OpenEMR\FHIR\Export\ExportJob; use OpenEMR\FHIR\Export\ExportStreamWriter; use OpenEMR\FHIR\Export\ExportWillShutdownException; +use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; interface IFhirExportableResourceService { @@ -59,4 +61,12 @@ function supportsGroupExport(); * @return bool true if this resource service should be called for a patient export operation, false otherwise */ function supportsPatientExport(); + + /** + * Returns the search field that represents the last modified date for the resource used in the export _since + * parameter for the export operation. If the resource does not support the _since parameter then this method + * will return null and the export should return ALL the resources for the resource service. + * @return ISearchField|null The search field that represents the last modified date for the resource + */ + function getLastModifiedSearchField(): ?FhirSearchParameterDefinition; } diff --git a/src/Services/FHIR/Observation/FhirObservationLaboratoryService.php b/src/Services/FHIR/Observation/FhirObservationLaboratoryService.php index b2f6aaad2df..61aa3bddc25 100644 --- a/src/Services/FHIR/Observation/FhirObservationLaboratoryService.php +++ b/src/Services/FHIR/Observation/FhirObservationLaboratoryService.php @@ -95,9 +95,15 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['report_date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('result_uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['report_date']); + } + /** * Searches for OpenEMR records using OpenEMR search parameters @@ -159,7 +165,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $observation = new FHIRObservation(); $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['report_date'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['report_date'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $observation->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/Observation/FhirObservationSocialHistoryService.php b/src/Services/FHIR/Observation/FhirObservationSocialHistoryService.php index fc64062dfc4..0ef55ca4d55 100644 --- a/src/Services/FHIR/Observation/FhirObservationSocialHistoryService.php +++ b/src/Services/FHIR/Observation/FhirObservationSocialHistoryService.php @@ -113,9 +113,15 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Inserts an OpenEMR record into the sytem. @@ -269,7 +275,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $observation = new FHIRObservation(); $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['date'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $observation->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/Observation/FhirObservationVitalsService.php b/src/Services/FHIR/Observation/FhirObservationVitalsService.php index 0c59793ac4d..6109b130676 100644 --- a/src/Services/FHIR/Observation/FhirObservationVitalsService.php +++ b/src/Services/FHIR/Observation/FhirObservationVitalsService.php @@ -241,9 +241,15 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Inserts an OpenEMR record into the sytem. @@ -342,6 +348,7 @@ private function parseVitalsIntoObservationRecords(ProcessingResult $processingR , "uuid" => UuidRegistry::uuidToString($uuidMappings[self::VITALS_PANEL_LOINC_CODE]) , "user_uuid" => $record['user_uuid'] , "date" => $record['date'] + , "last_updated" => $record['last_updated'] ]; foreach ($uuidMappings as $code => $uuid) { if (!$this->isVitalSignPanelCodes($code)) { // we will skip over our vital signs code, and any pediatric stuff @@ -365,6 +372,7 @@ private function parseVitalsIntoObservationRecords(ProcessingResult $processingR , "user_uuid" => $record['user_uuid'] ,"uuid" => UuidRegistry::uuidToString($uuidMappings[$code]) ,"date" => $record['date'] + , "last_updated" => $record['last_updated'] ]; $columns = $this->getColumnsForCode($code); @@ -421,7 +429,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $observation = new FHIRObservation(); $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $observation->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/Organization/FhirOrganizationFacilityService.php b/src/Services/FHIR/Organization/FhirOrganizationFacilityService.php index c8424c11a94..85fd9b93ddb 100644 --- a/src/Services/FHIR/Organization/FhirOrganizationFacilityService.php +++ b/src/Services/FHIR/Organization/FhirOrganizationFacilityService.php @@ -101,10 +101,16 @@ protected function loadSearchParameters() 'address-city' => new FhirSearchParameterDefinition('address-city', SearchFieldType::STRING, ['city']), 'address-postalcode' => new FhirSearchParameterDefinition('address-postalcode', SearchFieldType::STRING, ['postal_code', "zip"]), 'address-state' => new FhirSearchParameterDefinition('address-state', SearchFieldType::STRING, ['state']), - 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']) + 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Searches for OpenEMR records using OpenEMR search parameters * @param openEMRSearchParameters OpenEMR search fields @@ -160,10 +166,14 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $organizationResource = new FHIROrganization(); - $fhirMeta = new FHIRMeta(); - $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $organizationResource->setMeta($fhirMeta); + $meta = new FHIRMeta(); + $meta->setVersionId('1'); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $organizationResource->setMeta($meta); // facilities have no active / inactive state $organizationResource->setActive(true); diff --git a/src/Services/FHIR/Organization/FhirOrganizationInsuranceService.php b/src/Services/FHIR/Organization/FhirOrganizationInsuranceService.php index e5899daef0b..db6bdc6ff22 100644 --- a/src/Services/FHIR/Organization/FhirOrganizationInsuranceService.php +++ b/src/Services/FHIR/Organization/FhirOrganizationInsuranceService.php @@ -59,10 +59,16 @@ protected function loadSearchParameters() 'address-city' => new FhirSearchParameterDefinition('address-city', SearchFieldType::STRING, ['city']), 'address-postalcode' => new FhirSearchParameterDefinition('address-postalcode', SearchFieldType::STRING, ["zip"]), 'address-state' => new FhirSearchParameterDefinition('address-state', SearchFieldType::STRING, ['state']), - 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']) + 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + protected function searchForOpenEMRRecords($openEMRSearchParameters): ProcessingResult { if (!isset($openEMRSearchParameters['name'])) { @@ -91,10 +97,14 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $organizationResource = new FHIROrganization(); - $fhirMeta = new FHIRMeta(); - $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $organizationResource->setMeta($fhirMeta); + $meta = new FHIRMeta(); + $meta->setVersionId('1'); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $organizationResource->setMeta($meta); $organizationResource->setActive($dataRecord['inactive'] == '0'); $narrativeText = trim($dataRecord['name'] ?? ""); diff --git a/src/Services/FHIR/Organization/FhirOrganizationProcedureProviderService.php b/src/Services/FHIR/Organization/FhirOrganizationProcedureProviderService.php index 52aced40b2f..a7caed3aefa 100644 --- a/src/Services/FHIR/Organization/FhirOrganizationProcedureProviderService.php +++ b/src/Services/FHIR/Organization/FhirOrganizationProcedureProviderService.php @@ -55,10 +55,16 @@ protected function loadSearchParameters() { return [ '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), - 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']) + 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + protected function searchForOpenEMRRecords($openEMRSearchParameters): ProcessingResult { if (!isset($openEMRSearchParameters['name'])) { @@ -82,10 +88,14 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $organizationResource = new FHIROrganization(); - $fhirMeta = new FHIRMeta(); - $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $organizationResource->setMeta($fhirMeta); + $meta = new FHIRMeta(); + $meta->setVersionId('1'); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $organizationResource->setMeta($meta); $organizationResource->setActive($dataRecord['active'] == '1'); $narrativeText = trim($dataRecord['name'] ?? ""); diff --git a/src/Services/FHIR/Procedure/FhirProcedureOEProcedureService.php b/src/Services/FHIR/Procedure/FhirProcedureOEProcedureService.php index 12507f24be4..233abb18d5e 100644 --- a/src/Services/FHIR/Procedure/FhirProcedureOEProcedureService.php +++ b/src/Services/FHIR/Procedure/FhirProcedureOEProcedureService.php @@ -72,9 +72,15 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['report_date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('report_uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['report_date']); + } + /** * Searches for OpenEMR records using OpenEMR search parameters * @param openEMRSearchParameters OpenEMR search fields @@ -110,13 +116,17 @@ protected function searchForOpenEMRRecords($openEMRSearchParameters): Processing public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $procedureResource = new FHIRProcedure(); + $report = array_pop($dataRecord['reports']); $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($report['date'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($report['date'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $procedureResource->setMeta($meta); - $report = array_pop($dataRecord['reports']); $id = new FHIRId(); $id->setValue($report['uuid']); diff --git a/src/Services/FHIR/Procedure/FhirProcedureSurgeryService.php b/src/Services/FHIR/Procedure/FhirProcedureSurgeryService.php index 367e6df2e68..040e6a3f8e4 100644 --- a/src/Services/FHIR/Procedure/FhirProcedureSurgeryService.php +++ b/src/Services/FHIR/Procedure/FhirProcedureSurgeryService.php @@ -57,9 +57,15 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['begdate']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date_modified']); + } + /** * Searches for OpenEMR records using OpenEMR search parameters * @param openEMRSearchParameters OpenEMR search fields @@ -85,7 +91,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['date_modified'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date_modified'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $procedureResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/Traits/FhirBulkExportDomainResourceTrait.php b/src/Services/FHIR/Traits/FhirBulkExportDomainResourceTrait.php index 4629fd88247..68c408fc4b1 100644 --- a/src/Services/FHIR/Traits/FhirBulkExportDomainResourceTrait.php +++ b/src/Services/FHIR/Traits/FhirBulkExportDomainResourceTrait.php @@ -22,6 +22,10 @@ use OpenEMR\FHIR\R4\FHIRResource\FHIRDomainResource; use OpenEMR\Services\FHIR\IPatientCompartmentResourceService; use OpenEMR\Services\FHIR\IResourceReadableService; +use OpenEMR\Services\Search\DateSearchField; +use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; +use OpenEMR\Services\Search\SearchComparator; use OpenEMR\Services\Search\TokenSearchField; trait FhirBulkExportDomainResourceTrait @@ -68,6 +72,10 @@ public function export(ExportStreamWriter $writer, ExportJob $job, $lastResource $searchParams[$searchField->getName()] = implode(",", $patientUuids); } } + $searchField = $this->getLastModifiedSearchField(); + if ($searchField !== null) { + $searchParams[$searchField->getName()] = $job->getResourceIncludeSearchParamValue(); + } // if we can grab our list of patient ids from the export job... $processingResult = $this->getAll($searchParams); @@ -80,4 +88,9 @@ public function export(ExportStreamWriter $writer, ExportJob $job, $lastResource $lastResourceIdExported = $record->getId(); } } + + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return null; + } } diff --git a/src/Services/FHIR/UtilsService.php b/src/Services/FHIR/UtilsService.php index 8906169b2e8..2c570d0d12c 100644 --- a/src/Services/FHIR/UtilsService.php +++ b/src/Services/FHIR/UtilsService.php @@ -361,6 +361,20 @@ public static function getDateFormattedAsUTC(): string return (new \DateTime())->format(DATE_ATOM); } + public static function getLocalTimestampAsUTCDate($date) + { + // make this assumption explicit that we are using the current timezone specified in PHP + // when we use strtotime or gmdate we get bad behavior when dealing with DST + // we really should be storing dates internally as UTC instead of local time... but until that happens we have + // to do this. + // note this is what we were using before + // $date = gmdate('c', strtotime($dataRecord['date'])); + // w/ DST the date 2015-06-22 00:00:00 server time becomes 2015-06-22T04:00:00+00:00 w/o DST the server time becomes 2015-06-22T00:00:00-04:00 + $date = new \DateTime("@" . $date, new \DateTimeZone(date('P'))); + $utcDate = $date->format(DATE_ATOM); + return $utcDate; + } + public static function getLocalDateAsUTC($date) { // make this assumption explicit that we are using the current timezone specified in PHP diff --git a/src/Services/GroupService.php b/src/Services/GroupService.php index 5fc0ed3cbaa..cf6bd458816 100644 --- a/src/Services/GroupService.php +++ b/src/Services/GroupService.php @@ -46,7 +46,7 @@ public function searchPatientProviderGroups($search = array(), $isAndCondition = { // we inner join on status in case we ever decide to add a status property (and layers above this one can rely // on the property without changing code). - $sql = "SELECT + $sqlSelectFull = "SELECT patient_provider_groups.uuid ,patient_provider_groups.provider_id ,patient_provider_groups.provider_fname @@ -57,15 +57,20 @@ public function searchPatientProviderGroups($search = array(), $isAndCondition = ,patient_provider_groups.patient_fname ,patient_provider_groups.patient_mname ,patient_provider_groups.patient_lname - FROM ( + ,patient_provider_groups.creation_date + ,patient_provider_groups.patient_last_updated "; + $sqlIds = "SELECT DISTINCT patient_provider_groups.uuid "; + $sqlFrom = "FROM ( SELECT uuid_mapping.target_uuid AS pruuid ,uuid_mapping.uuid + ,uuid_mapping.created AS `creation_date` ,users.id AS provider_id ,users.fname AS provider_fname ,users.lname AS provider_lname ,users.mname AS provider_mname ,patients.uuid AS puuid + ,patients.last_updated AS patient_last_updated ,patients.title AS patient_title ,patients.fname AS patient_fname ,patients.mname AS patient_mname @@ -80,11 +85,18 @@ public function searchPatientProviderGroups($search = array(), $isAndCondition = $whereClause = FhirSearchWhereClauseBuilder::build($search, $isAndCondition); - $sql .= $whereClause->getFragment(); + $sqlIds .= $sqlFrom . $whereClause->getFragment(); $sqlBindArray = $whereClause->getBoundValues(); - $statementResults = QueryUtils::sqlStatementThrowException($sql, $sqlBindArray); - - $processingResult = $this->hydratePatientProviderSearchResultsFromQueryResource($statementResults); + $uuids = QueryUtils::fetchTableColumn($sqlIds, 'uuid', $sqlBindArray); + if (!empty($uuids)) { + // TODO: if we have a LARGE number of provider groups we will reach our max parameter count here... + // need to do optimization here for large # of providers. + $sqlSelectFull .= $sqlFrom . " WHERE patient_provider_groups.uuid IN (" . str_repeat("?, ", count($uuids) - 1) . "? )"; + $statementResults = QueryUtils::sqlStatementThrowException($sqlSelectFull, $uuids); + $processingResult = $this->hydratePatientProviderSearchResultsFromQueryResource($statementResults); + } else { + $processingResult = new ProcessingResult(); + } return $processingResult; } @@ -112,6 +124,7 @@ private function hydratePatientProviderSearchResultsFromQueryResource($queryReso $record = [ 'uuid' => $recordUuid ,'name' => $groupName + ,'last_modified_date' => $dbRecord['patient_last_updated'] ?? $dbRecord['creation_date'] ,'patients' => [] ]; $orderedList[] = $recordUuid; diff --git a/src/Services/ImmunizationService.php b/src/Services/ImmunizationService.php index fd0636d8526..3efc4c2a821 100644 --- a/src/Services/ImmunizationService.php +++ b/src/Services/ImmunizationService.php @@ -76,6 +76,7 @@ public function search($search, $isAndCondition = true) education_date, note, create_date, + update_date, amount_administered, amount_administered_unit, expiration_date, @@ -92,7 +93,7 @@ public function search($search, $isAndCondition = true) providers.provider_uuid, providers.provider_npi, providers.provider_username, - + IF( IF( information_source = 'new_immunization_record' AND @@ -133,7 +134,7 @@ public function search($search, $isAndCondition = true) notes AS refusal_reason_cdc_nip_code, codes AS refusal_reason_codes, title AS refusal_reason_description - FROM list_options + FROM list_options WHERE list_id = 'immunization_refusal_reason' ) refusal_reasons ON immunizations.refusal_reason = refusal_reasons.refusal_reason_id"; diff --git a/src/Services/InsuranceCompanyService.php b/src/Services/InsuranceCompanyService.php index 037f26aa7ca..f0d90acd33c 100644 --- a/src/Services/InsuranceCompanyService.php +++ b/src/Services/InsuranceCompanyService.php @@ -151,7 +151,9 @@ public function search($search, $isAndCondition = true) $sql .= " a.state,"; $sql .= " a.zip,"; $sql .= " a.plus_four,"; - $sql .= " a.country"; + $sql .= " a.country,"; + $sql .= " i.date_created,"; + $sql .= " i.last_updated"; $sql .= " FROM insurance_companies i "; $sql .= " LEFT JOIN (SELECT line1,line2,city,state,zip,plus_four,country,foreign_id FROM addresses) a ON i.id = a.foreign_id"; // the foreign_id here is a globally unique sequence so there is no conflict. diff --git a/src/Services/ListService.php b/src/Services/ListService.php index ab3bc55f472..2bbf7aef5d7 100644 --- a/src/Services/ListService.php +++ b/src/Services/ListService.php @@ -15,6 +15,12 @@ namespace OpenEMR\Services; use OpenEMR\Common\Database\QueryUtils; +use OpenEMR\Services\Search\FhirSearchWhereClauseBuilder; +use OpenEMR\Services\Search\SearchFieldException; +use OpenEMR\Services\Search\SearchModifier; +use OpenEMR\Services\Search\StringSearchField; +use OpenEMR\Services\Search\TokenSearchField; +use OpenEMR\Validators\ProcessingResult; use Particle\Validator\Validator; use OpenEMR\Common\Uuid\UuidRegistry; @@ -65,6 +71,50 @@ public function getListOptionsForLists($lists) return $records; } + /** + * Allows searching on the top level lists in the lists_options table. Will return the top level lists that match + * the search criteria as well as the last updated date of the sublist. + * @param $search + * @param $isAndCondition + * @return ProcessingResult + */ + public function searchLists($search, $isAndCondition = true) + { + // TODO: @adunsulag this is copy-pasta from BaseService... need to investigate if we can just have ListService extend BaseService + $processingResult = new ProcessingResult(); + try { + $sql = "SELECT + lo.*, + sub_list.sublist_updated_date + FROM + list_options lo + JOIN( + SELECT lo2.list_id AS sublist_list_id, + MAX(last_updated) AS sublist_updated_date + FROM + list_options lo2 + WHERE + lo2.list_id != 'lists' + GROUP BY + list_id + ) sub_list + ON + lo.option_id = sub_list.sublist_list_id "; + $whereFragment = FhirSearchWhereClauseBuilder::build($search, $isAndCondition); + $sql .= $whereFragment->getFragment() . " AND lo.list_id = 'lists' ORDER BY lo.seq, lo.list_id, lo.option_id "; + $records = QueryUtils::fetchRecords($sql, $whereFragment->getBoundValues()); + if (!empty($records)) { + foreach ($records as $row) { + $processingResult->addData($row); + } + } + } catch (SearchFieldException $exception) { + $processingResult->setValidationMessages([$exception->getField() => $exception->getMessage()]); + } + + return $processingResult; + } + public function getListIds() { $sql = "SELECT DISTINCT list_id FROM list_options ORDER BY list_id"; diff --git a/src/Services/LocationService.php b/src/Services/LocationService.php index a1cb52d82f5..44b8a929dbe 100644 --- a/src/Services/LocationService.php +++ b/src/Services/LocationService.php @@ -73,8 +73,9 @@ public function getAll($search = array(), $isAndCondition = true) null as fax, null as website, email, + `date` AS last_updated, "' . self::TYPE_PATIENT . '" AS `type` - from + from patient_data UNION SELECT uuid as table_uuid, @@ -88,8 +89,9 @@ public function getAll($search = array(), $isAndCondition = true) fax, website, email, + last_updated, "' . self::TYPE_FACILITY . '" AS `type` - from + from facility UNION SELECT uuid as table_uuid, @@ -103,8 +105,9 @@ public function getAll($search = array(), $isAndCondition = true) fax, url as website, email, + last_updated, "' . self::TYPE_USER . '" AS `type` - from + from users ) as location LEFT JOIN uuid_mapping ON uuid_mapping.target_uuid=location.table_uuid AND uuid_mapping.resource="Location"'; diff --git a/src/Services/PractitionerRoleService.php b/src/Services/PractitionerRoleService.php index da965b2f277..49e56954515 100644 --- a/src/Services/PractitionerRoleService.php +++ b/src/Services/PractitionerRoleService.php @@ -54,13 +54,18 @@ public function search($search, $isAndCondition = true) providers.user_name, providers.provider_id, providers.provider_uuid, + providers.provider_last_updated, facilities.facility_uuid, facilities.facility_name, role_codes.role_code, role_codes.role_title, + role_codes.role_last_updated, + specialty_codes.specialty_code, specialty_codes.specialty_title, + specialty_codes.specialty_last_updated, + physician_types.physician_type_codes, physician_types.physician_type, physician_types.physician_type_title @@ -68,13 +73,13 @@ public function search($search, $isAndCondition = true) select facility_user_ids.uuid AS facility_role_uuid, facility_user_ids.id AS facility_role_id, - -- field_value AS provider_id, facility_user_ids.facility_id, uid AS user_id, -- we are treating the user_id as the provider id -- TODO: @adunsulag figure out whether we should actually be using the user entered provider_id uid AS provider_id, users.uuid AS provider_uuid, + users.last_updated AS provider_last_updated, users.physician_type, CONCAT(COALESCE(users.fname,''), IF(users.mname IS NULL OR users.mname = '','',' '),COALESCE(users.mname,''), @@ -94,7 +99,9 @@ public function search($search, $isAndCondition = true) field_id, role.title AS role_title, facility_id, - uid AS user_id + uid AS user_id, + facility_user_ids.last_updated AS role_last_updated, + facility_user_ids.date_created AS role_date_created FROM facility_user_ids JOIN @@ -119,7 +126,9 @@ public function search($search, $isAndCondition = true) specialty.title AS specialty_title, field_id, facility_id, - uid AS user_id + uid AS user_id, + facilities_specialty.last_updated AS specialty_last_updated, + facilities_specialty.date_created AS specialty_date_created FROM facility_user_ids facilities_specialty JOIN diff --git a/src/Services/PrescriptionService.php b/src/Services/PrescriptionService.php index 6a4c013e9c6..ae87305b09f 100644 --- a/src/Services/PrescriptionService.php +++ b/src/Services/PrescriptionService.php @@ -82,7 +82,7 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n // order comes from our MedicationRequest intent value set, since we are only reporting on completed prescriptions // we will put the intent down as 'order' @see http://hl7.org/fhir/R4/valueset-medicationrequest-intent.html - $sql = "SELECT + $sql = "SELECT combined_prescriptions.uuid ,combined_prescriptions.source_table ,combined_prescriptions.drug @@ -100,6 +100,8 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,combined_prescriptions.note ,combined_prescriptions.status ,combined_prescriptions.drug_dosage_instructions + ,combined_prescriptions.date_added + ,combined_prescriptions.date_modified ,patient.puuid ,encounter.euuid ,practitioner.pruuid @@ -133,6 +135,7 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,IF(drugs.drug_code IS NULL, '', concat('RXCUI:',drugs.drug_code)) ) AS 'rxnorm_drugcode' ,date_added + ,date_modified ,COALESCE(prescriptions.unit,drugs.unit) AS unit ,prescriptions.`interval` ,COALESCE(prescriptions.`route`,drugs.`route`) AS 'route' @@ -142,12 +145,12 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,provider_id ,drugs.uuid AS drug_uuid ,prescriptions.drug_dosage_instructions - ,CASE + ,CASE WHEN prescriptions.end_date IS NOT NULL AND prescriptions.active = '1' THEN 'completed' WHEN prescriptions.active = '1' THEN 'active' ELSE 'stopped' END as 'status' - + FROM prescriptions LEFT JOIN @@ -166,6 +169,7 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,lists_medication.usage_category_title AS category_title ,lists.diagnosis AS rxnorm_drugcode ,`date` AS date_added + ,`modifydate` AS date_modified ,NULL as unit ,NULL as 'interval' ,NULL as `route` @@ -175,20 +179,20 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,users.id AS provider_id ,NULL as drug_uuid ,lists_medication.drug_dosage_instructions - ,CASE + ,CASE WHEN lists.enddate IS NOT NULL AND lists.activity = 1 THEN 'completed' WHEN lists.activity = 1 THEN 'active' ELSE 'stopped' END as 'status' FROM lists - LEFT JOIN + LEFT JOIN users ON users.username = lists.user LEFT JOIN lists_medication ON lists_medication.list_id = lists.id LEFT JOIN ( - select + select pid AS issues_encounter_pid , list_id AS issues_encounter_list_id -- lists have a 0..* relationship with issue_encounters which is a problem as FHIR treats medications as a 0.1 @@ -214,7 +218,7 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,title AS interval_title ,codes AS interval_codes FROM list_options - WHERE list_id='drug_route' + WHERE list_id='drug_route' ) intervals_list ON intervals_list.interval_id = combined_prescriptions.interval LEFT JOIN ( @@ -239,7 +243,7 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ) encounter ON encounter.encounter = combined_prescriptions.encounter LEFT JOIN ( - SELECT + SELECT id AS practitioner_id ,uuid AS pruuid FROM users diff --git a/src/Services/ProcedureProviderService.php b/src/Services/ProcedureProviderService.php index d89f9f240f7..60f7115ebd7 100644 --- a/src/Services/ProcedureProviderService.php +++ b/src/Services/ProcedureProviderService.php @@ -58,6 +58,8 @@ public function search($search, $isAndCondition = true) ,prov.lab_director ,prov.active ,prov.type + ,prov.last_updated + ,prov.date_created FROM procedure_providers prov "; diff --git a/src/Services/Search/DateSearchField.php b/src/Services/Search/DateSearchField.php index d13dcde2731..917c78179b4 100644 --- a/src/Services/Search/DateSearchField.php +++ b/src/Services/Search/DateSearchField.php @@ -41,7 +41,7 @@ class DateSearchField extends BasicSearchField private const COMPARATOR_MATCH = "/^(\D{2})?(\d{4})(-\d{2})?(-\d{2})?(?:(T\d{2}:\d{2})(:\d{2})?)?(\.\d{1,4})?(Z|(\+|-)(\d{2}):(\d{2}))?$/"; // php's DATE_ATOM does not handle milliseconds so we have to add them in manually - private const DATE_ATOM_MILLISECONDS = 'Y-m-d\TH:i:s.uP'; + public const DATE_ATOM_MILLISECONDS = 'Y-m-d\TH:i:s.uP'; private const COMPARATOR_INDEX_FULL = 0; diff --git a/src/Services/Search/FHIRSearchFieldFactory.php b/src/Services/Search/FHIRSearchFieldFactory.php index 79c94788264..34421e0295f 100644 --- a/src/Services/Search/FHIRSearchFieldFactory.php +++ b/src/Services/Search/FHIRSearchFieldFactory.php @@ -58,6 +58,16 @@ public function getFhirUrlResolver(): FhirUrlResolver return $this->fhirUrlResolver; } + /** + * @param $fhirSearchField + * @param FhirSearchParameterDefinition $definition + * @return void + */ + public function setSearchFieldDefinition(string $fhirSearchField, FhirSearchParameterDefinition $definition) + { + $this->resourceSearchParameters[$fhirSearchField] = $definition; + } + /** * Checks whethere the factory has a search definition for the passed in search field name * @param $fhirSearchField @@ -76,8 +86,8 @@ public function getSearchFieldDefinition($fhirSearchField): FhirSearchParameterD /** * Factory method to build a search field using the factory's search field definitions. - * @param $fhirSearchField The passed in parameter name for the search field the user agent sent. Can contain search modifiers - * @param $fhirSearchValues The array of search values the user agent sent for the $fhirSearchField + * @param $fhirSearchField string The passed in parameter name for the search field the user agent sent. Can contain search modifiers + * @param $fhirSearchValues array The array of search values the user agent sent for the $fhirSearchField * @throws \InvalidArgumentException If the factory does not have a search definition for $fhirSearchField * @return CompositeSearchField|DateSearchField|StringSearchField|TokenSearchField */ diff --git a/src/Services/SurgeryService.php b/src/Services/SurgeryService.php index 17d4c040ca8..6d54be74d14 100644 --- a/src/Services/SurgeryService.php +++ b/src/Services/SurgeryService.php @@ -58,7 +58,8 @@ public function search($search, $isAndCondition = true) encounter.euuid, recorders.recorder_npi, recorders.recorder_uuid, - recorders.recorder_username + recorders.recorder_username, + surgeries.date_modified FROM ( SELECT id @@ -71,6 +72,7 @@ public function search($search, $isAndCondition = true) ,`pid` ,`comments` ,`user` as surgery_recorder + ,`modifydate` AS date_modified FROM lists WHERE `type` = 'surgery' diff --git a/src/Services/UserService.php b/src/Services/UserService.php index ca63fde1cdd..5bdd1cb5e3a 100644 --- a/src/Services/UserService.php +++ b/src/Services/UserService.php @@ -254,13 +254,17 @@ public function search($search, $isAndCondition = true) phonecell, users.notes, state_license_number, - abook.title as abook_title"; + abook.title as abook_title, + last_updated "; if ($this->_includeUsername) { $sql .= ", username"; } + // grab our address book type, make sure to use the index w/ list_id and option_id $sql .= " FROM users - LEFT JOIN list_options as abook ON abook.option_id = users.abook_type"; + LEFT JOIN ( + SELECT list_id,option_id, title FROM list_options + ) abook ON abook.list_id = 'abook_type' AND abook.option_id = users.abook_type"; $whereClause = FhirSearchWhereClauseBuilder::build($search, $isAndCondition); $sql .= $whereClause->getFragment(); diff --git a/src/Services/Utils/DateFormatterUtils.php b/src/Services/Utils/DateFormatterUtils.php index 5152e5d9b9c..baaa47d3be4 100644 --- a/src/Services/Utils/DateFormatterUtils.php +++ b/src/Services/Utils/DateFormatterUtils.php @@ -140,4 +140,13 @@ public static function getTimeFormat($seconds = false) } return $formatted; } + + public static function getFormattedISO8601DateFromDateTime(\DateTime $dateTime): string + { + // ISO8601 doesn't support fractional dates so we need to change from microseconds to milliseconds + // TODO: @adunsulag this is a hack to get around the fact that PHP does microseconds and ISO8601 uses milliseconds + // , look at refactoring all of this so we don't have to do multiple date conversions up and down the stack. + $dateStr = substr($dateTime->format('Y-m-d\TH:i:s.u'), 0, -3) . $dateTime->format('P'); + return $dateStr; + } } diff --git a/src/Services/VitalsService.php b/src/Services/VitalsService.php index 94daf4ff138..bb338117c6d 100644 --- a/src/Services/VitalsService.php +++ b/src/Services/VitalsService.php @@ -115,6 +115,8 @@ public function search($search, $isAndCondition = true) ,vitals.ped_bmi ,vitals.ped_head_circ ,vitals.inhaled_oxygen_concentration + ,vitals.last_updated + ,forms.date_created ,details.details_id ,details.interpretation_list_id ,details.interpretation_option_id @@ -132,6 +134,7 @@ public function search($search, $isAndCondition = true) ,bpd,bps,weight,height,temperature,temp_method,pulse,respiration,BMI,BMI_status,waist_circ ,head_circ,oxygen_saturation,oxygen_flow_rate,inhaled_oxygen_concentration , ped_weight_height,ped_bmi,ped_head_circ + , last_updated FROM form_vitals ) vitals @@ -143,6 +146,7 @@ public function search($search, $isAndCondition = true) ,`user` ,deleted ,formdir + ,`date` AS date_created FROM forms ) forms ON vitals.id = forms.form_id @@ -151,9 +155,11 @@ public function search($search, $isAndCondition = true) encounter AS eid ,uuid AS euuid ,`date` AS encounter_date + ,pid AS encounter_pid FROM form_encounter - ) encounters ON encounters.eid = forms.encounter + -- use both columns in order to leverage the index + ) encounters ON encounters.encounter_pid = forms.form_pid AND encounters.eid = forms.encounter LEFT JOIN ( SELECT diff --git a/swagger/openemr-api.yaml b/swagger/openemr-api.yaml index 1f83fdf6430..b9d176c4b12 100644 --- a/swagger/openemr-api.yaml +++ b/swagger/openemr-api.yaml @@ -3854,6 +3854,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -3939,6 +3946,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4013,6 +4027,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4103,6 +4124,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4152,6 +4180,13 @@ paths: required: true schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string responses: '200': description: 'Standard Response' @@ -4190,6 +4225,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4272,6 +4314,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4359,6 +4408,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4444,6 +4500,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4551,6 +4614,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4727,6 +4797,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4818,6 +4895,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4899,6 +4983,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4989,6 +5080,20 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -5072,6 +5177,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string responses: '200': description: 'Standard Response' @@ -5137,6 +5249,21 @@ paths: tags: - fhir description: 'Returns a list of Medication resources.' + parameters: + - + name: _id + in: query + description: 'The uuid for the Medication resource.' + required: false + schema: + type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string responses: '200': description: 'Standard Response' @@ -5210,6 +5337,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -5309,6 +5443,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -5413,6 +5554,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: name in: query @@ -5677,6 +5825,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: identifier in: query @@ -5997,6 +6152,20 @@ paths: - fhir description: 'Returns a list of Person resources.' parameters: + - + name: _id + in: query + description: 'The uuid for the Person resource.' + required: false + schema: + type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: name in: query @@ -6149,6 +6318,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: name in: query @@ -6401,6 +6577,20 @@ paths: - fhir description: 'Returns a list of PractitionerRole resources.' parameters: + - + name: _id + in: query + description: 'The uuid for the PractitionerRole resource.' + required: false + schema: + type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: specialty in: query @@ -6488,6 +6678,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -6646,6 +6843,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string responses: '200': description: 'Standard Response' @@ -7540,7 +7744,6 @@ components: - subscriber_postal_code - subscriber_city - subscriber_state - - subscriber_country - subscriber_sex - accept_assignment properties: