/// <summary>
        ///  this is liquid syntax; plan is to replace it with a real liquid processor at some stage.
        ///  The names of the variables are FHIR Based, even though the internal model is not FHIR based
        /// </summary>
        /// <param name="templateId">Use on of the MessageTemplate consts</param>
        /// <param name="appt">Details of the appointment</param>
        /// <param name="vars">Additional information from the engine</param>
        /// <returns></returns>
        public string processTemplate(string templateId, PmsAppointment appt, Dictionary <string, string> vars)
        {
            string source = getTemplate(templateId);

            if (appt != null)
            {
                source = source.Replace("{{Patient.id}}", appt.PatientFhirID); // might be useful internal/debugging
                source = source.Replace("{{Patient.name}}", appt.PatientName);
                source = source.Replace("{{Patient.telecom.mobile}}", appt.PatientMobilePhone);
                source = source.Replace("{{Practitioner.name}}", appt.PractitionerName);
                source = source.Replace("{{Practitioner.id}}", appt.PractitionerFhirID);
                source = source.Replace("{{Appointment.id}}", appt.AppointmentFhirID);
                source = source.Replace("{{Appointment.status}}", appt.ArrivalStatus.ToString());
                source = source.Replace("{{Appointment.start}}", appt.AppointmentStartTime.ToString());
                source = source.Replace("{{Appointment.start.date}}", appt.AppointmentStartTime.ToString("d-MMM"));
                source = source.Replace("{{Appointment.start.time}}", appt.AppointmentStartTime.ToString("hh:mm tt"));
                source = source.Replace("{{Appointment.id}}", appt.AppointmentFhirID);
            }
            if (vars != null)
            {
                foreach (var s in vars.Keys)
                {
                    source = source.Replace("{{" + s + "}}", vars[s]);
                }
            }
            return(source);
        }
Exemple #2
0
        private static async System.Threading.Tasks.Task <PmsAppointment> ToPmsAppointment(Appointment entry, Func <ResourceReference, System.Threading.Tasks.Task <Resource> > resolveReference)
        {
            PmsAppointment appt = new PmsAppointment();

            appt.AppointmentFhirID    = entry.Id;
            appt.AppointmentStartTime = entry.Start.Value.DateTime;
            appt.ArrivalStatus        = entry.Status.Value;
            appt.PatientFhirID        = entry.Participant.FirstOrDefault(p => p.Actor.Reference?.StartsWith("Patient") == true)?.Actor?.Reference;
            appt.PractitionerFhirID   = entry.Participant.FirstOrDefault(p => p.Actor.Reference?.StartsWith("Practitioner") == true)?.Actor?.Reference;

            if (string.IsNullOrEmpty(appt.PatientFhirID))
            {
                return(null);
            }
            var patient = await resolveReference(new ResourceReference(appt.PatientFhirID));

            if (patient is Patient pat)
            {
                appt.PatientName = pat.Name.Select(n => $"{n.Family}, {String.Join(" ", n.Given)}").FirstOrDefault();
                // The PMS doesn't have enough detail to differentiate into sms usage
                appt.PatientMobilePhone = pat.Telecom.FirstOrDefault(t => t.Use == ContactPoint.ContactPointUse.Mobile)?.Value;
            }
            var practitioner = await resolveReference(new ResourceReference(appt.PractitionerFhirID));

            if (practitioner is Practitioner prac)
            {
                appt.PractitionerName = prac.Name.Select(n => $"{n.Family}, {String.Join(" ", n.Given)}").FirstOrDefault();
            }

            return(appt);
        }
Exemple #3
0
        private void ProcessScreeningResponse(PmsAppointment appt, SmsMessage msg)
        {
            // the patient should respond with "yes" or "no" but they may not bother and just respond with "arrived"
            // of course they might respond with anything else that we can't understand, so we'll explain apologetically if they do
            if (MessageMatches(msg.message, "yes", "y"))
            {
                // twilio:
                SmsMessage rmsg = new SmsMessage(msg.phone, TemplateProcessor.processTemplate(MessageTemplate.MSG_SCREENING_YES, appt, null));
                SmsSender.SendMessage(rmsg);
                LogMsg(OUT, rmsg, "process screening response 'yes'", appt);

                if (IsDoingVideo)
                {
                    // PMS:
                    appt.IsVideoConsultation = true;
                    if (VideoManager.AsksForVideoUrl())
                    {
                        AppointmentUpdater.SaveAppointmentAsVideoMeeting(appt, null, null);
                    }
                    else
                    {
                        var details = VideoManager.getConferenceDetails(appt, false);
                        AppointmentUpdater.SaveAppointmentAsVideoMeeting(appt, "Video URL: " + details, details);
                    }
                }

                // local storage
                appt.ExternalData.ScreeningMessageResponse = true;
                if (IsDoingVideo)
                {
                    appt.ExternalData.IsVideoConsultation = true;
                }
                else
                {
                    appt.IsVideoConsultation = false;
                }
                Storage.SaveAppointmentStatus(appt);
            }
            else if (MessageMatches(msg.message, "no", "n"))
            {
                SmsMessage rmsg = new SmsMessage(msg.phone, TemplateProcessor.processTemplate(MessageTemplate.MSG_SCREENING_NO, appt, null));
                SmsSender.SendMessage(rmsg);
                LogMsg(OUT, rmsg, "process screening response 'no'", appt);
                appt.ExternalData.ScreeningMessageResponse = true;
                appt.IsVideoConsultation = false;
                Storage.SaveAppointmentStatus(appt);
            }
            else if (MessageMatches(msg.message, "arrived", "here", "a"))
            {
                ProcessArrivalMessage(appt, msg);
            }
            else
            {
                // we haven't understood it
                SmsMessage rmsg = new SmsMessage(msg.phone, TemplateProcessor.processTemplate(MessageTemplate.MSG_DONT_UNDERSTAND_SCREENING, appt, null));
                SmsSender.SendMessage(rmsg);
                LogMsg(OUT, rmsg, "fail to process screening response", appt);
                UnprocessableMessages.Add(msg);
            }
        }
        /// <summary>
        /// This will retrieve the FHIR resource, set the AppointmentType for teleconsultation,
        /// include a link for the video link in the comment and update the resource
        /// </summary>
        /// <param name="appointment"></param>
        /// <param name="videoLinkComment"></param>
        public void SaveAppointmentAsVideoMeeting(PmsAppointment appt, string videoLinkComment, string VideoUrl)
        {
            // Get the Appointment based on the appointment having an ID
            var fhirServer = GetFhirClient();

            Hl7.Fhir.Model.Appointment fhirAppt = fhirServer.Read <Appointment>($"{fhirServer.Endpoint}Appointment/{appt.AppointmentFhirID}");
            if (fhirAppt == null)
            {
                throw new Exception("Unable to read appointment " + appt.AppointmentFhirID);
            }
            CodeableConcept teleHealth = new CodeableConcept("http://hl7.org/au/fhir/CodeSystem/AppointmentType", "teleconsultation");

            if (fhirAppt.AppointmentType == null || (fhirAppt.AppointmentType.Coding.FirstOrDefault()?.System != teleHealth.Coding[0].System ||
                                                     fhirAppt.AppointmentType.Coding.FirstOrDefault()?.Code != teleHealth.Coding[0].Code))
            {
                fhirAppt.AppointmentType = teleHealth;
            }
            fhirAppt.RemoveExtension("http://hl7.org.au/fhir/StructureDefinition/telehealth-videolink");
            if (VideoUrl != null)
            {
                fhirAppt.Extension.Add(new Extension()
                {
                    Url = "http://hl7.org.au/fhir/StructureDefinition/telehealth-videolink", Value = new FhirUrl(VideoUrl)
                });
            }
            fhirServer.Update(fhirAppt);
        }
Exemple #5
0
 private void ProcessArrivalMessage(PmsAppointment appt, SmsMessage msg)
 {
     if (MessageMatches(msg.message, "arrived", "here", "a"))
     {
         // twilio:
         SmsMessage rmsg = new SmsMessage(msg.phone, TemplateProcessor.processTemplate(MessageTemplate.MSG_ARRIVED_THX, appt, null));
         SmsSender.SendMessage(rmsg);
         LogMsg(OUT, rmsg, "process arrival message", appt);
         // PMS:
         appt.ArrivalStatus = AppointmentStatus.Arrived;
         AppointmentUpdater.SaveAppointmentStatusValue(appt);
         // local storage
         appt.ExternalData.ScreeningMessageResponse = true;
         appt.ExternalData.ArrivalStatus            = AppointmentStatus.Arrived;
         Storage.SaveAppointmentStatus(appt);
     }
     else
     {
         // we haven't understood it
         SmsMessage rmsg = new SmsMessage(msg.phone, TemplateProcessor.processTemplate(MessageTemplate.MSG_DONT_UNDERSTAND_ARRIVING, appt, null));
         SmsSender.SendMessage(rmsg);
         LogMsg(OUT, rmsg, "fail to process arrival message", appt);
         UnprocessableMessages.Add(msg);
     }
 }
        public void ExecuteCreateNewAppointment()
        {
            EditingAppointment.AppointmentFhirID = Guid.NewGuid().ToString();

            EditingAppointment.PractitionerFhirID = null;
            foreach (var pid in PractitionerFhirIds)
            {
                if (pid.Name == EditingAppointment.PractitionerName)
                {
                    EditingAppointment.PractitionerFhirID = pid.Id;
                }
            }
            if (EditingAppointment.PractitionerFhirID == null)
            {
                String id = Guid.NewGuid().ToString("X");
                PractitionerFhirIds.Add(new PractitionerId(EditingAppointment.PractitionerName, id));
                EditingAppointment.PractitionerFhirID = id;
            }

            Appointments.Add(EditingAppointment);
            Storage.SaveSimulationAppointments(Appointments);
            Storage.SaveSimulationIds(PractitionerFhirIds);
            EditingAppointment = new PmsAppointment();
            DateTime dt = DateTime.Now;

            EditingAppointment.AppointmentStartTime = dt.AddTicks(-(dt.Ticks % TimeSpan.TicksPerSecond));
            EditingAppointment.ArrivalStatus        = AppointmentStatus.Booked;
        }
Exemple #7
0
        private void ProcessUnexpectedResponse(PmsAppointment appt, SmsMessage msg)
        {
            SmsMessage rmsg = new SmsMessage(msg.phone, TemplateProcessor.processTemplate(MessageTemplate.MSG_UNEXPECTED, appt, null));

            SmsSender.SendMessage(rmsg);
            LogMsg(OUT, rmsg, "unexpected message", appt);
            UnprocessableMessages.Add(msg);
        }
Exemple #8
0
 /// <summary>
 /// This method is called every X seconds to process any incoming SMS appointments
 /// </summary>
 /// <param name="stored">The view of the appointments we already had (important, because it remembers what messages we already sent)</param>
 /// <param name="incoming">Sms Messages received since last poll</param>
 public void ProcessIncomingMessages(List <PmsAppointment> appts, IEnumerable <SmsMessage> incoming)
 {
     foreach (var msg in incoming)
     {
         try
         {
             LogMsg(IN, msg, null, null);
             // pseudo code
             // find the candidate appointments for this mobile phone
             // if there aren't any - return the 'please call reception message', and drop this message
             // if there's more than one, pick one
             // ok, now we have appointment and message
             // if we sent an invitation for a video conference
             //   process as a response to the invitation
             // else if we are expecting a response to the screening
             //   process as a response to the screening
             // else if we are expecting them to arrive
             //   process as an arrival messages
             // else
             //   we are not expecting a response - send message explaining that
             List <PmsAppointment> candidates = FindCandidateAppointments(appts, msg.phone);
             if (candidates.Count == 0)
             {
                 HandleUnknownMessage(msg);
             }
             else
             {
                 PmsAppointment appt = candidates.Count == 1 ? candidates[0] : ChooseRelevantAppointment(candidates, msg);
                 if (appt == null || !IsUseablePhoneNumber(appt.PatientMobilePhone))
                 {
                     ProcessUnexpectedResponse(candidates[0], msg);
                 }
                 else if (appt.ExternalData.VideoInviteSent)
                 {
                     ProcessVideoInviteResponse(appt, msg);
                 }
                 else if (appt.ExternalData.ScreeningMessageSent && !appt.ExternalData.ScreeningMessageResponse)
                 {
                     ProcessScreeningResponse(appt, msg);
                 }
                 // else if (appt.ExternalData.ScreeningMessageResponse && appt.IsVideoConsultation && )
                 else if (appt.ArrivalStatus == AppointmentStatus.Booked)
                 {
                     ProcessArrivalMessage(appt, msg);
                 }
                 else
                 {
                     ProcessUnexpectedResponse(appt, msg);
                 }
             }
         }
         catch (Exception e)
         {
             Logger.Log(ERR, "Exception processing message: " + e.Message + e.StackTrace);
         }
     }
 }
        public void SaveAppointmentStatusValue(PmsAppointment appointment)
        {
            var appt = Appointments.FirstOrDefault(a => a.AppointmentFhirID == appointment.AppointmentFhirID);

            if (appt != null)
            {
                appt.ArrivalStatus = appointment.ArrivalStatus;
            }
        }
Exemple #10
0
        public void SaveAppointmentAsVideoMeeting(PmsAppointment appointment, string videoLinkComment, string VideoUrl)
        {
            var appt = Appointments.FirstOrDefault(a => a.AppointmentFhirID == appointment.AppointmentFhirID);

            if (appt != null)
            {
                appt.IsVideoConsultation = appointment.IsVideoConsultation;
            }
        }
Exemple #11
0
 /// <summary>
 /// Get URL for conference
 /// </summary>
 /// <param name="id">The id of the appointment (unique ==> Appointment Resource id)</param>
 public String getConferenceDetails(PmsAppointment appointment, Boolean GetItReady)
 {
     if (String.IsNullOrEmpty(appointment.ExternalData.VideoSessionId))
     {
         var client = new OpenViduClient("https://video.healthintersections.com.au", secret);
         appointment.ExternalData.VideoSessionId = client.SetUpSession();
     }
     return("https://video.healthintersections.com.au/#" + appointment.ExternalData.VideoSessionId);
 }
        /// <summary>
        /// This will retrieve the FHIR appointment from the server,
        /// set the status value and then update back to the server
        /// </summary>
        /// <param name="appointment"></param>
        public void SaveAppointmentStatusValue(PmsAppointment appt)
        {
            // Get the Appointment based on the appointment having an ID
            // and update the status value
            var fhirServer = GetFhirClient();

            Hl7.Fhir.Model.Appointment fhirAppt = fhirServer.Read <Appointment>($"{fhirServer.Endpoint}Appointment/{appt.AppointmentFhirID}");
            if (fhirAppt.Status != appt.ArrivalStatus)
            {
                // Don't save it if hasn't changed
                fhirAppt.Status = appt.ArrivalStatus;
                fhirServer.Update(fhirAppt);
            }
        }
Exemple #13
0
        // Check for incoming messages

        // Handle arrived message
        public async System.Threading.Tasks.Task ArriveAppointment(PmsAppointment appt)
        {
            var         server      = GetServerConnection();
            Appointment appointment = await server.ReadAsync <Appointment>($"{server}/Appointment/{appt.AppointmentFhirID}");

            if (appointment.Status != Appointment.AppointmentStatus.Arrived)
            {
                appointment.Status = Appointment.AppointmentStatus.Arrived; // this means they've arrived (happy that they stay out there)
                server.Update(appointment);
            }

            //appointment.Status = Appointment.AppointmentStatus.Booked; // not turned up yet
            //appointment.Status = Appointment.AppointmentStatus.Arrived; // this means they've arrived (happy that they stay out there)
            //appointment.Status = Appointment.AppointmentStatus.Fulfilled; // seeing the practitioner
            //appointment.Status = Appointment.AppointmentStatus.Cancelled; // duh
        }
Exemple #14
0
 private void ProcessVideoInviteResponse(PmsAppointment appt, SmsMessage msg)
 {
     if (MessageMatches(msg.message, "joined", "ok", "j"))
     {
         // twilio:
         SmsMessage rmsg = new SmsMessage(msg.phone, TemplateProcessor.processTemplate(MessageTemplate.MSG_VIDEO_THX, appt, null));
         SmsSender.SendMessage(rmsg);
         LogMsg(OUT, rmsg, "accept video response", appt);
         // PMS:
         appt.ArrivalStatus = AppointmentStatus.Arrived;
         AppointmentUpdater.SaveAppointmentStatusValue(appt);
         // local storage:
         appt.ExternalData.ArrivalStatus = appt.ArrivalStatus;
         Storage.SaveAppointmentStatus(appt);
     }
     else
     {
         // we haven't understood it
         SmsMessage rmsg = new SmsMessage(msg.phone, TemplateProcessor.processTemplate(MessageTemplate.MSG_DONT_UNDERSTAND_VIDEO, appt, null));
         SmsSender.SendMessage(rmsg);
         LogMsg(OUT, rmsg, "fail to process video response", appt);
         UnprocessableMessages.Add(msg);
     }
 }
Exemple #15
0
        /// <summary>
        /// Check for outgoing messages
        /// If new appts are found, they will be added, missing ones will be removed
        /// (not a flush and add in again - as this will lose data)
        /// </summary>
        /// <param name="model"></param>
        public static async System.Threading.Tasks.Task CheckAppointments(ArrivalsModel model)
        {
            var oldexptecting = model.Expecting.ToList();
            var oldwaiting    = model.Waiting.ToList();

            var server   = GetServerConnection();
            var criteria = new SearchParams();

            criteria.Add("date", "2020-03-19"); // TODO: Change this to today's date
            criteria.Include.Add("Appointment:actor");
            var bundle = await server.SearchAsync <Appointment>(criteria);

            // Debugging
            var doc = System.Xml.Linq.XDocument.Parse(new Hl7.Fhir.Serialization.FhirXmlSerializer().SerializeToString(bundle));

            Console.WriteLine(doc.ToString(System.Xml.Linq.SaveOptions.None));

            Func <ResourceReference, System.Threading.Tasks.Task <Resource> > resolveReference = async(reference) =>
            {
                if (string.IsNullOrEmpty(reference.Reference))
                {
                    return(null);
                }
                ResourceIdentity ri = new ResourceIdentity(reference.Reference);
                var resource        = bundle.Entry.FirstOrDefault(e => e.Resource.ResourceType.GetLiteral() == ri.ResourceType && e.Resource.Id == ri.Id)?.Resource;
                if (resource == null)
                {
                    // wasn't returned in the bundle, so go searching for it
                    resource = await server.ReadAsync <Resource>(reference.Reference);
                }
                return(resource);
            };

            foreach (var entry in bundle.Entry.Select(e => e.Resource as Appointment).Where(e => e != null))
            {
                if (entry.Status == Appointment.AppointmentStatus.Booked)
                {
                    PmsAppointment app = await ToPmsAppointment(entry, resolveReference);

                    if (app != null && !model.Expecting.Contains(app))
                    {
                        model.Expecting.Add(app);
                    }
                }
                if (entry.Status == Appointment.AppointmentStatus.Arrived)
                {
                    PmsAppointment app = await ToPmsAppointment(entry, resolveReference);

                    if (app != null && !model.Waiting.Contains(app))
                    {
                        model.Waiting.Add(app);
                    }
                }
            }

            // finished processing all the items, so remove any that are left
            foreach (var item in oldexptecting)
            {
                model.Expecting.Remove(item);
            }
            foreach (var item in oldwaiting)
            {
                model.Waiting.Remove(item);
            }
        }
Exemple #16
0
        /// <summary>
        /// Retrieve the set of appointments that have mobile numbers with a status of booked, arrived, or fulfilled
        /// And attach the processing status data from local storage too
        /// </summary>
        /// <param name="model"></param>
        public async System.Threading.Tasks.Task <List <PmsAppointment> > SearchAppointments(DateTime date, IList <DoctorRoomLabelMapping> roomMappings, IArrivalsLocalStorage storage)
        {
            List <PmsAppointment> results = new List <PmsAppointment>();

            var server = GetServerConnection();

            if (server == null)
            {
                return(null);
            }

            var criteria = new SearchParams();

            criteria.Add("date", date.Date.ToString("yyyy-MM-dd"));
            criteria.Include.Add("Appointment:actor");
            var bundle = await server.SearchAsync <Appointment>(criteria);

            // Debugging
            var doc = System.Xml.Linq.XDocument.Parse(new Hl7.Fhir.Serialization.FhirXmlSerializer().SerializeToString(bundle));
            // Console.WriteLine(doc.ToString(System.Xml.Linq.SaveOptions.None));

            Func <ResourceReference, System.Threading.Tasks.Task <Resource> > resolveReference = async(reference) =>
            {
                if (string.IsNullOrEmpty(reference.Reference))
                {
                    return(null);
                }
                ResourceIdentity ri = new ResourceIdentity(reference.Reference);
                var resource        = bundle.Entry.FirstOrDefault(e => e.Resource.ResourceType.GetLiteral() == ri.ResourceType && e.Resource.Id == ri.Id)?.Resource;
                if (resource == null)
                {
                    // wasn't returned in the bundle, so go searching for it
                    resource = await server.ReadAsync <Resource>(reference.Reference);
                }
                return(resource);
            };

            foreach (var entry in bundle.Entry.Select(e => e.Resource as Appointment).Where(e => e != null))
            {
                PmsAppointment appt = null;
                if (entry.Status == Appointment.AppointmentStatus.Booked ||
                    entry.Status == Appointment.AppointmentStatus.Arrived ||
                    entry.Status == Appointment.AppointmentStatus.Fulfilled)
                {
                    appt = await ToPmsAppointment(entry, resolveReference);

                    if (appt != null)
                    {
                        if (!string.IsNullOrEmpty(appt.PatientMobilePhone))
                        {
                            results.Add(appt);

                            // Check if the practitioner has a mapping already
                            if (!roomMappings.Any(m => m.PractitionerFhirID == appt.PractitionerFhirID))
                            {
                                // Add in an empty room mapping
                                roomMappings.Add(new DoctorRoomLabelMapping()
                                {
                                    PractitionerFhirID = appt.PractitionerFhirID,
                                    PractitionerName   = appt.PractitionerName
                                });
                            }

                            // And read in the extended content from storage
                            await storage.LoadAppointmentStatus(appt);
                        }
                    }
                }
            }
            return(results);
        }
Exemple #17
0
 /// <summary>
 /// Get URL for conference
 /// </summary>
 /// <param name="id">The id of the appointment (unique ==> Appointment Resource id)</param>
 public String getConferenceDetails(PmsAppointment appointment, Boolean GetItReady)
 {
     return(null);
 }
Exemple #18
0
 private void LogMsg(bool isIn, SmsMessage msg, string op, PmsAppointment appt)
 {
     Logger.Log(MSG, (isIn ? "Receive from " : "Send to ") + msg.phone + ": " + msg.message + (op != null ? " (" + op + " on " + (appt == null ? "null" : appt.AppointmentFhirID) + ")" : ""));
 }
Exemple #19
0
        public async Task LoadAppointmentStatus(PmsAppointment appt)
        {
            PmsAppointmentExtendedData content = await LoadFile(appt.AppointmentStartTime.Date.ToString("yyyy-MM-dd"), $"{appt.AppointmentFhirID}.json", new PmsAppointmentExtendedData());

            appt.ExternalData = content;
        }
Exemple #20
0
 /// <summary>
 /// Get URL for conference
 /// </summary>
 /// <param name="id">The id of the appointment (unique ==> Appointment Resource id)</param>
 public String getConferenceDetails(PmsAppointment appointment, Boolean GetItReady)
 {
     return("https://meet.jit.si/" + systemId.ToString() + "-" + appointment.AppointmentFhirID);
 }
Exemple #21
0
 // TODO: Not sure if we actually need to store anything in here, maybe the actual message content that was received.
 // Could this go into a note in the Appointment itself as a write-back instead?
 public Task SaveAppointmentStatus(PmsAppointment appt)
 {
     return(SaveFile(appt.AppointmentStartTime.Date.ToString("yyyy-MM-dd"), $"{appt.AppointmentFhirID}.json", appt.ExternalData));
 }