/// <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); }
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); }
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); }
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; }
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); }
/// <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; } }
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; } }
/// <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); } }
// 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 }
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); } }
/// <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); } }
/// <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); }
/// <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); }
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) + ")" : "")); }
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; }
/// <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); }
// 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)); }