From 012a4023baf6cbd99b71e44e0ddf1054589c601d Mon Sep 17 00:00:00 2001 From: Daniil <94884910+Filienko@users.noreply.github.com> Date: Sat, 17 Feb 2024 16:06:26 -0800 Subject: [PATCH] Worked: outgoing messages --- isacc_messaging/api/isacc_record_creator.py | 133 +++++++++--------- isacc_messaging/models/isacc_communication.py | 6 + .../models/isacc_communicationrequest.py | 7 + 3 files changed, 78 insertions(+), 68 deletions(-) diff --git a/isacc_messaging/api/isacc_record_creator.py b/isacc_messaging/api/isacc_record_creator.py index bc85e37c..18814357 100644 --- a/isacc_messaging/api/isacc_record_creator.py +++ b/isacc_messaging/api/isacc_record_creator.py @@ -59,7 +59,6 @@ def __init__(self): def dispatch_cr(self, cr: CommunicationRequest): if cr.dispatched(): return cr.dispatched_message_status() - # Default succesfull status and statusReason status = "" statusReason = "" target_phone = resolve_reference(cr.recipient[0].reference).get_phone_number() @@ -234,22 +233,20 @@ def on_twilio_message_status_update(self, values): })) if existing_comm is None: # Callback only occurs on completed Communications - c = cr.create_communication_from_request(status="completed") - c = Communication(c) - result = HAPI_request('POST', 'Communication', resource=c.as_json()) + comm_json = cr.create_communication_from_request(status="completed") + result = HAPI_request('POST', 'Communication', resource=comm_json) audit_entry( f"Created Communication resource on Twilio callback:", extra={"resource": result}, level='debug' ) # if this was a manual message, mark patient as having been followed up with - if c.is_manual_follow_up_message(): + if comm.is_manual_follow_up_message(): patient.mark_followup_extension() else: # Update the status of the communication to completed comm = Communication(existing_comm) - comm.status = "completed" - result = comm.persist() + comm.change_status(status="completed") audit_entry( f"Received /MessageStatus callback with status {message_status} on existing Communication resource", extra={"resource": result, @@ -269,7 +266,6 @@ def on_twilio_message_status_update(self, values): # maintain next outgoing and last followed up Twilio message # extensions after each send (now know to be complete) - patient.mark_next_outgoing() patient.mark_followup_extension() def on_twilio_message_received(self, values): @@ -326,7 +322,6 @@ def execute_requests(self) -> Tuple[List[dict], List[dict]]: """ successes = [] errors = [] - skipped_crs = [] now = datetime.now().astimezone() cutoff = now - timedelta(days=2) @@ -339,80 +334,82 @@ def execute_requests(self) -> Tuple[List[dict], List[dict]]: for cr_json in next_in_bundle(result): cr = CommunicationRequest(cr_json) + # as that message was likely the next-outgoing for the patient, + # update the extension used to track next-outgoing-message time patient = resolve_reference(cr.recipient[0].reference) - # Happens when the patient removes their phone number completely. - # Should not occur in production. - try: - patient_unsubscribed = any( - telecom_entry.system.lower() == 'sms' and telecom_entry.period.end - for telecom_entry in patient.telecom - ) - except Exception as e: - skipped_crs.append({cr, False, "No Phone Number Registered"}) - continue + patient.mark_next_outgoing() - if cr.occurrenceDateTime.date < cutoff: - # Anything older than cutoff will never be sent (#1861758) - # and needs a status adjustment lest it throws off other queries - # like next outgoing message time - skipped_crs.append((cr, "aborted", "Past the cutoff")) + # Do not interact with CR if the patient is inactive or sending date is past the cutoff + if not patient.active or cr.occurrenceDateTime.date < cutoff: + cr.status = "revoked" + cr.persist() + revoked_reason = "" + if not patient.active: + revoked_reason = "Recipient is not active" + else: + revoked_reason = "Past the cutoff" + cr.report_cr_status(status_reason=revoked_reason) + errors.append({'id': cr.id, 'error': revoked_reason}) continue - if patient_unsubscribed or not patient.active: - revoked_status = (cr, "aborted", 'Recipient is not active') - if patient_unsubscribed: - revoked_status = (cr, "stopped", 'Recipient unsubscribed') - skipped_crs.append(revoked_status) - # Do not cancel future sms + + # Otherwise, create a communication + comm_json = cr.create_communication_from_request(status="in-progress") + updated_comm = HAPI_request('POST', 'Communication', resource=comm_json) + comm = Communication(updated_comm) + audit_entry( + f"Generated an {comm.status} Communication/{comm.id} for CommunicationRequest/{cr.id}", + extra={"resource": updated_comm}, + level="debug" + ) + # Update the CommunicationRequest as completed, since new Communication was sent + cr.status = "completed" + cr.persist() + + # If patient unsubscribed, mark as stopped + if any( + telecom_entry.system.lower() == 'sms' and telecom_entry.period.end + for telecom_entry in getattr(patient, 'telecom', []) + ): + comm.change_status(status="stopped") + errors.append({'id': cr.id, 'error': "Patient unsubscribed"}) + audit_entry( + f"Updated {comm} to {comm.status}, because patient unsubscribed", + extra={"resource": comm,}, + level='debug' + ) continue + + # Otherwise, update according to the feedback from the dispatch try: comm_status, comm_statusReason = self.process_cr(cr, successes) - c = cr.create_communication_from_request(status=comm_status) - c = Communication(c) - HAPI_request('POST', 'Communication', resource=c.as_json()) + comm.change_status(status=comm_status) audit_entry( - f"Sent a message for {cr.id}. Status of Communication: {comm_status}", - extra={"resource": f"CommunicationResource/{cr.id}", "feedback": comm_statusReason}, - level='exception' + f"Updated status of Communication/{comm.id} to {comm_status}", + extra={"resource": f"Communication/{comm.id}", "statusReason": comm_statusReason}, + level='debug' ) + if comm_status == "in-progress": + # In-progress status entails that sms was successfully dispatched + successes.append({'id': cr.id, 'status': comm_statusReason}) + else: + # Register an error encountered when sending a message + audit_entry( + f"Failed to send the message for CommunicationRequest/{cr.id} because {comm_statusReason}", + extra={"resource": f"Communication/{comm.id}", "statusReason": comm_status}, + level='exception' + ) + except Exception as e: + # Register an error when sending a message + comm.change_status(status="unknown") audit_entry( - "Failed to send the message", - extra={"resource": f"CommunicationResource/{cr.id}", "exception": e}, + f"Failed to send the message for CommunicationRequest/{cr.id} because {e}", + extra={"resource": f"Communication/{comm.id}", "statusReason": e}, level='exception' ) - skipped_crs.append((cr, "unknown", str(e))) - - for cr, revoked_reason, e in skipped_crs: - # Aborted class marking CRs that should not be send - if revoked_reason != "aborted": - c = cr.create_communication_from_request(status=revoked_reason) - c = Communication(c) - result = HAPI_request('POST', 'Communication', resource=c.as_json()) - audit_entry( - f"Generated new Communication for a revoked CR. Reason: {e}", - extra={"resource": f"{result}"}, - level='debug' - ) - cr.status = "revoked" - HAPI_request( - "PUT", - "CommunicationRequest", - resource_id=cr.id, - resource=cr.as_json()) - audit_entry( - f"Skipped CommunicationRequest({cr.id}); status set to {cr.status} because {e}", - extra={"CommunicationRequest": cr.as_json(), "reason": revoked_reason, "exception": e}) - errors.append({'id': cr.id, 'error': str(e)}) - # as that message was likely the next-outgoing for the patient, - # update the extension used to track next-outgoing-message time - patient = resolve_reference(cr.recipient[0].reference) - patient.mark_next_outgoing() return successes, errors def process_cr(self, cr: CommunicationRequest, successes: list): status, statusReason = self.dispatch_cr(cr=cr) - # In-progress status entails that sms was successfully dispatched - if status == "in-progress": - successes.append({'id': cr.id, 'status': statusReason}) return status, statusReason \ No newline at end of file diff --git a/isacc_messaging/models/isacc_communication.py b/isacc_messaging/models/isacc_communication.py index 317643ad..13a99c07 100644 --- a/isacc_messaging/models/isacc_communication.py +++ b/isacc_messaging/models/isacc_communication.py @@ -29,6 +29,12 @@ def persist(self): response = HAPI_request('PUT', 'Communication', resource_id=self.id, resource=self.as_json()) return response + def change_status(self, status): + """Persist self state to FHIR store""" + self.status = status + response = self.persist() + return response + @staticmethod def about_patient(patient): """Query for "outside" Communications about the patient diff --git a/isacc_messaging/models/isacc_communicationrequest.py b/isacc_messaging/models/isacc_communicationrequest.py index caf89ce8..627da83e 100644 --- a/isacc_messaging/models/isacc_communicationrequest.py +++ b/isacc_messaging/models/isacc_communicationrequest.py @@ -6,6 +6,7 @@ from datetime import datetime from fhirclient.models.communicationrequest import CommunicationRequest from fhirclient.models.identifier import Identifier +from isacc_messaging.audit import audit_entry from isacc_messaging.models.fhir import HAPI_request, first_in_bundle @@ -105,3 +106,9 @@ def persist(self): """Persist self state to FHIR store""" response = HAPI_request('PUT', 'CommunicationRequest', resource_id=self.id, resource=self.as_json()) return response + + def report_cr_status(self, status_reason): + audit_entry( + f"CommunicationRequest({self.id}) status set to {self.status} because {status_reason}", + extra={"CommunicationRequest": self.as_json(), "reason": status_reason} + )