create a reminder for a booked appointment

This commit is contained in:
Eugene Zadyra
2022-08-10 23:29:51 +02:00
parent e870e404e1
commit a735271263
14 changed files with 398 additions and 23 deletions

View File

@@ -0,0 +1,75 @@
databaseChangeLog:
- changeSet:
id: 06
author: dyrkin
preConditions:
onFail: MARK_RAN
not:
tableExists:
tableName: reminder
changes:
- createTable:
tableName: reminder
columns:
- column:
autoIncrement: true
constraints:
primaryKey: true
primaryKeyName: bug_pkey
name: record_id
type: BIGSERIAL
- column:
constraints:
nullable: false
name: user_id
type: BIGINT
- column:
constraints:
nullable: false
name: account_id
type: BIGINT
- column:
constraints:
nullable: false
name: chat_id
type: BIGINT
- column:
constraints:
nullable: false
name: source_system_id
type: BIGINT
- column:
constraints:
nullable: false
name: city_name
type: VARCHAR(255)
- column:
constraints:
nullable: false
name: clinic_name
type: VARCHAR(255)
- column:
constraints:
nullable: false
name: service_name
type: VARCHAR(255)
- column:
constraints:
nullable: false
name: doctor_name
type: VARCHAR(255)
- column:
constraints:
nullable: false
name: appointment_time
type: TIME WITHOUT TIME ZONE
- column:
constraints:
nullable: true
name: remind_at_time
type: TIME WITHOUT TIME ZONE
- column:
constraints:
nullable: true
name: active
type: BOOLEAN

View File

@@ -16,4 +16,7 @@ databaseChangeLog:
relativeToChangelogFile: true
- include:
file: changelog/05-drop-bugs-table.yml
relativeToChangelogFile: true
relativeToChangelogFile: true
- include:
file: changelog/06-reminders.yml
relativeToChangelogFile: true

View File

@@ -7,7 +7,7 @@ import com.lbs.bot.telegram.TelegramBot
import com.lbs.server.conversation._
import com.lbs.server.lang.Localization
import com.lbs.server.repository.model.Monitoring
import com.lbs.server.service.{ApiService, DataService, MonitoringService}
import com.lbs.server.service.{ApiService, DataService, MonitoringService, ReminderService}
import org.jasypt.util.text.{StrongTextEncryptor, TextEncryptor}
import org.springframework.beans.factory.annotation.{Autowired, Value}
import org.springframework.context.annotation.{Bean, Configuration}
@@ -29,6 +29,9 @@ class BootConfig {
@Autowired
private var monitoringService: MonitoringService = _
@Autowired
private var reminderService: ReminderService = _
@Autowired
private var localization: Localization = _
@@ -123,8 +126,8 @@ class BootConfig {
userId =>
new Chat(
userId,
dataService,
monitoringService,
reminderService,
bookFactory,
helpFactory,
monitoringsFactory,

View File

@@ -5,7 +5,7 @@ import com.lbs.bot.model.Command
import com.lbs.server.conversation.Chat._
import com.lbs.server.conversation.Login.UserId
import com.lbs.server.conversation.base.{Conversation, Interactional}
import com.lbs.server.service.{DataService, MonitoringService}
import com.lbs.server.service.{MonitoringService, ReminderService}
import com.lbs.server.util.MessageExtractors._
import com.typesafe.scalalogging.StrictLogging
@@ -13,8 +13,8 @@ import scala.util.matching.Regex
class Chat(
val userId: UserId,
dataService: DataService,
monitoringService: MonitoringService,
reminderService: ReminderService,
bookingFactory: UserIdTo[Book],
helpFactory: UserIdTo[Help],
monitoringsFactory: UserIdTo[Monitorings],
@@ -127,12 +127,12 @@ class Chat(
case Msg(cmd @ TextCommand("/accounts"), _) =>
self ! cmd
goto(accountChat)
case Msg(TextCommand(ReserveTerm(monitoringIdStr, scheduleIdStr, timeStr)), _) =>
val monitoringId = monitoringIdStr.toLong
val scheduleId = scheduleIdStr.toLong
val time = timeStr.toLong
case Msg(TextCommand(ReserveTermRegex(LongString(monitoringId), LongString(scheduleId), LongString(time))), _) =>
monitoringService.bookAppointmentByScheduleId(userId.accountId, monitoringId, scheduleId, time)
stay()
case Msg(CallbackCommand(RemindRegexp(LongString(reminderId), LongString(time))), _) =>
reminderService.activateReminder(userId.accountId, reminderId, time)
stay()
case Msg(cmd: Command, _) =>
interactional ! cmd
stay()
@@ -151,5 +151,6 @@ class Chat(
}
object Chat {
val ReserveTerm: Regex = s"/reserve_(\\d+)_(\\d+)_(\\d+)".r
val ReserveTermRegex: Regex = "/reserve_(\\d+)_(\\d+)_(\\d+)".r
val RemindRegexp: Regex = "remind_at_(\\d+)_(\\d+)".r
}

View File

@@ -3,7 +3,7 @@ package com.lbs.server.lang
import com.lbs.api.json.model.{Event, TermExt}
import com.lbs.server.conversation.Book
import com.lbs.server.conversation.StaticData.StaticDataConfig
import com.lbs.server.repository.model.Monitoring
import com.lbs.server.repository.model.{Monitoring, Reminder}
import com.lbs.server.util.DateTimeUtil._
import java.time.{LocalDateTime, LocalTime}
@@ -173,7 +173,7 @@ object En extends Lang {
override def help: String =
s""" Non official bot for <b>Portal Pacjenta LUX MED (v.${Lang.version})</b>.
|It can help you to book a visit to a doctor, create term monitoring, view upcoming appointments and visit history.
|The bot can help you book a visit to a doctor, create term monitorings, view upcoming appointments and visit history.
|
|<b>➡</b> Supported commands
|/book - reserve a visit, or create a monitoring
@@ -377,4 +377,17 @@ object En extends Lang {
override def canNotDetectPayer(error: String): String = s"Can't determine payer. Reason: $error"
override def pleaseChoosePayer: String = "<b>➡</b> Can't determine default payer. Please choose one"
override def youHaveAppointmentAt(reminder: Reminder): String =
s"""👍 You have an appointment at ⏱ <b>${formatTime(reminder.appointmentTime.toLocalTime)}</b>!
|
|${capitalize(doctor)}: ${reminder.doctorName}
|${capitalize(service)}: ${reminder.serviceName}
|${capitalize(clinic)}: ${reminder.clinicName}
|${capitalize(city)}: ${reminder.cityName}""".stripMargin
override def remindAt(time: LocalDateTime): String = s"⏱ Remind at ${formatTime(time.toLocalTime)}"
override def appointmentIsOutdated(appointmentTime: LocalDateTime): String =
s"Your appointment has already taken place at ${formatDateTime(appointmentTime, locale)}"
}

View File

@@ -3,7 +3,7 @@ package com.lbs.server.lang
import com.lbs.api.json.model.{Event, TermExt}
import com.lbs.server.conversation.Book.BookingData
import com.lbs.server.conversation.StaticData.StaticDataConfig
import com.lbs.server.repository.model.Monitoring
import com.lbs.server.repository.model.{Monitoring, Reminder}
import java.time.{LocalDateTime, LocalTime}
import java.util.Locale
@@ -235,4 +235,10 @@ trait Lang {
def pleaseChooseAccount(currentAccountName: String): String
def accountSwitched(username: String): String
def youHaveAppointmentAt(reminder: Reminder): String
def remindAt(time: LocalDateTime): String
def appointmentIsOutdated(appointmentTime: LocalDateTime): String
}

View File

@@ -3,7 +3,7 @@ package com.lbs.server.lang
import com.lbs.api.json.model.{Event, TermExt}
import com.lbs.server.conversation.Book
import com.lbs.server.conversation.StaticData.StaticDataConfig
import com.lbs.server.repository.model.Monitoring
import com.lbs.server.repository.model.{Monitoring, Reminder}
import com.lbs.server.util.DateTimeUtil._
import java.time.{LocalDateTime, LocalTime}
@@ -379,4 +379,17 @@ object Pl extends Lang {
override def canNotDetectPayer(error: String): String = s"Nie udało się ustalić płatnika. Powód: $error"
override def pleaseChoosePayer: String = "<b>➡</b> Nie udało się ustalić domyślnego płatnika, wybierz jakiegoś."
override def youHaveAppointmentAt(reminder: Reminder): String =
s"""👍 Macie wizytę o ⏱ <b>${formatTime(reminder.appointmentTime.toLocalTime)}</b>!
|
|${capitalize(doctor)}: ${reminder.doctorName}
|${capitalize(service)}: ${reminder.serviceName}
|${capitalize(clinic)}: ${reminder.clinicName}
|${capitalize(city)}: ${reminder.cityName}""".stripMargin
override def remindAt(time: LocalDateTime): String = s"⏱ Przypomnij o ${formatTime(time.toLocalTime)}"
override def appointmentIsOutdated(appointmentTime: LocalDateTime): String =
s"Twoja wizyta już się odbyła o godzine ${formatDateTime(appointmentTime, locale)}"
}

View File

@@ -3,7 +3,7 @@ package com.lbs.server.lang
import com.lbs.api.json.model.{Event, TermExt}
import com.lbs.server.conversation.Book
import com.lbs.server.conversation.StaticData.StaticDataConfig
import com.lbs.server.repository.model.Monitoring
import com.lbs.server.repository.model.{Monitoring, Reminder}
import com.lbs.server.util.DateTimeUtil._
import java.time.{LocalDateTime, LocalTime}
@@ -378,4 +378,17 @@ object Ua extends Lang {
override def pleaseChoosePayer: String =
"<b>➡</b> Не можу визначити платника за замовчуванням. Будь ласка, виберіть платника"
override def youHaveAppointmentAt(reminder: Reminder): String =
s"""👍 У вас є візит о ⏱ <b>${formatTime(reminder.appointmentTime.toLocalTime)}</b>!
|
|${capitalize(doctor)}: ${reminder.doctorName}
|${capitalize(service)}: ${reminder.serviceName}
|${capitalize(clinic)}: ${reminder.clinicName}
|${capitalize(city)}: ${reminder.cityName}""".stripMargin
override def remindAt(time: LocalDateTime): String = s"⏱ Remind at ${formatTime(time.toLocalTime)}"
override def appointmentIsOutdated(appointmentTime: LocalDateTime): String =
s"Your appointment has already taken place at ${formatDateTime(appointmentTime, locale)}"
}

View File

@@ -1,6 +1,6 @@
package com.lbs.server.repository
import com.lbs.server.repository.model.{CityHistory, ClinicHistory, Credentials, DoctorHistory, JLong, Monitoring, ServiceHistory, Settings, Source, SystemUser}
import com.lbs.server.repository.model.{CityHistory, ClinicHistory, Credentials, DoctorHistory, JLong, Monitoring, Reminder, ServiceHistory, Settings, Source, SystemUser}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository
@@ -95,6 +95,15 @@ class DataRepository(@Autowired em: EntityManager) {
.toSeq
}
def getActiveReminders: Seq[Reminder] = {
em.createQuery(
"""select reminder from Reminder reminder where reminder.active = true""".stripMargin,
classOf[Reminder]
).getResultList
.asScala
.toSeq
}
def getActiveMonitoringsCount(accountId: Long): JLong = {
em.createQuery(
"""select count(monitoring) from Monitoring monitoring where monitoring.active = true
@@ -177,6 +186,18 @@ class DataRepository(@Autowired em: EntityManager) {
.headOption
}
def findReminder(accountId: Long, reminderId: Long): Option[Reminder] = {
em.createQuery(
"""select reminder from Reminder reminder where reminder.accountId = :accountId
| and reminder.recordId = :reminderId""".stripMargin,
classOf[Reminder]
).setParameter("accountId", accountId)
.setParameter("reminderId", reminderId)
.getResultList
.asScala
.headOption
}
def findSettings(userId: Long): Option[Settings] = {
em.createQuery("select settings from Settings settings where settings.userId = :userId", classOf[Settings])
.setParameter("userId", userId)

View File

@@ -0,0 +1,83 @@
package com.lbs.server.repository.model
import java.time.LocalDateTime
import javax.persistence.{Access, AccessType, Column, Entity}
import scala.beans.BeanProperty
@Entity
@Access(AccessType.FIELD)
class Reminder extends RecordId {
@BeanProperty
@Column(name = "user_id", nullable = false)
var userId: JLong = _
@BeanProperty
@Column(name = "account_id", nullable = false)
var accountId: JLong = _
@BeanProperty
@Column(name = "chat_id", nullable = false)
var chatId: String = _
@BeanProperty
@Column(name = "source_system_id", nullable = false)
var sourceSystemId: JLong = _
@BeanProperty
@Column(name = "city_name", nullable = false)
var cityName: String = _
@BeanProperty
@Column(name = "clinic_name", nullable = false)
var clinicName: String = _
@BeanProperty
@Column(name = "service_name", nullable = false)
var serviceName: String = _
@BeanProperty
@Column(name = "doctor_name", nullable = false)
var doctorName: String = _
@BeanProperty
@Column(name = "appointment_time", nullable = false)
var appointmentTime: LocalDateTime = _
@BeanProperty
@Column(name = "remind_at_time", nullable = true)
var remindAt: LocalDateTime = _
@BeanProperty
@Column(nullable = false)
var active: Boolean = false
}
object Reminder {
def apply(
userId: Long,
accountId: Long,
chatId: String,
sourceSystemId: Long,
cityName: String,
clinicName: String,
serviceName: String,
doctorName: String,
appointmentTime: LocalDateTime,
remindAt: LocalDateTime,
active: Boolean
): Reminder = {
val reminder = new Reminder
reminder.userId = userId
reminder.accountId = accountId
reminder.chatId = chatId
reminder.sourceSystemId = sourceSystemId
reminder.cityName = cityName
reminder.clinicName = clinicName
reminder.serviceName = serviceName
reminder.doctorName = doctorName
reminder.appointmentTime = appointmentTime
reminder.remindAt = remindAt
reminder.active = active
reminder
}
}

View File

@@ -48,6 +48,15 @@ class DataService {
dataRepository.saveEntity(monitoring)
}
@Transactional
def saveReminder(reminder: Reminder): Reminder = {
dataRepository.saveEntity(reminder)
}
def getActiveReminders: Seq[Reminder] = {
dataRepository.getActiveReminders
}
def getActiveMonitorings: Seq[Monitoring] = {
dataRepository.getActiveMonitorings
}
@@ -76,6 +85,10 @@ class DataService {
dataRepository.findMonitoring(accountId, monitoringId)
}
def findReminder(accountId: Long, reminderId: Long): Option[Reminder] = {
dataRepository.findReminder(accountId, reminderId)
}
def findSettings(userId: Long): Option[Settings] = {
dataRepository.findSettings(userId)
}

View File

@@ -2,8 +2,8 @@ package com.lbs.server.service
import com.lbs.api.exception.InvalidLoginOrPasswordException
import com.lbs.api.json.model._
import com.lbs.bot.Bot
import com.lbs.bot.model.{MessageSource, MessageSourceSystem}
import com.lbs.bot.model.{Button, MessageSource, MessageSourceSystem}
import com.lbs.bot.{Bot, createInlineKeyboard}
import com.lbs.common.Scheduler
import com.lbs.server.lang.Localization
import com.lbs.server.repository.model._
@@ -13,7 +13,7 @@ import com.typesafe.scalalogging.StrictLogging
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import java.time.{LocalDateTime, ZonedDateTime}
import java.time.{LocalDateTime, ZoneId, ZonedDateTime}
import java.util.concurrent.ScheduledFuture
import javax.annotation.PostConstruct
import scala.collection.mutable
@@ -30,6 +30,8 @@ class MonitoringService extends StrictLogging {
@Autowired
private var apiService: ApiService = _
@Autowired
private var reminderService: ReminderService = _
@Autowired
private var localization: Localization = _
private var activeMonitorings = mutable.Map.empty[JLong, (Monitoring, ScheduledFuture[_])]
@@ -195,8 +197,23 @@ class MonitoringService extends StrictLogging {
} yield response
bookingResult match {
case Right(_) =>
bot.sendMessage(monitoring.source, lang(monitoring.userId).appointmentIsBooked(term, monitoring))
deactivateMonitoring(monitoring.accountId, monitoring.recordId)
val reminder = reminderService.createInactiveReminder((term.term, monitoring).mapTo[Reminder])
val remind1h = term.term.dateTimeFrom.get.minusHours(1)
val remind1hMillis = remind1h.atZone(ZoneId.systemDefault()).toInstant.toEpochMilli
val remind2h = term.term.dateTimeFrom.get.minusHours(2)
val remind2hMillis = remind2h.atZone(ZoneId.systemDefault()).toInstant.toEpochMilli
val messages = lang(monitoring.userId)
bot.sendMessage(
monitoring.source,
messages.appointmentIsBooked(term, monitoring),
inlineKeyboard = createInlineKeyboard(
Seq(
Button(messages.remindAt(remind1h), s"remind_at_${reminder.recordId}_$remind1hMillis"),
Button(messages.remindAt(remind2h), s"remind_at_${reminder.recordId}_$remind2hMillis")
)
)
)
case Left(ex) =>
logger.error(s"Unable to book appointment by monitoring [${monitoring.recordId}]", ex)
}
@@ -206,7 +223,7 @@ class MonitoringService extends StrictLogging {
accountId: Long,
xsrfToken: XsrfToken,
temporaryReservationId: Long,
fn: (Long) => Either[Throwable, T]
fn: Long => Either[Throwable, T]
): Either[Throwable, T] = {
fn(accountId) match {
case r @ Left(_) =>

View File

@@ -0,0 +1,94 @@
package com.lbs.server.service
import com.lbs.bot.Bot
import com.lbs.bot.model.{MessageSource, MessageSourceSystem}
import com.lbs.common.Scheduler
import com.lbs.server.lang.Localization
import com.lbs.server.repository.model._
import com.typesafe.scalalogging.StrictLogging
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import java.time.{Instant, LocalDateTime, ZoneId}
import javax.annotation.PostConstruct
import scala.concurrent.duration._
@Service
class ReminderService extends StrictLogging {
@Autowired
private var bot: Bot = _
@Autowired
private var dataService: DataService = _
@Autowired
private var localization: Localization = _
private val dbChecker = new Scheduler(1)
private def deactivateReminder(accountId: JLong, reminderId: JLong): Unit = {
dataService.findReminder(accountId, reminderId).foreach { reminder =>
reminder.active = false
dataService.saveReminder(reminder)
}
}
def remindUserAboutAppointment(reminder: Reminder): Unit = {
deactivateReminder(reminder.accountId, reminder.recordId)
val messages = lang(reminder.userId)
val message = messages.youHaveAppointmentAt(reminder)
bot.sendMessage(reminder.source, message)
}
private def checkReminders(): Unit = {
logger.debug(s"Looking for active reminders")
val activeReminders = dataService.getActiveReminders
logger.debug(s"Found [${activeReminders.size}] active reminders")
val now = LocalDateTime.now()
activeReminders.foreach {
case reminder if reminder.remindAt.isAfter(now) =>
logger.debug(s"Notifying user [${reminder.userId}] about appointment at [${reminder.appointmentTime}]")
remindUserAboutAppointment(reminder)
}
}
private def initializeDbChecker(): Unit = {
dbChecker.schedule(checkReminders(), 20.seconds)
}
def createInactiveReminder(reminder: Reminder): Reminder = {
reminder.active = false
dataService.saveReminder(reminder)
}
def activateReminder(accountId: Long, reminderId: Long, timeMillis: Long): Unit = {
val time = Instant.ofEpochMilli(timeMillis).atZone(ZoneId.systemDefault()).toLocalDateTime
val reminderMaybe = dataService.findReminder(accountId, reminderId)
reminderMaybe.foreach { reminder =>
if (reminder.appointmentTime.isBefore(time)) {
reminder.active = true
reminder.remindAt = time
dataService.saveReminder(reminder)
} else {
val messages = lang(reminder.userId)
val message = messages.appointmentIsOutdated(reminder.appointmentTime)
bot.sendMessage(reminder.source, message)
}
}
}
implicit class ReminderAsSource(reminder: Reminder) {
def source: MessageSource = MessageSource(MessageSourceSystem(reminder.sourceSystemId), reminder.chatId)
}
private def lang(userId: Long) = localization.lang(userId)
@PostConstruct
private def initialize(): Unit = {
initializeDbChecker()
}
}

View File

@@ -5,12 +5,12 @@ import com.lbs.bot.model.Command
import com.lbs.common.ModelConverters
import com.lbs.server.conversation.Book.BookingData
import com.lbs.server.conversation.Login.UserId
import com.lbs.server.repository.model.{History, Monitoring}
import com.lbs.server.repository.model.{History, Monitoring, Reminder}
import java.time._
import java.time.format.DateTimeFormatter
import java.util.Locale
import scala.language.{higherKinds, implicitConversions}
import scala.language.implicitConversions
import scala.util.Try
package object util {
@@ -109,6 +109,24 @@ package object util {
)
}
implicit val TermAndMonitoringToReminder: ObjectConverter[(Term, Monitoring), Reminder] =
(data: (Term, Monitoring)) => {
val (term, monitoring) = data
Reminder(
userId = monitoring.userId,
accountId = monitoring.accountId,
chatId = monitoring.chatId,
sourceSystemId = monitoring.sourceSystemId,
cityName = monitoring.cityName,
clinicName = monitoring.clinicName,
serviceName = monitoring.serviceName,
doctorName = monitoring.doctorName,
appointmentTime = term.dateTimeFrom.get,
remindAt = null,
active = false
)
}
implicit val HistoryToIdNameConverter: ObjectConverter[History, IdName] =
(history: History) => IdName(history.id, history.name)
}
@@ -167,6 +185,8 @@ package object util {
def formatDateTime(date: LuxmedFunnyDateTime, locale: Locale): String = date.get.format(DateTimeFormat(locale))
def formatDateTime(date: LocalDateTime, locale: Locale): String = date.format(DateTimeFormat(locale))
private val EpochMinutesTillBeginOf2022: Long = epochMinutes(LocalDateTime.of(2022, 1, 1, 0, 0, 0, 0))
def epochMinutes(time: LocalDateTime): Long = time.toInstant(ZonedDateTime.now().getOffset).getEpochSecond / 60