abstracted luxmed api over cats monad error

This commit is contained in:
Eugene Zadyra
2019-05-09 12:08:18 +02:00
parent 823b409c92
commit 84007fb140
12 changed files with 171 additions and 109 deletions

View File

@@ -4,18 +4,21 @@ package com.lbs.api
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import cats.implicits.toFunctorOps
import com.lbs.api.ApiResponseMutators._ import com.lbs.api.ApiResponseMutators._
import com.lbs.api.http._ import com.lbs.api.http._
import com.lbs.api.http.headers._ import com.lbs.api.http.headers._
import com.lbs.api.json.JsonSerializer.extensions._ import com.lbs.api.json.JsonSerializer.extensions._
import com.lbs.api.json.model._ import com.lbs.api.json.model.{AvailableTermsResponse, ReservationFilterResponse, ReservedVisitsResponse, VisitsHistoryResponse, _}
import scalaj.http.{HttpRequest, HttpResponse} import scalaj.http.{HttpRequest, HttpResponse}
object LuxmedApi extends ApiBase { import scala.language.higherKinds
class LuxmedApi[F[_] : ThrowableMonad] extends ApiBase {
private val dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") private val dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
def login(username: String, password: String, clientId: String = "iPhone"): Either[Throwable, LoginResponse] = { def login(username: String, password: String, clientId: String = "iPhone"): F[LoginResponse] = {
val request = http("token"). val request = http("token").
header(`Content-Type`, "application/x-www-form-urlencoded"). header(`Content-Type`, "application/x-www-form-urlencoded").
header(`x-api-client-identifier`, clientId). header(`x-api-client-identifier`, clientId).
@@ -26,7 +29,7 @@ object LuxmedApi extends ApiBase {
post[LoginResponse](request) post[LoginResponse](request)
} }
def refreshToken(refreshToken: String, clientId: String = "iPhone"): Either[Throwable, LoginResponse] = { def refreshToken(refreshToken: String, clientId: String = "iPhone"): F[LoginResponse] = {
val request = http("token"). val request = http("token").
header(`Content-Type`, "application/x-www-form-urlencoded"). header(`Content-Type`, "application/x-www-form-urlencoded").
header(`x-api-client-identifier`, clientId). header(`x-api-client-identifier`, clientId).
@@ -37,7 +40,7 @@ object LuxmedApi extends ApiBase {
} }
def reservedVisits(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(), def reservedVisits(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(),
toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3)): Either[Throwable, ReservedVisitsResponse] = { toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3)): F[ReservedVisitsResponse] = {
val request = http("visits/reserved"). val request = http("visits/reserved").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken"). header(Authorization, s"$tokenType $accessToken").
@@ -47,7 +50,7 @@ object LuxmedApi extends ApiBase {
} }
def visitsHistory(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1), def visitsHistory(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1),
toDate: ZonedDateTime = ZonedDateTime.now(), page: Int = 1, pageSize: Int = 100): Either[Throwable, VisitsHistoryResponse] = { toDate: ZonedDateTime = ZonedDateTime.now(), page: Int = 1, pageSize: Int = 100): F[VisitsHistoryResponse] = {
val request = http("visits/history"). val request = http("visits/history").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken"). header(Authorization, s"$tokenType $accessToken").
@@ -60,7 +63,7 @@ object LuxmedApi extends ApiBase {
def reservationFilter(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(), def reservationFilter(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(),
toDate: Option[ZonedDateTime] = None, cityId: Option[Long] = None, clinicId: Option[Long] = None, toDate: Option[ZonedDateTime] = None, cityId: Option[Long] = None, clinicId: Option[Long] = None,
serviceId: Option[Long] = None): Either[Throwable, ReservationFilterResponse] = { serviceId: Option[Long] = None): F[ReservationFilterResponse] = {
val request = http("visits/available-terms/reservation-filter"). val request = http("visits/available-terms/reservation-filter").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken"). header(Authorization, s"$tokenType $accessToken").
@@ -74,7 +77,7 @@ object LuxmedApi extends ApiBase {
def availableTerms(accessToken: String, tokenType: String, payerId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long, doctorId: Option[Long], 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, fromDate: ZonedDateTime = ZonedDateTime.now(), toDate: Option[ZonedDateTime] = None, timeOfDay: Int = 0,
languageId: Long = 10, findFirstFreeTerm: Boolean = false): Either[Throwable, AvailableTermsResponse] = { languageId: Long = 10, findFirstFreeTerm: Boolean = false): F[AvailableTermsResponse] = {
val request = http("visits/available-terms"). val request = http("visits/available-terms").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken"). header(Authorization, s"$tokenType $accessToken").
@@ -91,35 +94,35 @@ object LuxmedApi extends ApiBase {
get[AvailableTermsResponse](request).mutate get[AvailableTermsResponse](request).mutate
} }
def temporaryReservation(accessToken: String, tokenType: String, temporaryReservationRequest: TemporaryReservationRequest): Either[Throwable, TemporaryReservationResponse] = { def temporaryReservation(accessToken: String, tokenType: String, temporaryReservationRequest: TemporaryReservationRequest): F[TemporaryReservationResponse] = {
val request = http("visits/temporary-reservation"). val request = http("visits/temporary-reservation").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken") header(Authorization, s"$tokenType $accessToken")
post[TemporaryReservationResponse](request, bodyOpt = Some(temporaryReservationRequest)) post[TemporaryReservationResponse](request, bodyOpt = Some(temporaryReservationRequest))
} }
def deleteTemporaryReservation(accessToken: String, tokenType: String, temporaryReservationId: Long): Either[Throwable, HttpResponse[String]] = { def deleteTemporaryReservation(accessToken: String, tokenType: String, temporaryReservationId: Long): F[HttpResponse[String]] = {
val request = http(s"visits/temporary-reservation/$temporaryReservationId"). val request = http(s"visits/temporary-reservation/$temporaryReservationId").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken") header(Authorization, s"$tokenType $accessToken")
delete(request) delete(request)
} }
def valuations(accessToken: String, tokenType: String, valuationsRequest: ValuationsRequest): Either[Throwable, ValuationsResponse] = { def valuations(accessToken: String, tokenType: String, valuationsRequest: ValuationsRequest): F[ValuationsResponse] = {
val request = http("visits/available-terms/valuations"). val request = http("visits/available-terms/valuations").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken") header(Authorization, s"$tokenType $accessToken")
post[ValuationsResponse](request, bodyOpt = Some(valuationsRequest)) post[ValuationsResponse](request, bodyOpt = Some(valuationsRequest))
} }
def reservation(accessToken: String, tokenType: String, reservationRequest: ReservationRequest): Either[Throwable, ReservationResponse] = { def reservation(accessToken: String, tokenType: String, reservationRequest: ReservationRequest): F[ReservationResponse] = {
val request = http("visits/reserved"). val request = http("visits/reserved").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken") header(Authorization, s"$tokenType $accessToken")
post[ReservationResponse](request, bodyOpt = Some(reservationRequest)) post[ReservationResponse](request, bodyOpt = Some(reservationRequest))
} }
def deleteReservation(accessToken: String, tokenType: String, reservationId: Long): Either[Throwable, HttpResponse[String]] = { def deleteReservation(accessToken: String, tokenType: String, reservationId: Long): F[HttpResponse[String]] = {
val request = http(s"visits/reserved/$reservationId"). val request = http(s"visits/reserved/$reservationId").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken") header(Authorization, s"$tokenType $accessToken")
@@ -127,63 +130,63 @@ object LuxmedApi extends ApiBase {
} }
//204 means OK? //204 means OK?
def canTermBeChanged(accessToken: String, tokenType: String, reservationId: Long): Either[Throwable, HttpResponse[String]] = { def canTermBeChanged(accessToken: String, tokenType: String, reservationId: Long): F[HttpResponse[String]] = {
val request = http(s"visits/reserved/$reservationId/can-term-be-changed"). val request = http(s"visits/reserved/$reservationId/can-term-be-changed").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken") header(Authorization, s"$tokenType $accessToken")
request.toEither request.invoke
} }
def detailToChangeTerm(accessToken: String, tokenType: String, reservationId: Long): Either[Throwable, ChangeTermDetailsResponse] = { def detailToChangeTerm(accessToken: String, tokenType: String, reservationId: Long): F[ChangeTermDetailsResponse] = {
val request = http(s"visits/reserved/$reservationId/details-to-change-term"). val request = http(s"visits/reserved/$reservationId/details-to-change-term").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken") header(Authorization, s"$tokenType $accessToken")
get[ChangeTermDetailsResponse](request) get[ChangeTermDetailsResponse](request)
} }
def temporaryReservationToChangeTerm(accessToken: String, tokenType: String, reservationId: Long, temporaryReservationRequest: TemporaryReservationRequest): Either[Throwable, TemporaryReservationResponse] = { def temporaryReservationToChangeTerm(accessToken: String, tokenType: String, reservationId: Long, temporaryReservationRequest: TemporaryReservationRequest): F[TemporaryReservationResponse] = {
val request = http(s"visits/reserved/$reservationId/temporary-reservation-to-change-term"). val request = http(s"visits/reserved/$reservationId/temporary-reservation-to-change-term").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken") header(Authorization, s"$tokenType $accessToken")
post[TemporaryReservationResponse](request, bodyOpt = Some(temporaryReservationRequest)) post[TemporaryReservationResponse](request, bodyOpt = Some(temporaryReservationRequest))
} }
def valuationToChangeTerm(accessToken: String, tokenType: String, reservationId: Long, valuationsRequest: ValuationsRequest): Either[Throwable, ValuationsResponse] = { def valuationToChangeTerm(accessToken: String, tokenType: String, reservationId: Long, valuationsRequest: ValuationsRequest): F[ValuationsResponse] = {
val request = http(s"visits/reserved/$reservationId/valuations-to-change-term"). val request = http(s"visits/reserved/$reservationId/valuations-to-change-term").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken") header(Authorization, s"$tokenType $accessToken")
post[ValuationsResponse](request, bodyOpt = Some(valuationsRequest)) post[ValuationsResponse](request, bodyOpt = Some(valuationsRequest))
} }
def changeTerm(accessToken: String, tokenType: String, reservationId: Long, reservationRequest: ReservationRequest): Either[Throwable, ChangeTermResponse] = { def changeTerm(accessToken: String, tokenType: String, reservationId: Long, reservationRequest: ReservationRequest): F[ChangeTermResponse] = {
val request = http(s"visits/reserved/$reservationId/term"). val request = http(s"visits/reserved/$reservationId/term").
header(`Content-Type`, "application/json"). header(`Content-Type`, "application/json").
header(Authorization, s"$tokenType $accessToken") header(Authorization, s"$tokenType $accessToken")
put[ChangeTermResponse](request, bodyOpt = Some(reservationRequest)) put[ChangeTermResponse](request, bodyOpt = Some(reservationRequest))
} }
private def get[T <: SerializableJsonObject](request: HttpRequest)(implicit mf: scala.reflect.Manifest[T]): Either[Throwable, T] = { private def get[T <: SerializableJsonObject](request: HttpRequest)(implicit mf: scala.reflect.Manifest[T]): F[T] = {
request.toEither.map(_.body.as[T]) request.invoke.map(_.body.as[T])
} }
private def post[T <: SerializableJsonObject](request: HttpRequest, bodyOpt: Option[SerializableJsonObject] = None)(implicit mf: scala.reflect.Manifest[T]): Either[Throwable, T] = { private def post[T <: SerializableJsonObject](request: HttpRequest, bodyOpt: Option[SerializableJsonObject] = None)(implicit mf: scala.reflect.Manifest[T]): F[T] = {
val postRequest = bodyOpt match { val postRequest = bodyOpt match {
case Some(body) => request.postData(body.asJson) case Some(body) => request.postData(body.asJson)
case None => request.postForm case None => request.postForm
} }
postRequest.toEither.map(_.body.as[T]) postRequest.invoke.map(_.body.as[T])
} }
private def put[T <: SerializableJsonObject](request: HttpRequest, bodyOpt: Option[SerializableJsonObject] = None)(implicit mf: scala.reflect.Manifest[T]): Either[Throwable, T] = { private def put[T <: SerializableJsonObject](request: HttpRequest, bodyOpt: Option[SerializableJsonObject] = None)(implicit mf: scala.reflect.Manifest[T]): F[T] = {
val putRequest = bodyOpt match { val putRequest = bodyOpt match {
case Some(body) => request.put(body.asJson) case Some(body) => request.put(body.asJson)
case None => request.method("PUT") case None => request.method("PUT")
} }
putRequest.toEither.map(_.body.as[T]) putRequest.invoke.map(_.body.as[T])
} }
private def delete(request: HttpRequest): Either[Throwable, HttpResponse[String]] = { private def delete(request: HttpRequest): F[HttpResponse[String]] = {
request.postForm.method("DELETE").toEither request.postForm.method("DELETE").invoke
} }
} }

View File

@@ -1,6 +1,6 @@
package com.lbs.api.exception package com.lbs.api.exception
class GenericException(val code: Int, val status: String, val message: String) extends ApiException(message) { case class GenericException(code: Int, message: String) extends ApiException(message) {
override def toString: String = s"Code: $code, status: $status, message: $message" override def toString: String = s"Code: $code, message: $message"
} }

View File

@@ -1,14 +1,16 @@
package com.lbs.api package com.lbs.api
import com.lbs.api.exception.{ApiException, GenericException, InvalidLoginOrPasswordException, ServiceIsAlreadyBookedException, SessionExpiredException} import cats.MonadError
import cats.implicits._
import com.lbs.api.exception._
import com.lbs.api.json.JsonSerializer.extensions._ import com.lbs.api.json.JsonSerializer.extensions._
import com.lbs.api.json.model._ import com.lbs.api.json.model._
import com.lbs.common.Logger import com.lbs.common.Logger
import scalaj.http.{HttpRequest, HttpResponse} import scalaj.http.{HttpRequest, HttpResponse}
import scala.concurrent.{ExecutionContext, Future} import scala.language.higherKinds
import scala.util.{Failure, Try} import scala.util.{Failure, Success, Try}
package object http extends Logger { package object http extends Logger {
@@ -29,25 +31,25 @@ package object http extends Logger {
def asEntity[T <: SerializableJsonObject](implicit mf: scala.reflect.Manifest[T]): HttpResponse[T] = { def asEntity[T <: SerializableJsonObject](implicit mf: scala.reflect.Manifest[T]): HttpResponse[T] = {
httpResponse.copy(body = httpResponse.body.as[T]) httpResponse.copy(body = httpResponse.body.as[T])
} }
def asEntityAsync[T <: SerializableJsonObject](implicit mf: scala.reflect.Manifest[T], ec: ExecutionContext): Future[HttpResponse[T]] = {
Future(asEntity[T])
}
} }
implicit class ExtendedHttpRequest(httpRequest: HttpRequest) { implicit class ExtendedHttpRequest[F[_] : ThrowableMonad](httpRequest: HttpRequest) {
def invoke: F[HttpResponse[String]] = {
def toEither: Either[Throwable, HttpResponse[String]] = { val me = MonadError[F, Throwable]
toTry.toEither
}
def toTry: Try[HttpResponse[String]] = {
debug(s"Sending request:\n${hidePasswords(httpRequest)}") debug(s"Sending request:\n${hidePasswords(httpRequest)}")
val httpResponse = Try(httpRequest.asString) val httpResponse = me.pure(httpRequest.asString)
debug(s"Received response:\n$httpResponse") debug(s"Received response:\n$httpResponse")
extractLuxmedError(httpResponse) match {
case Some(error) => Failure(error) httpResponse.flatMap { response =>
case None => httpResponse.map(_.throwError) val errorMaybe = extractLuxmedError(response)
errorMaybe match {
case Some(error) => me.raiseError(error)
case None =>
Try(response.throwError) match {
case Failure(error) => me.raiseError(error)
case Success(value) => me.pure(value)
}
}
} }
} }
@@ -55,8 +57,8 @@ package object http extends Logger {
value.map(v => httpRequest.param(key, v)).getOrElse(httpRequest) value.map(v => httpRequest.param(key, v)).getOrElse(httpRequest)
} }
private def luxmedErrorToApiException[T <: LuxmedBaseError](ler: HttpResponse[T]): ApiException = { private def luxmedErrorToApiException[T <: LuxmedBaseError](code: Int, error: T): ApiException = {
val message = ler.body.message val message = error.message
val errorMessage = message.toLowerCase val errorMessage = message.toLowerCase
if (errorMessage.contains("invalid login or password")) if (errorMessage.contains("invalid login or password"))
new InvalidLoginOrPasswordException new InvalidLoginOrPasswordException
@@ -65,16 +67,17 @@ package object http extends Logger {
else if (errorMessage.contains("session has expired")) else if (errorMessage.contains("session has expired"))
new SessionExpiredException new SessionExpiredException
else else
new GenericException(ler.code, ler.statusLine, message) new GenericException(code, message)
} }
private def extractLuxmedError(httpResponse: Try[HttpResponse[String]]) = { private def extractLuxmedError(httpResponse: HttpResponse[String]) = {
httpResponse.flatMap { response => val body = httpResponse.body
Try(response.asEntity[LuxmedErrorsMap]) val code = httpResponse.code
.orElse(Try(response.asEntity[LuxmedErrorsList])) Try(body.as[LuxmedErrorsMap])
.orElse(Try(response.asEntity[LuxmedError])) .orElse(Try(body.as[LuxmedErrorsList]))
.map(e => luxmedErrorToApiException(e.asInstanceOf[HttpResponse[LuxmedBaseError]])) .orElse(Try(body.as[LuxmedError]))
}.toOption .map(error => luxmedErrorToApiException(code, error))
.toOption
} }
private def hidePasswords(httpRequest: HttpRequest) = { private def hidePasswords(httpRequest: HttpRequest) = {

View File

@@ -2,5 +2,5 @@
package com.lbs.api.json.model package com.lbs.api.json.model
case class LuxmedErrorsMap(errors: Map[String, List[String]]) extends SerializableJsonObject with LuxmedBaseError { case class LuxmedErrorsMap(errors: Map[String, List[String]]) extends SerializableJsonObject with LuxmedBaseError {
override def message: String = errors.values.mkString("; ") override def message: String = errors.values.map(_.mkString("; ")).mkString("; ")
} }

View File

@@ -1,13 +1,18 @@
package com.lbs package com.lbs
import cats.MonadError
import cats.implicits._
import com.lbs.api.json.model.{AvailableTermsResponse, ReservationFilterResponse, ReservedVisitsResponse, VisitsHistoryResponse} import com.lbs.api.json.model.{AvailableTermsResponse, ReservationFilterResponse, ReservedVisitsResponse, VisitsHistoryResponse}
import com.softwaremill.quicklens._ import com.softwaremill.quicklens._
import scala.language.higherKinds
import scala.util.matching.Regex import scala.util.matching.Regex
package object api { package object api {
type ThrowableMonad[F[_]] = MonadError[F, Throwable]
object ApiResponseMutators { object ApiResponseMutators {
private val DoctorPrefixes: Regex = """\s*(dr\s*n.\s*med.|dr\s*hab.\s*n.\s*med|lek.\s*med.|lek.\s*stom.)\s*""".r private val DoctorPrefixes: Regex = """\s*(dr\s*n.\s*med.|dr\s*hab.\s*n.\s*med|lek.\s*med.|lek.\s*stom.)\s*""".r
@@ -17,8 +22,8 @@ package object api {
def mutate(response: T): T def mutate(response: T): T
} }
implicit class ResponseOps[T: ResponseMutator](response: Either[Throwable, T]) { implicit class ResponseOps[T: ResponseMutator, F[_] : ThrowableMonad](response: F[T]) {
def mutate: Either[Throwable, T] = { def mutate: F[T] = {
val mutator = implicitly[ResponseMutator[T]] val mutator = implicitly[ResponseMutator[T]]
response.map(mutator.mutate) response.map(mutator.mutate)
} }

View File

@@ -0,0 +1,38 @@
package com.lbs.api.http
import cats.instances.either._
import com.lbs.api.exception.GenericException
import org.mockito.Mockito._
import org.scalatest.mockito.MockitoSugar
import org.scalatest.{BeforeAndAfterEach, FunSuiteLike, Matchers}
import scalaj.http.{HttpRequest, HttpResponse}
class ExtendedHttpRequestSpec extends FunSuiteLike with Matchers with MockitoSugar with BeforeAndAfterEach {
private val request = mock[HttpRequest]
private type ThrowableOr[T] = Either[Throwable, T]
override protected def beforeEach(): Unit = {
reset(request)
when(request.params).thenReturn(Seq())
}
test("ok response") {
val okResponse = HttpResponse("ok", 200, Map())
when(request.asString).thenReturn(okResponse)
assert(invoke(request) == Right(okResponse))
}
test("error response") {
val errorResponse = HttpResponse("""{"Errors":{"ToDate.Date":["'To Date. Date' must be greater than or equal to '06/04/2018 00:00:00'."]}}""", 200, Map())
when(request.asString).thenReturn(errorResponse)
val result = invoke(request)
assert(result == Left(GenericException(200, "'To Date. Date' must be greater than or equal to '06/04/2018 00:00:00'.")))
}
private def invoke(request: HttpRequest) = {
ExtendedHttpRequest[ThrowableOr](request).invoke
}
}

View File

@@ -2,4 +2,5 @@ dependencies {
compile group: "org.slf4j", name: "slf4j-api", version: "1.7.25" compile group: "org.slf4j", name: "slf4j-api", version: "1.7.25"
compile group: "ch.qos.logback", name: "logback-classic", version: "1.2.3" compile group: "ch.qos.logback", name: "logback-classic", version: "1.2.3"
compile group: "ch.qos.logback", name: "logback-core", version: "1.2.3" compile group: "ch.qos.logback", name: "logback-core", version: "1.2.3"
compile("org.typelevel:cats-core_2.12:2.0.0-M1")
} }

View File

@@ -5,6 +5,7 @@ import akka.actor.ActorSystem
import com.lbs.api.json.model.IdName import com.lbs.api.json.model.IdName
import com.lbs.bot.model.{Button, Command, TaggedButton} import com.lbs.bot.model.{Button, Command, TaggedButton}
import com.lbs.bot.{Bot, _} import com.lbs.bot.{Bot, _}
import com.lbs.server.ThrowableOr
import com.lbs.server.conversation.Login.UserId import com.lbs.server.conversation.Login.UserId
import com.lbs.server.conversation.StaticData._ import com.lbs.server.conversation.StaticData._
import com.lbs.server.conversation.base.{Conversation, Interactional} import com.lbs.server.conversation.base.{Conversation, Interactional}
@@ -78,6 +79,6 @@ object StaticData {
case class FindOptions(searchText: String) case class FindOptions(searchText: String)
case class FoundOptions(option: Either[Throwable, List[IdName]]) case class FoundOptions(option: ThrowableOr[List[IdName]])
} }

View File

@@ -6,12 +6,13 @@ import com.lbs.bot.model.Command
import com.lbs.server.conversation.Book.BookingData import com.lbs.server.conversation.Book.BookingData
import com.lbs.server.conversation.StaticData.{FindOptions, FoundOptions, LatestOptions, StaticDataConfig} import com.lbs.server.conversation.StaticData.{FindOptions, FoundOptions, LatestOptions, StaticDataConfig}
import com.lbs.server.conversation.base.Conversation import com.lbs.server.conversation.base.Conversation
import com.lbs.server.ThrowableOr
trait StaticDataForBooking extends Conversation[BookingData] { trait StaticDataForBooking extends Conversation[BookingData] {
private[conversation] def staticData: StaticData private[conversation] def staticData: StaticData
protected def withFunctions(latestOptions: => Seq[IdName], staticOptions: => Either[Throwable, List[IdName]], applyId: IdName => BookingData): Step => MessageProcessorFn = { protected def withFunctions(latestOptions: => Seq[IdName], staticOptions: => ThrowableOr[List[IdName]], applyId: IdName => BookingData): Step => MessageProcessorFn = {
nextStep: Step => { nextStep: Step => {
case Msg(cmd: Command, _) => case Msg(cmd: Command, _) =>
staticData ! cmd staticData ! cmd
@@ -38,7 +39,7 @@ trait StaticDataForBooking extends Conversation[BookingData] {
} }
} }
private def filterOptions(options: Either[Throwable, List[IdName]], searchText: String) = { private def filterOptions(options: ThrowableOr[List[IdName]], searchText: String) = {
options.map(opt => opt.filter(c => c.name.toLowerCase.contains(searchText))) options.map(opt => opt.filter(c => c.name.toLowerCase.contains(searchText)))
} }
} }

View File

@@ -0,0 +1,5 @@
package com.lbs
package object server {
type ThrowableOr[T] = Either[Throwable, T]
}

View File

@@ -3,8 +3,10 @@ package com.lbs.server.service
import java.time.{LocalTime, ZonedDateTime} import java.time.{LocalTime, ZonedDateTime}
import cats.instances.either._
import com.lbs.api.LuxmedApi import com.lbs.api.LuxmedApi
import com.lbs.api.json.model._ import com.lbs.api.json.model._
import com.lbs.server.ThrowableOr
import com.lbs.server.util.ServerModelConverters._ import com.lbs.server.util.ServerModelConverters._
import org.jasypt.util.text.TextEncryptor import org.jasypt.util.text.TextEncryptor
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@@ -19,34 +21,36 @@ class ApiService extends SessionSupport {
@Autowired @Autowired
private var textEncryptor: TextEncryptor = _ private var textEncryptor: TextEncryptor = _
def getAllCities(accountId: Long): Either[Throwable, List[IdName]] = private val luxmedApi = new LuxmedApi[ThrowableOr]
def getAllCities(accountId: Long): ThrowableOr[List[IdName]] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.reservationFilter(session.accessToken, session.tokenType).map(_.cities) luxmedApi.reservationFilter(session.accessToken, session.tokenType).map(_.cities)
} }
def getAllClinics(accountId: Long, cityId: Long): Either[Throwable, List[IdName]] = def getAllClinics(accountId: Long, cityId: Long): ThrowableOr[List[IdName]] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.reservationFilter(session.accessToken, luxmedApi.reservationFilter(session.accessToken,
session.tokenType, cityId = Some(cityId)).map(_.clinics) session.tokenType, cityId = Some(cityId)).map(_.clinics)
} }
def getAllServices(accountId: Long, cityId: Long, clinicId: Option[Long]): Either[Throwable, List[IdName]] = def getAllServices(accountId: Long, cityId: Long, clinicId: Option[Long]): ThrowableOr[List[IdName]] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.reservationFilter(session.accessToken, luxmedApi.reservationFilter(session.accessToken,
session.tokenType, cityId = Some(cityId), session.tokenType, cityId = Some(cityId),
clinicId = clinicId).map(_.services) clinicId = clinicId).map(_.services)
} }
def getAllDoctors(accountId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long): Either[Throwable, List[IdName]] = def getAllDoctors(accountId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long): ThrowableOr[List[IdName]] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.reservationFilter(session.accessToken, luxmedApi.reservationFilter(session.accessToken,
session.tokenType, cityId = Some(cityId), session.tokenType, cityId = Some(cityId),
clinicId = clinicId, serviceId = Some(serviceId)).map(_.doctors) clinicId = clinicId, serviceId = Some(serviceId)).map(_.doctors)
} }
def getPayers(accountId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long): Either[Throwable, (Option[IdName], Seq[IdName])] = def getPayers(accountId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long): ThrowableOr[(Option[IdName], Seq[IdName])] =
withSession(accountId) { session => withSession(accountId) { session =>
val reservationFilterResponse = LuxmedApi.reservationFilter(session.accessToken, val reservationFilterResponse = luxmedApi.reservationFilter(session.accessToken,
session.tokenType, cityId = Some(cityId), session.tokenType, cityId = Some(cityId),
clinicId = clinicId, serviceId = Some(serviceId)) clinicId = clinicId, serviceId = Some(serviceId))
reservationFilterResponse.map { response => reservationFilterResponse.map { response =>
@@ -56,9 +60,9 @@ class ApiService extends SessionSupport {
def getAvailableTerms(accountId: Long, payerId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long, doctorId: Option[Long], def getAvailableTerms(accountId: Long, payerId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long, doctorId: Option[Long],
fromDate: ZonedDateTime = ZonedDateTime.now(), toDate: Option[ZonedDateTime] = None, timeFrom: LocalTime, timeTo: LocalTime, fromDate: ZonedDateTime = ZonedDateTime.now(), toDate: Option[ZonedDateTime] = None, timeFrom: LocalTime, timeTo: LocalTime,
languageId: Long = 10, findFirstFreeTerm: Boolean = false): Either[Throwable, List[AvailableVisitsTermPresentation]] = languageId: Long = 10, findFirstFreeTerm: Boolean = false): ThrowableOr[List[AvailableVisitsTermPresentation]] =
withSession(accountId) { session => withSession(accountId) { session =>
val termsEither = LuxmedApi.availableTerms(session.accessToken, session.tokenType, payerId, cityId, clinicId, serviceId, doctorId, val termsEither = luxmedApi.availableTerms(session.accessToken, session.tokenType, payerId, cityId, clinicId, serviceId, doctorId,
fromDate, toDate, languageId = languageId, findFirstFreeTerm = findFirstFreeTerm).map(_.availableVisitsTermPresentation) fromDate, toDate, languageId = languageId, findFirstFreeTerm = findFirstFreeTerm).map(_.availableVisitsTermPresentation)
termsEither.map { terms => termsEither.map { terms =>
terms.filter { term => terms.filter { term =>
@@ -68,25 +72,25 @@ class ApiService extends SessionSupport {
} }
} }
def temporaryReservation(accountId: Long, temporaryReservationRequest: TemporaryReservationRequest, valuationsRequest: ValuationsRequest): Either[Throwable, (TemporaryReservationResponse, ValuationsResponse)] = def temporaryReservation(accountId: Long, temporaryReservationRequest: TemporaryReservationRequest, valuationsRequest: ValuationsRequest): ThrowableOr[(TemporaryReservationResponse, ValuationsResponse)] =
withSession(accountId) { session => withSession(accountId) { session =>
for { for {
temporaryReservation <- LuxmedApi.temporaryReservation(session.accessToken, session.tokenType, temporaryReservationRequest) temporaryReservation <- luxmedApi.temporaryReservation(session.accessToken, session.tokenType, temporaryReservationRequest)
valuationsResponse <- LuxmedApi.valuations(session.accessToken, session.tokenType, valuationsRequest) valuationsResponse <- luxmedApi.valuations(session.accessToken, session.tokenType, valuationsRequest)
} yield temporaryReservation -> valuationsResponse } yield temporaryReservation -> valuationsResponse
} }
def deleteTemporaryReservation(accountId: Long, temporaryReservationId: Long): Either[Throwable, HttpResponse[String]] = def deleteTemporaryReservation(accountId: Long, temporaryReservationId: Long): ThrowableOr[HttpResponse[String]] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.deleteTemporaryReservation(session.accessToken, session.tokenType, temporaryReservationId) luxmedApi.deleteTemporaryReservation(session.accessToken, session.tokenType, temporaryReservationId)
} }
def reservation(accountId: Long, reservationRequest: ReservationRequest): Either[Throwable, ReservationResponse] = def reservation(accountId: Long, reservationRequest: ReservationRequest): ThrowableOr[ReservationResponse] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.reservation(session.accessToken, session.tokenType, reservationRequest) luxmedApi.reservation(session.accessToken, session.tokenType, reservationRequest)
} }
def reserveVisit(accountId: Long, term: AvailableVisitsTermPresentation): Either[Throwable, ReservationResponse] = { def reserveVisit(accountId: Long, term: AvailableVisitsTermPresentation): ThrowableOr[ReservationResponse] = {
val temporaryReservationRequest = term.mapTo[TemporaryReservationRequest] val temporaryReservationRequest = term.mapTo[TemporaryReservationRequest]
val valuationsRequest = term.mapTo[ValuationsRequest] val valuationsRequest = term.mapTo[ValuationsRequest]
for { for {
@@ -99,38 +103,38 @@ class ApiService extends SessionSupport {
} yield reservation } yield reservation
} }
def canTermBeChanged(accountId: Long, reservationId: Long): Either[Throwable, HttpResponse[String]] = def canTermBeChanged(accountId: Long, reservationId: Long): ThrowableOr[HttpResponse[String]] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.canTermBeChanged(session.accessToken, session.tokenType, reservationId) luxmedApi.canTermBeChanged(session.accessToken, session.tokenType, reservationId)
} }
def detailToChangeTerm(accountId: Long, reservationId: Long): Either[Throwable, ChangeTermDetailsResponse] = def detailToChangeTerm(accountId: Long, reservationId: Long): ThrowableOr[ChangeTermDetailsResponse] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.detailToChangeTerm(session.accessToken, session.tokenType, reservationId) luxmedApi.detailToChangeTerm(session.accessToken, session.tokenType, reservationId)
} }
def temporaryReservationToChangeTerm(accountId: Long, reservationId: Long, temporaryReservationRequest: TemporaryReservationRequest, valuationsRequest: ValuationsRequest): Either[Throwable, (TemporaryReservationResponse, ValuationsResponse)] = def temporaryReservationToChangeTerm(accountId: Long, reservationId: Long, temporaryReservationRequest: TemporaryReservationRequest, valuationsRequest: ValuationsRequest): ThrowableOr[(TemporaryReservationResponse, ValuationsResponse)] =
withSession(accountId) { session => withSession(accountId) { session =>
for { for {
temporaryReservation <- LuxmedApi.temporaryReservationToChangeTerm(session.accessToken, session.tokenType, reservationId, temporaryReservationRequest) temporaryReservation <- luxmedApi.temporaryReservationToChangeTerm(session.accessToken, session.tokenType, reservationId, temporaryReservationRequest)
valuationsResponse <- LuxmedApi.valuationToChangeTerm(session.accessToken, session.tokenType, reservationId, valuationsRequest) valuationsResponse <- luxmedApi.valuationToChangeTerm(session.accessToken, session.tokenType, reservationId, valuationsRequest)
} yield temporaryReservation -> valuationsResponse } yield temporaryReservation -> valuationsResponse
} }
def valuationToChangeTerm(accountId: Long, reservationId: Long, valuationsRequest: ValuationsRequest): Either[Throwable, ValuationsResponse] = def valuationToChangeTerm(accountId: Long, reservationId: Long, valuationsRequest: ValuationsRequest): ThrowableOr[ValuationsResponse] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.valuationToChangeTerm(session.accessToken, session.tokenType, reservationId, valuationsRequest) luxmedApi.valuationToChangeTerm(session.accessToken, session.tokenType, reservationId, valuationsRequest)
} }
def changeTerm(accountId: Long, reservationId: Long, reservationRequest: ReservationRequest): Either[Throwable, ChangeTermResponse] = def changeTerm(accountId: Long, reservationId: Long, reservationRequest: ReservationRequest): ThrowableOr[ChangeTermResponse] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.changeTerm(session.accessToken, session.tokenType, reservationId, reservationRequest) luxmedApi.changeTerm(session.accessToken, session.tokenType, reservationId, reservationRequest)
} }
def updateReservedVisit(accountId: Long, term: AvailableVisitsTermPresentation): Either[Throwable, ChangeTermResponse] = { def updateReservedVisit(accountId: Long, term: AvailableVisitsTermPresentation): ThrowableOr[ChangeTermResponse] = {
val reservedVisitEither = reservedVisits(accountId, toDate = ZonedDateTime.now().plusMonths(6)).map(_.find(_.service.id == term.serviceId)) val reservedVisitMaybe = reservedVisits(accountId, toDate = ZonedDateTime.now().plusMonths(6)).map(_.find(_.service.id == term.serviceId))
reservedVisitEither match { reservedVisitMaybe match {
case Right(Some(reservedVisit: ReservedVisit)) => case Right(Some(reservedVisit: ReservedVisit)) =>
val reservationId = reservedVisit.reservationId val reservationId = reservedVisit.reservationId
val temporaryReservationRequest = term.mapTo[TemporaryReservationRequest] val temporaryReservationRequest = term.mapTo[TemporaryReservationRequest]
@@ -154,24 +158,24 @@ class ApiService extends SessionSupport {
} }
def visitsHistory(accountId: Long, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1), def visitsHistory(accountId: Long, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1),
toDate: ZonedDateTime = ZonedDateTime.now(), page: Int = 1, pageSize: Int = 100): Either[Throwable, List[HistoricVisit]] = toDate: ZonedDateTime = ZonedDateTime.now(), page: Int = 1, pageSize: Int = 100): ThrowableOr[List[HistoricVisit]] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.visitsHistory(session.accessToken, session.tokenType, fromDate, toDate, page, pageSize).map(_.historicVisits) luxmedApi.visitsHistory(session.accessToken, session.tokenType, fromDate, toDate, page, pageSize).map(_.historicVisits)
} }
def reservedVisits(accountId: Long, fromDate: ZonedDateTime = ZonedDateTime.now(), def reservedVisits(accountId: Long, fromDate: ZonedDateTime = ZonedDateTime.now(),
toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3)): Either[Throwable, List[ReservedVisit]] = toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3)): ThrowableOr[List[ReservedVisit]] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.reservedVisits(session.accessToken, session.tokenType, fromDate, toDate).map(_.reservedVisits) luxmedApi.reservedVisits(session.accessToken, session.tokenType, fromDate, toDate).map(_.reservedVisits)
} }
def deleteReservation(accountId: Long, reservationId: Long): Either[Throwable, HttpResponse[String]] = def deleteReservation(accountId: Long, reservationId: Long): ThrowableOr[HttpResponse[String]] =
withSession(accountId) { session => withSession(accountId) { session =>
LuxmedApi.deleteReservation(session.accessToken, session.tokenType, reservationId) luxmedApi.deleteReservation(session.accessToken, session.tokenType, reservationId)
} }
def login(username: String, password: String): Either[Throwable, LoginResponse] = { def login(username: String, password: String): ThrowableOr[LoginResponse] = {
LuxmedApi.login(username, textEncryptor.decrypt(password)) luxmedApi.login(username, textEncryptor.decrypt(password))
} }
private def left(msg: String) = Left(new RuntimeException(msg)) private def left(msg: String) = Left(new RuntimeException(msg))

View File

@@ -4,6 +4,7 @@ package com.lbs.server.service
import com.lbs.api.exception.SessionExpiredException import com.lbs.api.exception.SessionExpiredException
import com.lbs.api.json.model.LoginResponse import com.lbs.api.json.model.LoginResponse
import com.lbs.common.{Logger, ParametrizedLock} import com.lbs.common.{Logger, ParametrizedLock}
import com.lbs.server.ThrowableOr
import com.lbs.server.exception.UserNotFoundException import com.lbs.server.exception.UserNotFoundException
import scala.collection.mutable import scala.collection.mutable
@@ -12,7 +13,7 @@ trait SessionSupport extends Logger {
case class Session(accessToken: String, tokenType: String) case class Session(accessToken: String, tokenType: String)
def login(username: String, password: String): Either[Throwable, LoginResponse] def login(username: String, password: String): ThrowableOr[LoginResponse]
protected def dataService: DataService protected def dataService: DataService
@@ -20,10 +21,10 @@ trait SessionSupport extends Logger {
private val lock = new ParametrizedLock[Long] private val lock = new ParametrizedLock[Long]
protected def withSession[T](accountId: Long)(fn: Session => Either[Throwable, T]): Either[Throwable, T] = protected def withSession[T](accountId: Long)(fn: Session => ThrowableOr[T]): ThrowableOr[T] =
lock.obtainLock(accountId).synchronized { lock.obtainLock(accountId).synchronized {
def auth: Either[Throwable, Session] = { def auth: ThrowableOr[Session] = {
val credentialsMaybe = dataService.getCredentials(accountId) val credentialsMaybe = dataService.getCredentials(accountId)
credentialsMaybe match { credentialsMaybe match {
case Some(credentials) => case Some(credentials) =>
@@ -33,7 +34,7 @@ trait SessionSupport extends Logger {
} }
} }
def getSession: Either[Throwable, Session] = { def getSession: ThrowableOr[Session] = {
sessions.get(accountId) match { sessions.get(accountId) match {
case Some(sess) => Right(sess) case Some(sess) => Right(sess)
case None => case None =>