diff --git a/api/src/main/scala/com/lbs/api/LuxmedApiAsync.scala b/api/src/main/scala/com/lbs/api/LuxmedApiAsync.scala deleted file mode 100644 index 902a554..0000000 --- a/api/src/main/scala/com/lbs/api/LuxmedApiAsync.scala +++ /dev/null @@ -1,73 +0,0 @@ - -package com.lbs.api - -import java.time.ZonedDateTime - -import com.lbs.api.json.model._ -import scalaj.http.HttpResponse - -import scala.concurrent.{ExecutionContext, Future} -import scala.language.implicitConversions - - -object LuxmedApiAsync { - - private val syncApi = LuxmedApi - - def login(username: String, password: String, clientId: String = "iPhone")(implicit ec: ExecutionContext): Future[LoginResponse] = { - async(syncApi.login(username, password, clientId)) - } - - def refreshToken(refreshToken: String, clientId: String = "iPhone")(implicit ec: ExecutionContext): Future[LoginResponse] = { - async(syncApi.refreshToken(refreshToken, clientId)) - } - - def reservedVisits(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(), - toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3))(implicit ec: ExecutionContext): Future[ReservedVisitsResponse] = { - async(syncApi.reservedVisits(accessToken, tokenType, fromDate, toDate)) - } - - def visitsHistory(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1), - toDate: ZonedDateTime, page: Int = 1, pageSize: Int = 100)(implicit ec: ExecutionContext): Future[VisitsHistoryResponse] = { - async(syncApi.visitsHistory(accessToken, tokenType, fromDate, toDate, page, pageSize)) - } - - def reservationFilter(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(), - toDate: Option[ZonedDateTime] = None, cityId: Option[Long] = None, - serviceId: Option[Long] = None)(implicit ec: ExecutionContext): Future[ReservationFilterResponse] = { - async(syncApi.reservationFilter(accessToken, tokenType, fromDate, toDate, cityId, serviceId)) - } - - def availableTerms(accessToken: String, tokenType: String, payerId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long, doctorId: Option[Long], - fromDate: ZonedDateTime = ZonedDateTime.now(), toDate: Option[ZonedDateTime] = None, timeOfDay: Int = 0, - languageId: Long = 10, findFirstFreeTerm: Boolean = true)(implicit ec: ExecutionContext): Future[AvailableTermsResponse] = { - async(syncApi.availableTerms(accessToken, tokenType, cityId, payerId, clinicId, serviceId, doctorId, fromDate, toDate, timeOfDay, languageId, findFirstFreeTerm)) - } - - def temporaryReservation(accessToken: String, tokenType: String, temporaryReservationRequest: TemporaryReservationRequest)(implicit ec: ExecutionContext): Future[TemporaryReservationResponse] = { - async(syncApi.temporaryReservation(accessToken, tokenType, temporaryReservationRequest)) - } - - def deleteTemporaryReservation(accessToken: String, tokenType: String, temporaryReservationId: Long)(implicit ec: ExecutionContext): Future[HttpResponse[String]] = { - async(syncApi.deleteTemporaryReservation(accessToken, tokenType, temporaryReservationId)) - } - - def valuations(accessToken: String, tokenType: String, valuationsRequest: ValuationsRequest)(implicit ec: ExecutionContext): Future[ValuationsResponse] = { - async(syncApi.valuations(accessToken, tokenType, valuationsRequest)) - } - - def reservation(accessToken: String, tokenType: String, reservationRequest: ReservationRequest)(implicit ec: ExecutionContext): Future[ReservationResponse] = { - async(syncApi.reservation(accessToken, tokenType, reservationRequest)) - } - - def deleteReservation(accessToken: String, tokenType: String, reservationId: Long)(implicit ec: ExecutionContext): Future[HttpResponse[String]] = { - async(syncApi.deleteReservation(accessToken, tokenType, reservationId)) - } - - private def async[T](f: => Either[Throwable, T])(implicit ec: ExecutionContext) = { - Future(f).flatMap { - case Right(r) => Future.successful(r) - case Left(ex) => Future.failed(ex) - } - } -} diff --git a/api/src/main/scala/com/lbs/api/exception/GenericException.scala b/api/src/main/scala/com/lbs/api/exception/GenericException.scala index 170369e..9038211 100644 --- a/api/src/main/scala/com/lbs/api/exception/GenericException.scala +++ b/api/src/main/scala/com/lbs/api/exception/GenericException.scala @@ -1,6 +1,6 @@ package com.lbs.api.exception -class GenericException(code: Int, status: String, message: String) extends ApiException(message) { +class GenericException(val code: Int, val status: String, val message: String) extends ApiException(message) { override def toString: String = s"Code: $code, status: $status, message: $message" } diff --git a/api/src/main/scala/com/lbs/api/exception/ServiceIsAlreadyBookedException.scala b/api/src/main/scala/com/lbs/api/exception/ServiceIsAlreadyBookedException.scala index 1e3b12b..57d3fb3 100644 --- a/api/src/main/scala/com/lbs/api/exception/ServiceIsAlreadyBookedException.scala +++ b/api/src/main/scala/com/lbs/api/exception/ServiceIsAlreadyBookedException.scala @@ -1,4 +1,4 @@ package com.lbs.api.exception -class ServiceIsAlreadyBookedException extends ApiException("Service is already booked") +class ServiceIsAlreadyBookedException extends ApiException("You have already booked this service") diff --git a/api/src/main/scala/com/lbs/api/http/package.scala b/api/src/main/scala/com/lbs/api/http/package.scala index 701e130..a321be6 100644 --- a/api/src/main/scala/com/lbs/api/http/package.scala +++ b/api/src/main/scala/com/lbs/api/http/package.scala @@ -56,18 +56,19 @@ package object http extends Logger { } private def luxmedErrorToApiException[T <: LuxmedBaseError](ler: HttpResponse[T]): ApiException = { - ler.body match { + val genericException = ler.body match { case e: LuxmedCompositeError => new GenericException(ler.code, ler.statusLine, e.errors.map(_.message).mkString("; ")) case e: LuxmedError => - val errorMessage = e.message.toLowerCase - if (errorMessage.contains("invalid login or password")) - new InvalidLoginOrPasswordException - else if (errorMessage.contains("have already booked this service")) - new ServiceIsAlreadyBookedException - else - new GenericException(ler.code, ler.statusLine, e.message) + new GenericException(ler.code, ler.statusLine, e.message) } + + val errorMessage = genericException.message.toLowerCase + if (errorMessage.contains("invalid login or password")) + new InvalidLoginOrPasswordException + else if (errorMessage.contains("already booked this service")) + new ServiceIsAlreadyBookedException + else genericException } private def extractLuxmedError(httpResponse: Try[HttpResponse[String]]) = { diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8295719..29394a5 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.4' services: luxmedbookingservice: - image: eugenezadyra/luxmed-bot:1.0.0 + image: eugenezadyra/luxmed-bot:1.0.1 environment: DB_HOST: "database" volumes: diff --git a/server/src/main/scala/com/lbs/server/conversation/Book.scala b/server/src/main/scala/com/lbs/server/conversation/Book.scala index b51c78c..a78c392 100644 --- a/server/src/main/scala/com/lbs/server/conversation/Book.scala +++ b/server/src/main/scala/com/lbs/server/conversation/Book.scala @@ -16,7 +16,7 @@ import com.lbs.server.conversation.base.Conversation import com.lbs.server.lang.{Localizable, Localization} import com.lbs.server.repository.model.Monitoring import com.lbs.server.service.{ApiService, DataService, MonitoringService} -import com.lbs.server.util.MessageExtractors.CallbackCommand +import com.lbs.server.util.MessageExtractors.{BooleanString, CallbackCommand} import com.lbs.server.util.ServerModelConverters._ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: DataService, monitoringService: MonitoringService, @@ -165,7 +165,7 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da goto(requestDateFrom) using bookingData.copy(dateFrom = ZonedDateTime.now(), dateTo = ZonedDateTime.now().plusDays(1L)) case Msg(CallbackCommand(Tags.CreateMonitoring), _) => - goto(askMonitoringOptions) + goto(askMonitoringAutobookOption) } private def awaitReservation: Step = @@ -200,14 +200,24 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da } } - private def askMonitoringOptions: Step = + private def askMonitoringAutobookOption: Step = ask { _ => bot.sendMessage(userId.source, lang.chooseTypeOfMonitoring, inlineKeyboard = createInlineKeyboard(Seq(Button(lang.bookByApplication, Tags.BookByApplication), Button(lang.bookManually, Tags.BookManually)), columns = 1)) } onReply { - case Msg(CallbackCommand(autobookStr), bookingData: BookingData) => - val autobook = autobookStr.toBoolean - goto(createMonitoring) using bookingData.copy(autobook = autobook) + case Msg(CallbackCommand(BooleanString(autobook)), bookingData: BookingData) => + val data = bookingData.copy(autobook = autobook) + if(autobook) goto(askMonitoringRebookOption) using data + else goto(createMonitoring) using data + } + + private def askMonitoringRebookOption: Step = + ask { _ => + bot.sendMessage(userId.source, lang.rebookIfExists, + inlineKeyboard = createInlineKeyboard(Seq(Button(lang.yes, Tags.RebookYes), Button(lang.no, Tags.RebookNo)), columns = 1)) + } onReply { + case Msg(CallbackCommand(BooleanString(rebookIfExists)), bookingData: BookingData) => + goto(createMonitoring) using bookingData.copy(rebookIfExists = rebookIfExists) } private def createMonitoring: Step = @@ -244,8 +254,10 @@ object Book { case class BookingData(cityId: IdName = null, clinicId: IdName = null, serviceId: IdName = null, doctorId: IdName = null, dateFrom: ZonedDateTime = ZonedDateTime.now(), - dateTo: ZonedDateTime = ZonedDateTime.now().plusDays(1L), timeFrom: LocalTime = LocalTime.of(7, 0), timeTo: LocalTime = LocalTime.of(21, 0), autobook: Boolean = false, term: Option[AvailableVisitsTermPresentation] = None, - temporaryReservationId: Option[Long] = None, valuations: Option[ValuationsResponse] = None) + dateTo: ZonedDateTime = ZonedDateTime.now().plusDays(1L), timeFrom: LocalTime = LocalTime.of(7, 0), + timeTo: LocalTime = LocalTime.of(21, 0), autobook: Boolean = false, rebookIfExists: Boolean = false, + term: Option[AvailableVisitsTermPresentation] = None, temporaryReservationId: Option[Long] = None, + valuations: Option[ValuationsResponse] = None) object Tags { val Cancel = "cancel" @@ -255,6 +267,8 @@ object Book { val CreateMonitoring = "create_monitoring" val BookManually = "false" val BookByApplication = "true" + val RebookYes = "true" + val RebookNo = "false" } } diff --git a/server/src/main/scala/com/lbs/server/lang/En.scala b/server/src/main/scala/com/lbs/server/lang/En.scala index 04e4f1f..cec8b61 100644 --- a/server/src/main/scala/com/lbs/server/lang/En.scala +++ b/server/src/main/scala/com/lbs/server/lang/En.scala @@ -95,6 +95,8 @@ object En extends Lang { override def bookManually: String = "👤 Book manually" + override def rebookIfExists: String = " Do you want to update term if reservation already exists?" + override def city: String = "city" override def clinic: String = "clinic" diff --git a/server/src/main/scala/com/lbs/server/lang/Lang.scala b/server/src/main/scala/com/lbs/server/lang/Lang.scala index 7ef800a..0ce376a 100644 --- a/server/src/main/scala/com/lbs/server/lang/Lang.scala +++ b/server/src/main/scala/com/lbs/server/lang/Lang.scala @@ -85,6 +85,8 @@ trait Lang { def bookManually: String + def rebookIfExists: String + def city: String def clinic: String diff --git a/server/src/main/scala/com/lbs/server/lang/Ua.scala b/server/src/main/scala/com/lbs/server/lang/Ua.scala index fe99f8a..4f7307c 100644 --- a/server/src/main/scala/com/lbs/server/lang/Ua.scala +++ b/server/src/main/scala/com/lbs/server/lang/Ua.scala @@ -95,6 +95,8 @@ object Ua extends Lang { override def bookManually: String = "👤 Ручна резервація" + override def rebookIfExists: String = " Чи хотіли би ви змінити термін в разі, якщо резервація вже існує?" + override def city: String = "місто" override def clinic: String = "клініка" diff --git a/server/src/main/scala/com/lbs/server/repository/model/Monitoring.scala b/server/src/main/scala/com/lbs/server/repository/model/Monitoring.scala index 4f5f521..20478ab 100644 --- a/server/src/main/scala/com/lbs/server/repository/model/Monitoring.scala +++ b/server/src/main/scala/com/lbs/server/repository/model/Monitoring.scala @@ -78,6 +78,10 @@ class Monitoring extends RecordId { @Column(nullable = false) var autobook: Boolean = false + @BeanProperty + @Column(name = "rebook_if_exists", nullable = false) + var rebookIfExists: Boolean = false + @BeanProperty @Column(nullable = false) var created: ZonedDateTime = _ @@ -90,7 +94,7 @@ class Monitoring extends RecordId { object Monitoring { def apply(userId: Long, accountId: Long, chatId: String, sourceSystemId: Long, cityId: Long, cityName: String, clinicId: Option[Long], clinicName: String, serviceId: Long, serviceName: String, doctorId: Option[Long], doctorName: String, dateFrom: ZonedDateTime, - dateTo: ZonedDateTime, autobook: Boolean = false, created: ZonedDateTime = ZonedDateTime.now(), timeFrom: LocalTime, timeTo: LocalTime, + dateTo: ZonedDateTime, autobook: Boolean = false, rebookIfExists: Boolean = false, created: ZonedDateTime = ZonedDateTime.now(), timeFrom: LocalTime, timeTo: LocalTime, active: Boolean = true): Monitoring = { val monitoring = new Monitoring monitoring.userId = userId @@ -110,6 +114,7 @@ object Monitoring { monitoring.timeFrom = timeFrom monitoring.timeTo = timeTo monitoring.autobook = autobook + monitoring.rebookIfExists = rebookIfExists monitoring.created = created monitoring.active = active monitoring diff --git a/server/src/main/scala/com/lbs/server/service/ApiService.scala b/server/src/main/scala/com/lbs/server/service/ApiService.scala index 51236fc..5a16fbc 100644 --- a/server/src/main/scala/com/lbs/server/service/ApiService.scala +++ b/server/src/main/scala/com/lbs/server/service/ApiService.scala @@ -5,6 +5,7 @@ import java.time.{LocalTime, ZonedDateTime} import com.lbs.api.LuxmedApi import com.lbs.api.json.model._ +import com.lbs.server.util.ServerModelConverters._ import org.jasypt.util.text.TextEncryptor import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @@ -91,6 +92,68 @@ class ApiService extends SessionSupport { LuxmedApi.reservation(session.accessToken, session.tokenType, reservationRequest) } + def reserveVisit(accountId: Long, term: AvailableVisitsTermPresentation): Either[Throwable, ReservationResponse] = { + val temporaryReservationRequest = term.mapTo[TemporaryReservationRequest] + val valuationsRequest = term.mapTo[ValuationsRequest] + for { + okResponse <- temporaryReservation(accountId, temporaryReservationRequest, valuationsRequest) + (temporaryReservation, valuations) = okResponse + temporaryReservationId = temporaryReservation.id + visitTermVariant = valuations.visitTermVariants.head + reservationRequest = (temporaryReservationId, visitTermVariant, term).mapTo[ReservationRequest] + reservation <- reservation(accountId, reservationRequest) + } yield reservation + } + + def canTermBeChanged(accountId: Long, reservationId: Long): Either[Throwable, HttpResponse[String]] = + withSession(accountId) { session => + LuxmedApi.canTermBeChanged(session.accessToken, session.tokenType, reservationId) + } + + + def detailToChangeTerm(accountId: Long, reservationId: Long): Either[Throwable, ChangeTermDetailsResponse] = + withSession(accountId) { session => + LuxmedApi.detailToChangeTerm(session.accessToken, session.tokenType, reservationId) + } + + def temporaryReservationToChangeTerm(accountId: Long, reservationId: Long, temporaryReservationRequest: TemporaryReservationRequest, valuationsRequest: ValuationsRequest): Either[Throwable, (TemporaryReservationResponse, ValuationsResponse)] = + withSession(accountId) { session => + LuxmedApi.temporaryReservationToChangeTerm(session.accessToken, session.tokenType, reservationId, temporaryReservationRequest) match { + case Left(ex) => Left(ex) + case Right(temporaryReservation) => + LuxmedApi.valuationToChangeTerm(session.accessToken, session.tokenType, reservationId, valuationsRequest) match { + case Left(ex) => Left(ex) + case Right(valuationsResponse) => Right(temporaryReservation -> valuationsResponse) + } + } + } + + def valuationToChangeTerm(accountId: Long, reservationId: Long, valuationsRequest: ValuationsRequest): Either[Throwable, ValuationsResponse] = + withSession(accountId) { session => + LuxmedApi.valuationToChangeTerm(session.accessToken, session.tokenType, reservationId, valuationsRequest) + } + + def changeTerm(accountId: Long, reservationId: Long, reservationRequest: ReservationRequest): Either[Throwable, ChangeTermResponse] = + withSession(accountId) { session => + LuxmedApi.changeTerm(session.accessToken, session.tokenType, reservationId, reservationRequest) + } + + def updateTerm(accountId: Long, reservationId: Long, term: AvailableVisitsTermPresentation): Either[Throwable, ChangeTermResponse] = { + val temporaryReservationRequest = term.mapTo[TemporaryReservationRequest] + val valuationsRequest = term.mapTo[ValuationsRequest] + val canTermBeChangedResponse = canTermBeChanged(accountId, reservationId) + if (canTermBeChangedResponse.exists(_.code == 204)) { + for { + okResponse <- temporaryReservationToChangeTerm(accountId, reservationId, temporaryReservationRequest, valuationsRequest) + (temporaryReservation, valuations) = okResponse + temporaryReservationId = temporaryReservation.id + visitTermVariant = valuations.visitTermVariants.head + reservationRequest = (temporaryReservationId, visitTermVariant, term).mapTo[ReservationRequest] + reservation <- changeTerm(accountId, reservationId, reservationRequest) + } yield reservation + } else Left(new RuntimeException(s"Term for reservation [$reservationId] can't be changed")) + } + def visitsHistory(accountId: Long, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1), toDate: ZonedDateTime = ZonedDateTime.now(), page: Int = 1, pageSize: Int = 100): Either[Throwable, List[HistoricVisit]] = withSession(accountId) { session => diff --git a/server/src/main/scala/com/lbs/server/service/MonitoringService.scala b/server/src/main/scala/com/lbs/server/service/MonitoringService.scala index a7f6594..814b2b4 100644 --- a/server/src/main/scala/com/lbs/server/service/MonitoringService.scala +++ b/server/src/main/scala/com/lbs/server/service/MonitoringService.scala @@ -4,22 +4,21 @@ package com.lbs.server.service import java.time.ZonedDateTime import java.util.concurrent.ScheduledFuture -import com.lbs.api.exception.InvalidLoginOrPasswordException -import com.lbs.api.json.model.{AvailableVisitsTermPresentation, ReservationRequest, TemporaryReservationRequest, ValuationsRequest} +import com.lbs.api.exception.{InvalidLoginOrPasswordException, ServiceIsAlreadyBookedException} +import com.lbs.api.json.model.AvailableVisitsTermPresentation import com.lbs.bot.Bot import com.lbs.bot.model.{MessageSource, MessageSourceSystem} import com.lbs.common.{Logger, Scheduler} import com.lbs.server.lang.Localization import com.lbs.server.repository.model._ import com.lbs.server.util.DateTimeUtil._ -import com.lbs.server.util.ServerModelConverters._ import javax.annotation.PostConstruct import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import scala.collection.mutable import scala.concurrent.duration._ -import scala.util.Random +import scala.util.{Failure, Random} @Service class MonitoringService extends Logger { @@ -147,25 +146,22 @@ class MonitoringService extends Logger { } private def bookAppointment(term: AvailableVisitsTermPresentation, monitoring: Monitoring): Unit = { - val temporaryReservationRequest = term.mapTo[TemporaryReservationRequest] - val valuationsRequest = term.mapTo[ValuationsRequest] - val reservationMaybe = for { - okResponse <- apiService.temporaryReservation(monitoring.accountId, temporaryReservationRequest, valuationsRequest) - (temporaryReservation, valuations) = okResponse - temporaryReservationId = temporaryReservation.id - visitTermVariant = valuations.visitTermVariants.head - reservationRequest = (temporaryReservationId, visitTermVariant, term).mapTo[ReservationRequest] - reservation <- apiService.reservation(monitoring.accountId, reservationRequest) - } yield reservation - - reservationMaybe match { + apiService.reserveVisit(monitoring.accountId, term).toTry.recoverWith { + case _: ServiceIsAlreadyBookedException if monitoring.rebookIfExists => + info(s"Service [${monitoring.serviceName}] is already booked. Trying to update term") + val reservation = apiService.reservedVisits(monitoring.accountId, toDate = ZonedDateTime.now().plusMonths(6)).map(_.head) + reservation.toTry.flatMap { r => + val reservationId = r.reservationId + apiService.updateTerm(monitoring.accountId, reservationId, term).toTry + } + case ex => Failure(ex) + }.toEither match { case Right(_) => bot.sendMessage(monitoring.source, lang(monitoring.userId).appointmentIsBooked(term, monitoring)) deactivateMonitoring(monitoring.recordId) case Left(ex) => error(s"Unable to book appointment by monitoring [${monitoring.recordId}]", ex) } - } def deactivateMonitoring(monitoringId: JLong): Unit = { diff --git a/server/src/main/scala/com/lbs/server/util/package.scala b/server/src/main/scala/com/lbs/server/util/package.scala index f15f8e6..ba73555 100644 --- a/server/src/main/scala/com/lbs/server/util/package.scala +++ b/server/src/main/scala/com/lbs/server/util/package.scala @@ -14,6 +14,7 @@ import com.lbs.server.repository.model.{History, Monitoring} import scala.collection.generic.CanBuildFrom import scala.language.{higherKinds, implicitConversions} +import scala.util.Try package object util { @@ -42,7 +43,8 @@ package object util { dateTo = bookingData.dateTo, timeFrom = bookingData.timeFrom, timeTo = bookingData.timeTo, - autobook = bookingData.autobook + autobook = bookingData.autobook, + rebookIfExists = bookingData.rebookIfExists ) } } @@ -117,6 +119,10 @@ package object util { def unapply(cmd: Command): Option[String] = cmd.callbackData } + object BooleanString { + def unapply(string: String): Option[Boolean] = Try(string.toBoolean).toOption + } + } object DateTimeUtil { @@ -154,4 +160,11 @@ package object util { } } + implicit class RichEither[T](either: Either[Throwable, T]) { + def toTry: Try[T] = either match { + case Left(ex) => throw ex + case Right(v) => Try(v) + } + } + }