diff --git a/api/build.gradle b/api/build.gradle index 7265bb5..4100376 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,7 +1,7 @@ dependencies { compile project(':common') - compile group: "org.scalaj", name: "scalaj-http_2.12", version: "2.4.1" + compile group: "org.scalaj", name: "scalaj-http_2.12", version: "2.4.2" compile group: "org.json4s", name: "json4s-jackson_2.12", version: "3.6.0-M3" compile group: "com.softwaremill.quicklens", name: "quicklens_2.12", version: "1.4.12" } diff --git a/api/src/main/scala/com/lbs/api/ApiBase.scala b/api/src/main/scala/com/lbs/api/ApiBase.scala index 6459c0b..8ccd916 100644 --- a/api/src/main/scala/com/lbs/api/ApiBase.scala +++ b/api/src/main/scala/com/lbs/api/ApiBase.scala @@ -1,26 +1,52 @@ package com.lbs.api +import com.lbs.api.http.Session import com.lbs.api.http.headers._ import scalaj.http.{BaseHttp, HttpRequest} +import java.net.HttpCookie + object ApiHttp extends BaseHttp( - userAgent = "PatientPortal/4.20.5 (pl.luxmed.pp.LUX-MED; build:853; iOS 13.5.1) Alamofire/4.9.1" + userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 15_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" ) trait ApiBase { private val CommonHeaders = Map( Host -> "portalpacjenta.luxmed.pl", - `Custom-User-Agent` -> "PatientPortal; 4.20.5; 4380E6AC-D291-4895-8B1B-F774C318BD7D; iOS; 14.5.1; iPhone8,1", - Accept -> "*/*", - Connection -> "keep-alive", - `Accept-Encoding` -> "gzip;q=1.0, compress;q=0.5", - `Accept-Language` -> "en;q=1.0, en-PL;q=0.9, pl-PL;q=0.8, ru-PL;q=0.7, uk-PL;q=0.6" + Origin -> "https://portalpacjenta.luxmed.pl", + `Custom-User-Agent` -> "Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", + `User-Agent` -> "Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", + Accept -> "application/json, text/plain, */*", + `Accept-Encoding` -> "gzip, deflate, br", + `Accept-Language` -> "pl;q=1.0, pl;q=0.9, en;q=0.8" ) - protected def http(url: String): HttpRequest = { - ApiHttp(s"https://portalpacjenta.luxmed.pl/PatientPortalMobileAPI/api/$url").headers(CommonHeaders) + protected def httpUnauthorized(url: String): HttpRequest = { + ApiHttp(s"https://portalpacjenta.luxmed.pl/PatientPortalMobileAPI/api/$url") + .headers(CommonHeaders) + } + + + protected def http(url: String, session: Session): HttpRequest = { + ApiHttp(s"https://portalpacjenta.luxmed.pl/PatientPortalMobileAPI/api/$url") + .headers(CommonHeaders) + .cookies(session.cookies) + .header(Authorization, s"${session.tokenType} ${session.accessToken}") + } + + protected def httpNewApi(url: String, session: Session, cookiesMaybe: Option[Seq[HttpCookie]] = None): HttpRequest = { + val req = ApiHttp(s"https://portalpacjenta.luxmed.pl/PatientPortal/$url") + .headers(CommonHeaders) + .header(Authorization, session.accessToken) + cookiesMaybe.map(cookies => req.cookies(cookies)).getOrElse(req.cookies(session.cookies)) + } + + protected def httpNewApi(url: String, cookies: IndexedSeq[HttpCookie]): HttpRequest = { + ApiHttp(s"https://portalpacjenta.luxmed.pl/PatientPortal/$url") + .headers(CommonHeaders) + .cookies(cookies) } } diff --git a/api/src/main/scala/com/lbs/api/LuxmedApi.scala b/api/src/main/scala/com/lbs/api/LuxmedApi.scala index 689c3cc..cc0b9b8 100644 --- a/api/src/main/scala/com/lbs/api/LuxmedApi.scala +++ b/api/src/main/scala/com/lbs/api/LuxmedApi.scala @@ -1,25 +1,24 @@ package com.lbs.api -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - import cats.implicits.toFunctorOps -import com.lbs.api.ApiResponseMutators._ import com.lbs.api.http._ import com.lbs.api.http.headers._ import com.lbs.api.json.JsonSerializer.extensions._ -import com.lbs.api.json.model.{AvailableTermsResponse, ReservationFilterResponse, ReservedVisitsResponse, VisitsHistoryResponse, _} +import com.lbs.api.json.model.{EventsResponse, TermsIndexResponse, _} import scalaj.http.{HttpRequest, HttpResponse} +import java.time.format.DateTimeFormatter +import java.time.{LocalDateTime, ZonedDateTime} 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 dateFormatNewPortal = DateTimeFormatter.ofPattern("yyyy-MM-dd") + private val dateFormatEvents = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ") - def login(username: String, password: String, clientId: String = "iPhone"): F[LoginResponse] = { - val request = http("token"). + def login(username: String, password: String, clientId: String = "iPhone"): F[HttpResponse[LoginResponse]] = { + val request = httpUnauthorized("token"). header(`Content-Type`, "application/x-www-form-urlencoded"). header(`x-api-client-identifier`, clientId). param("client_id", clientId). @@ -29,160 +28,132 @@ class LuxmedApi[F[_] : ThrowableMonad] extends ApiBase { post[LoginResponse](request) } - def refreshToken(refreshToken: String, clientId: String = "iPhone"): F[LoginResponse] = { - val request = http("token"). - header(`Content-Type`, "application/x-www-form-urlencoded"). - header(`x-api-client-identifier`, clientId). - param("client_id", clientId). - param("grant_type", "refresh_token"). - param("refresh_token", refreshToken) - post[LoginResponse](request) + def loginToApp(session: Session): F[HttpResponse[Unit]] = { + val request = httpNewApi("Account/LogInToApp?app=search&lang=pl&client=2&paymentSupported=true", session) + .header(Authorization, session.accessToken) + getVoid(request) } - def reservedVisits(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(), - toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3)): F[ReservedVisitsResponse] = { - val request = http("visits/reserved"). - header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken"). - param("fromDate", dateFormat.format(fromDate)). - param("toDate", dateFormat.format(toDate)) - get[ReservedVisitsResponse](request).mutate + def getForgeryToken(session: Session): F[HttpResponse[ForgeryTokenResponse]] = { + val request = httpNewApi("security/getforgerytoken", session) + get[ForgeryTokenResponse](request) } - def visitsHistory(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1), - toDate: ZonedDateTime = ZonedDateTime.now(), page: Int = 1, pageSize: Int = 100): F[VisitsHistoryResponse] = { - val request = http("visits/history"). + def events(session: Session, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1), + toDate: ZonedDateTime = ZonedDateTime.now()): F[EventsResponse] = { + val request = http("Events", session). header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken"). - param("fromDate", dateFormat.format(fromDate)). - param("toDate", dateFormat.format(toDate)). - param("page", page.toString). - param("pageSize", pageSize.toString) - get[VisitsHistoryResponse](request).mutate + param("filter.filterDateFrom", dateFormatEvents.format(fromDate)). + param("filter.filterDateTo", dateFormatEvents.format(toDate)) + get[EventsResponse](request).map(_.body) } - def reservationFilter(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(), - toDate: Option[ZonedDateTime] = None, cityId: Option[Long] = None, clinicId: Option[Long] = None, - serviceId: Option[Long] = None): F[ReservationFilterResponse] = { - val request = http("visits/available-terms/reservation-filter"). + def dictionaryCities(session: Session): F[List[DictionaryCity]] = { + val request = httpNewApi("NewPortal/Dictionary/cities", session). + header(`Content-Type`, "application/json") + getList[DictionaryCity](request).map(_.body) + } + + def dictionaryServiceVariants(session: Session): F[List[DictionaryServiceVariants]] = { + val request = httpNewApi("NewPortal/Dictionary/serviceVariantsGroups", session). + header(`Content-Type`, "application/json") + getList[DictionaryServiceVariants](request).map(_.body) + } + + def dictionaryFacilitiesAndDoctors(session: Session, cityId: Option[Long], serviceVariantId: Option[Long]): F[FacilitiesAndDoctors] = { + val request = httpNewApi("NewPortal/Dictionary/facilitiesAndDoctors", session). header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken"). param("cityId", cityId.map(_.toString)). - param("clinicId", clinicId.map(_.toString)). - param("fromDate", dateFormat.format(fromDate)). - param("toDate", toDate.map(dateFormat.format)). - param("serviceId", serviceId.map(_.toString)) - get[ReservationFilterResponse](request).mutate + param("serviceVariantId", serviceVariantId.map(_.toString)) + get[FacilitiesAndDoctors](request).map(_.body) } - 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 = false): F[AvailableTermsResponse] = { - val request = http("visits/available-terms"). + def termsIndex(session: Session, cityId: Long, clinicId: Option[Long], serviceId: Long, doctorId: Option[Long], + fromDate: LocalDateTime = LocalDateTime.now(), toDate: LocalDateTime, languageId: Long = 10): F[TermsIndexResponse] = { + val request = httpNewApi("NewPortal/terms/index", session). header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken"). param("cityId", cityId.toString). - param("doctorId", doctorId.map(_.toString)). - param("findFirstFreeTerm", findFirstFreeTerm.toString). - param("fromDate", dateFormat.format(fromDate)). + param("serviceVariantId", serviceId.toString). param("languageId", languageId.toString). - param("payerId", payerId.toString). - param("clinicId", clinicId.map(_.toString)). - param("serviceId", serviceId.toString). - param("timeOfDay", timeOfDay.toString). - param("toDate", dateFormat.format(toDate.getOrElse(fromDate.plusMonths(3)))) - get[AvailableTermsResponse](request).mutate + param("searchDateFrom", dateFormatNewPortal.format(fromDate)). + param("searchDateTo", dateFormatNewPortal.format(toDate)). + param("searchDatePreset", 14.toString). + param("facilitiesIds", clinicId.map(_.toString)). + param("doctorsIds", doctorId.map(_.toString)). + param("nextSearch", false.toString). + param("searchByMedicalSpecialist", false.toString) + get[TermsIndexResponse](request).map(_.body) } - def temporaryReservation(accessToken: String, tokenType: String, temporaryReservationRequest: TemporaryReservationRequest): F[TemporaryReservationResponse] = { - val request = http("visits/temporary-reservation"). - header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken") - post[TemporaryReservationResponse](request, bodyOpt = Some(temporaryReservationRequest)) + def reservationLockterm(session: Session, xsrfToken: XsrfToken, reservationLocktermRequest: ReservationLocktermRequest): F[ReservationLocktermResponse] = { + val request = httpNewApi("NewPortal/reservation/lockterm", session, Some(session.cookies ++ xsrfToken.cookies)). + header(`Content-Type`, "application/json") + .header(`xsrf-token`, xsrfToken.token) + post[ReservationLocktermResponse](request, bodyOpt = Some(reservationLocktermRequest)).map(_.body) } - def deleteTemporaryReservation(accessToken: String, tokenType: String, temporaryReservationId: Long): F[HttpResponse[String]] = { - val request = http(s"visits/temporary-reservation/$temporaryReservationId"). - header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken") + def deleteTemporaryReservation(session: Session, xsrfToken: XsrfToken, temporaryReservationId: Long): F[Unit] = { + val request = httpNewApi(s"NewPortal/reservation/releaseterm?reservationId=$temporaryReservationId", session, Some(session.cookies ++ xsrfToken.cookies)). + header(`Content-Type`, "application/json") + .header(`xsrf-token`, xsrfToken.token) + postVoid(request, bodyOpt = Some(Empty())) + } + + def reservationConfirm(session: Session, xsrfToken: XsrfToken, reservationConfirmRequest: ReservationConfirmRequest): F[ReservationConfirmResponse] = { + val request = httpNewApi("NewPortal/reservation/confirm", session, Some(session.cookies ++ xsrfToken.cookies)). + header(`Content-Type`, "application/json") + .header(`xsrf-token`, xsrfToken.token) + post[ReservationConfirmResponse](request, bodyOpt = Some(reservationConfirmRequest)).map(_.body) + } + + def reservationChangeTerm(session: Session, xsrfToken: XsrfToken, reservationChangetermRequest: ReservationChangetermRequest): F[ReservationConfirmResponse] = { + val request = httpNewApi("NewPortal/reservation/changeterm", session, Some(session.cookies ++ xsrfToken.cookies)). + header(`Content-Type`, "application/json") + .header(`xsrf-token`, xsrfToken.token) + post[ReservationConfirmResponse](request, bodyOpt = Some(reservationChangetermRequest)).map(_.body) + } + + def reservationDelete(session: Session, reservationId: Long): F[HttpResponse[String]] = { + val request = http(s"events/Visit/$reservationId", session). + header(`Content-Type`, "application/json") delete(request) } - def valuations(accessToken: String, tokenType: String, valuationsRequest: ValuationsRequest): F[ValuationsResponse] = { - val request = http("visits/available-terms/valuations"). - header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken") - post[ValuationsResponse](request, bodyOpt = Some(valuationsRequest)) + private def get[T <: SerializableJsonObject](request: HttpRequest)(implicit mf: scala.reflect.Manifest[T]): F[HttpResponse[T]] = { + request.invoke.map(r => r.copy(body = r.body.as[T])) } - def reservation(accessToken: String, tokenType: String, reservationRequest: ReservationRequest): F[ReservationResponse] = { - val request = http("visits/reserved"). - header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken") - post[ReservationResponse](request, bodyOpt = Some(reservationRequest)) + private def getList[T <: SerializableJsonObject](request: HttpRequest)(implicit mf: scala.reflect.Manifest[T]): F[HttpResponse[List[T]]] = { + request.invoke.map(r => r.copy(body = r.body.asList[T])) } - def deleteReservation(accessToken: String, tokenType: String, reservationId: Long): F[HttpResponse[String]] = { - val request = http(s"visits/reserved/$reservationId"). - header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken") - delete(request) + private def getVoid[T <: SerializableJsonObject](request: HttpRequest)(implicit mf: scala.reflect.Manifest[T]): F[HttpResponse[Unit]] = { + request.invoke.map(r => r.copy(body = {})) } - //204 means OK? - def canTermBeChanged(accessToken: String, tokenType: String, reservationId: Long): F[HttpResponse[String]] = { - val request = http(s"visits/reserved/$reservationId/can-term-be-changed"). - header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken") - request.invoke - } - - def detailToChangeTerm(accessToken: String, tokenType: String, reservationId: Long): F[ChangeTermDetailsResponse] = { - val request = http(s"visits/reserved/$reservationId/details-to-change-term"). - header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken") - get[ChangeTermDetailsResponse](request) - } - - def temporaryReservationToChangeTerm(accessToken: String, tokenType: String, reservationId: Long, temporaryReservationRequest: TemporaryReservationRequest): F[TemporaryReservationResponse] = { - val request = http(s"visits/reserved/$reservationId/temporary-reservation-to-change-term"). - header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken") - post[TemporaryReservationResponse](request, bodyOpt = Some(temporaryReservationRequest)) - } - - def valuationToChangeTerm(accessToken: String, tokenType: String, reservationId: Long, valuationsRequest: ValuationsRequest): F[ValuationsResponse] = { - val request = http(s"visits/reserved/$reservationId/valuations-to-change-term"). - header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken") - post[ValuationsResponse](request, bodyOpt = Some(valuationsRequest)) - } - - def changeTerm(accessToken: String, tokenType: String, reservationId: Long, reservationRequest: ReservationRequest): F[ChangeTermResponse] = { - val request = http(s"visits/reserved/$reservationId/term"). - header(`Content-Type`, "application/json"). - header(Authorization, s"$tokenType $accessToken") - put[ChangeTermResponse](request, bodyOpt = Some(reservationRequest)) - } - - private def get[T <: SerializableJsonObject](request: HttpRequest)(implicit mf: scala.reflect.Manifest[T]): F[T] = { - request.invoke.map(_.body.as[T]) - } - - private def post[T <: SerializableJsonObject](request: HttpRequest, bodyOpt: Option[SerializableJsonObject] = None)(implicit mf: scala.reflect.Manifest[T]): F[T] = { + private def post[T <: SerializableJsonObject](request: HttpRequest, bodyOpt: Option[SerializableJsonObject] = None)(implicit mf: scala.reflect.Manifest[T]): F[HttpResponse[T]] = { val postRequest = bodyOpt match { case Some(body) => request.postData(body.asJson) case None => request.postForm } - postRequest.invoke.map(_.body.as[T]) + postRequest.invoke.map(r => r.copy(body = r.body.as[T])) } - private def put[T <: SerializableJsonObject](request: HttpRequest, bodyOpt: Option[SerializableJsonObject] = None)(implicit mf: scala.reflect.Manifest[T]): F[T] = { + private def put[T <: SerializableJsonObject](request: HttpRequest, bodyOpt: Option[SerializableJsonObject] = None)(implicit mf: scala.reflect.Manifest[T]): F[HttpResponse[T]] = { val putRequest = bodyOpt match { case Some(body) => request.put(body.asJson) case None => request.method("PUT") } - putRequest.invoke.map(_.body.as[T]) + putRequest.invoke.map(r => r.copy(body = r.body.as[T])) + } + + + private def postVoid(request: HttpRequest, bodyOpt: Option[SerializableJsonObject] = None): F[Unit] = { + val postRequest = bodyOpt match { + case Some(body) => request.postData(body.asJson) + case None => request.postForm + } + postRequest.invoke.void } private def delete(request: HttpRequest): F[HttpResponse[String]] = { diff --git a/api/src/main/scala/com/lbs/api/exception/ServiceIsAlreadyBookedException.scala b/api/src/main/scala/com/lbs/api/exception/ServiceIsAlreadyBookedException.scala deleted file mode 100644 index 57d3fb3..0000000 --- a/api/src/main/scala/com/lbs/api/exception/ServiceIsAlreadyBookedException.scala +++ /dev/null @@ -1,4 +0,0 @@ - -package com.lbs.api.exception - -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 fed55b8..7b3ca8c 100644 --- a/api/src/main/scala/com/lbs/api/http/package.scala +++ b/api/src/main/scala/com/lbs/api/http/package.scala @@ -9,33 +9,31 @@ import com.lbs.api.json.model._ import com.lbs.common.Logger import scalaj.http.{HttpRequest, HttpResponse} +import java.net.HttpCookie import scala.language.higherKinds import scala.util.{Failure, Success, Try} package object http extends Logger { + case class Session(accessToken: String, tokenType: String, cookies: Seq[HttpCookie]) + object headers { val `Content-Type` = "Content-Type" + val `xsrf-token` = "xsrf-token" val Host = "Host" + val Origin = "Origin" val Accept = "Accept" val Connection = "Connection" val `Accept-Encoding` = "Accept-Encoding" val `User-Agent` = "User-Agent" val `Custom-User-Agent` = "Custom-User-Agent" val `x-api-client-identifier` = "x-api-client-identifier" - val `Accept-Language` = "Accept-Language" + val `Accept-Language` = "accept-language" val Authorization = "Authorization" } private val SensitiveHeaders = List("passw", "access_token", "refresh_token", "authorization") - implicit class HttpResponseWithJsonDeserializationSupport(httpResponse: HttpResponse[String]) { - - def asEntity[T <: SerializableJsonObject](implicit mf: scala.reflect.Manifest[T]): HttpResponse[T] = { - httpResponse.copy(body = httpResponse.body.as[T]) - } - } - implicit class ExtendedHttpRequest[F[_] : ThrowableMonad](httpRequest: HttpRequest) { def invoke: F[HttpResponse[String]] = { val me = MonadError[F, Throwable] @@ -65,8 +63,6 @@ package object http extends Logger { val errorMessage = message.toLowerCase if (errorMessage.contains("invalid login or password")) new InvalidLoginOrPasswordException - else if (errorMessage.contains("already booked this service")) - new ServiceIsAlreadyBookedException else if (errorMessage.contains("session has expired")) new SessionExpiredException else @@ -77,7 +73,6 @@ package object http extends Logger { val body = httpResponse.body val code = httpResponse.code Try(body.as[LuxmedErrorsMap]) - .orElse(Try(body.as[LuxmedErrorsList])) .orElse(Try(body.as[LuxmedError])) .map(error => luxmedErrorToApiException(code, error)) .toOption diff --git a/api/src/main/scala/com/lbs/api/json/JsonSerializer.scala b/api/src/main/scala/com/lbs/api/json/JsonSerializer.scala index 5c81160..1fbe949 100644 --- a/api/src/main/scala/com/lbs/api/json/JsonSerializer.scala +++ b/api/src/main/scala/com/lbs/api/json/JsonSerializer.scala @@ -1,31 +1,48 @@ package com.lbs.api.json -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - import com.lbs.api.json.model.SerializableJsonObject +import com.lbs.common.Logger import org.json4s._ import org.json4s.jackson.JsonMethods._ +import java.time.format.DateTimeFormatter +import java.time.{LocalDateTime, LocalTime, ZonedDateTime} -object JsonSerializer { - private val localDateTimeSerializer = new CustomSerializer[ZonedDateTime](_ => ( { +object JsonSerializer extends Logger { + + private val zonedDateTimeSerializer = new CustomSerializer[ZonedDateTime](_ => ( { case JString(str) => ZonedDateTime.parse(str, DateTimeFormatter.ISO_OFFSET_DATE_TIME) }, { case zonedDateTime: ZonedDateTime => JString(zonedDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) } )) - private implicit val formats: Formats = DefaultFormats.withStrictArrayExtraction + localDateTimeSerializer + private val localTimeSerializer = new CustomSerializer[LocalTime](_ => ( { + case JString(str) => LocalTime.parse(str) + }, { + case localTime: LocalTime => JString(localTime.toString) + } + )) - def extract[T <: SerializableJsonObject](jsonString: String)(implicit mf: scala.reflect.Manifest[T]): T = { + private val localDateTimeSerializer = new CustomSerializer[LocalDateTime](_ => ( { + case JString(str) => LocalDateTime.parse(str) + }, { + case localTime: LocalDateTime => JString(localTime.toString) + } + )) + + private implicit val formats: Formats = DefaultFormats.withStrictArrayExtraction + zonedDateTimeSerializer + localTimeSerializer + localDateTimeSerializer + + def extract[T](jsonString: String)(implicit mf: scala.reflect.Manifest[T]): T = { parse(jsonString).camelizeKeys.extract[T] } - def write[T <: SerializableJsonObject](jsonObject: T): String = { - pretty(render(Extraction.decompose(jsonObject).pascalizeKeys)) + def write[T](jsonObject: T): String = { + val json = pretty(render(Extraction.decompose(jsonObject))) + info(json) + json } object extensions { @@ -34,6 +51,10 @@ object JsonSerializer { def as[T <: SerializableJsonObject](implicit mf: scala.reflect.Manifest[T]): T = { extract[T](jsonString) } + + def asList[T <: SerializableJsonObject](implicit mf: scala.reflect.Manifest[T]): List[T] = { + extract[List[T]](jsonString) + } } implicit class JsonObjectToString[T <: SerializableJsonObject](jsonObject: T) { diff --git a/api/src/main/scala/com/lbs/api/json/model/AvailbaleTermsResponse.scala b/api/src/main/scala/com/lbs/api/json/model/AvailbaleTermsResponse.scala deleted file mode 100644 index 8dafea5..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/AvailbaleTermsResponse.scala +++ /dev/null @@ -1,64 +0,0 @@ - -package com.lbs.api.json.model - -/** - * -{ - "AvailableVisitsTermPresentation": [ - { - "Clinic": { - "Id": 6, - "Name": "LX Wrocław - Szewska 3A" - }, - "Doctor": { - "Id": 38275, - "Name": "lek. med. ANNA ABRAMCZYK" - }, - "Impediment": { - "ImpedimentText": "", - "IsImpediment": false - }, - "IsFree": false, - "PayerDetailsList": [ - { - "BrandId": 2, - "ContractId": 1111111, - "PayerId": 22222, - "PayerName": "FIRMA POLAND SP. Z O.O.", - "ProductElementId": 3333333, - "ProductId": 44444, - "ProductInContractId": 555555, - "ServaAppId": 0, - "ServaId": 6666 - }, - { - "BrandId": 2, - "ContractId": 1111111, - "PayerId": 22222, - "PayerName": "FIRMA POLAND SP. Z O.O.", - "ProductElementId": 8547135, - "ProductId": 44444, - "ProductInContractId": 555555, - "ServaAppId": 1, - "ServaId": 6666 - } - ], - "ReferralRequiredByProduct": false, - "ReferralRequiredByService": false, - "RoomId": 543, - "ScheduleId": 3331908, - "ServiceId": 6666, - "VisitDate": { - "FormattedDate": "26th April, Thu. at 12:40 pm", - "StartDateTime": "2018-02-23T11:30:00+02:00" - } - } - ] -} - - */ -case class AvailableTermsResponse(availableVisitsTermPresentation: List[AvailableVisitsTermPresentation]) extends SerializableJsonObject - -case class AvailableVisitsTermPresentation(clinic: IdName, doctor: IdName, payerDetailsList: List[PayerDetails], - referralRequiredByProduct: Boolean, referralRequiredByService: Boolean, - roomId: Long, scheduleId: Long, serviceId: Long, visitDate: VisitDate) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/ChangeTermDetailsResponse.scala b/api/src/main/scala/com/lbs/api/json/model/ChangeTermDetailsResponse.scala deleted file mode 100644 index 9140527..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/ChangeTermDetailsResponse.scala +++ /dev/null @@ -1,23 +0,0 @@ - -package com.lbs.api.json.model - -/** -{ - "AvailableNewPayers": [ - { - "Id": 45185, - "IsFeeForService": false, - "Name": "Moja firma SP. Z O.O." - } - ], - "CityId": 5, - "Payer": { - "Id": 45185, - "IsFeeForService": false, - "Name": "Moja firma SP. Z O.O." - } -} - */ -case class ChangeTermDetailsResponse(availableNewPayers: ShortPayerDetails, cityId: Long, payer: ShortPayerDetails) extends SerializableJsonObject - -case class ShortPayerDetails(id: Long, isFeeForService: Boolean, name: String) extends SerializableJsonObject \ No newline at end of file diff --git a/api/src/main/scala/com/lbs/api/json/model/ChangeTermResponse.scala b/api/src/main/scala/com/lbs/api/json/model/ChangeTermResponse.scala deleted file mode 100644 index 75deb26..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/ChangeTermResponse.scala +++ /dev/null @@ -1,11 +0,0 @@ - -package com.lbs.api.json.model - -/** -{ - "PreparationInfo": { - "IsPreparationRequired": true - } -} - */ -case class ChangeTermResponse(preparationInfo: PreparationInfo) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/DictionaryCity.scala b/api/src/main/scala/com/lbs/api/json/model/DictionaryCity.scala new file mode 100644 index 0000000..7ecf40e --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/DictionaryCity.scala @@ -0,0 +1,21 @@ + +package com.lbs.api.json.model + +/** +[ + { + "id": 70, + "name": "Białystok" + }, + { + "id": 12, + "name": "Bielsk Podlaski" + }, + { + "id": 100, + "name": "Bielsko-Biała" + } +] + * + */ +case class DictionaryCity(override val id: Long, override val name: String) extends Identified with SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/DictionaryServiceVariants.scala b/api/src/main/scala/com/lbs/api/json/model/DictionaryServiceVariants.scala new file mode 100644 index 0000000..fe783d7 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/DictionaryServiceVariants.scala @@ -0,0 +1,175 @@ + +package com.lbs.api.json.model + +/** +[ + { + "actionCode": "", + "children": [ + { + "actionCode": null, + "children": [], + "expanded": false, + "id": 4502, + "isTelemedicine": false, + "name": "Consultation with a general practitioner", + "paymentType": 2, + "type": 0 + }, + { + "actionCode": null, + "children": [], + "expanded": false, + "id": 4480, + "isTelemedicine": false, + "name": "Gynaecological consultation", + "paymentType": 2, + "type": 0 + } + ], + "expanded": true, + "id": 2, + "isTelemedicine": false, + "name": "Most popular", + "paymentType": 0, + "type": 2 + }, + { + "actionCode": "", + "children": [ + { + "actionCode": null, + "children": [], + "expanded": false, + "id": 4387, + "isTelemedicine": false, + "name": "Allergologist consultation", + "paymentType": 2, + "type": 0 + } + ], + "expanded": true, + "id": 1, + "isTelemedicine": false, + "name": "On-site consultations", + "paymentType": 0, + "type": 1 + }, + { + "actionCode": "", + "children": [ + { + "actionCode": null, + "children": [], + "expanded": false, + "id": 13764, + "isTelemedicine": true, + "name": "Telephone consultation - Allergist", + "paymentType": 2, + "type": 0 + }, + { + "actionCode": null, + "children": [], + "expanded": false, + "id": 13775, + "isTelemedicine": true, + "name": "Telephone consultation - Cardiologist", + "paymentType": 2, + "type": 0 + }, + { + "actionCode": null, + "children": [], + "expanded": false, + "id": 13800, + "isTelemedicine": true, + "name": "Telephone consultation - Dentist", + "paymentType": 2, + "type": 0 + }, + { + "actionCode": null, + "children": [], + "expanded": false, + "id": 13766, + "isTelemedicine": true, + "name": "Telephone consultation - Dermatologist", + "paymentType": 2, + "type": 0 + } + ], + "expanded": false, + "id": 13, + "isTelemedicine": false, + "name": "Telephone consultations", + "paymentType": 0, + "type": 1 + }, + { + "actionCode": "", + "children": [ + { + "actionCode": null, + "children": [], + "expanded": false, + "id": 8904, + "isTelemedicine": false, + "name": "Arranging an appointment with a dental surgeon", + "paymentType": 2, + "type": 0 + }, + { + "actionCode": null, + "children": [], + "expanded": false, + "id": 6817, + "isTelemedicine": false, + "name": "Arranging an appointment with a dental hygienist", + "paymentType": 2, + "type": 0 + }, + { + "actionCode": null, + "children": [], + "expanded": false, + "id": 6621, + "isTelemedicine": false, + "name": "Arranging an appointment with a dentist", + "paymentType": 2, + "type": 0 + } + ], + "expanded": false, + "id": 4, + "isTelemedicine": false, + "name": "Dentist", + "paymentType": 0, + "type": 1 + }, + { + "actionCode": "WIZYTY_DOMOWE", + "children": [], + "expanded": false, + "id": 14, + "isTelemedicine": false, + "name": "Home visits", + "paymentType": 0, + "type": 0 + }, + { + "actionCode": "NFZ", + "children": [], + "expanded": false, + "id": 12, + "isTelemedicine": false, + "name": "NHF visits", + "paymentType": 0, + "type": 0 + } +] + * + */ +case class DictionaryServiceVariants(override val id: Long, override val name: String, expanded: Boolean, children: List[DictionaryServiceVariants], isTelemedicine: Boolean, paymentType: Long) extends Identified with SerializableJsonObject { + def flatten: List[DictionaryServiceVariants] = List(this) ::: children.flatMap(_.flatten) +} diff --git a/api/src/main/scala/com/lbs/api/json/model/Doctor.scala b/api/src/main/scala/com/lbs/api/json/model/Doctor.scala new file mode 100644 index 0000000..ab44d5c --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/Doctor.scala @@ -0,0 +1,19 @@ +package com.lbs.api.json.model + +/** + * { + "academicTitle": "dr n. med.", + "facilityGroupIds": [ + 78 + ], + "firstName": "TARAS", + "id": 11111, + "isEnglishSpeaker": true, + "lastName": "SHEVCHENKO" + } + */ + +case class Doctor(academicTitle: String, facilityGroupIds: Option[List[Long]], firstName: String, isEnglishSpeaker: Option[Boolean], + genderId: Option[Long], id: Long, lastName: String) extends Identified { + override def name: String = firstName + " " + lastName +} diff --git a/api/src/main/scala/com/lbs/api/json/model/Empty.scala b/api/src/main/scala/com/lbs/api/json/model/Empty.scala new file mode 100644 index 0000000..59fdcb6 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/Empty.scala @@ -0,0 +1,7 @@ + +package com.lbs.api.json.model + +/** +{} + */ +case class Empty() extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/EventsResponse.scala b/api/src/main/scala/com/lbs/api/json/model/EventsResponse.scala new file mode 100644 index 0000000..9aabdf3 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/EventsResponse.scala @@ -0,0 +1,109 @@ + +package com.lbs.api.json.model + +import java.time.ZonedDateTime + +/** + * { + * "DataAvailableFrom": "2016-09-01T00:00:00+02:00", + * "Events": [ + * { + *"AutoConfirmationInfo": { + *"Message": null, + *"Type": "None" + *}, + *"Clinic": { + *"Address": "WOŁOWSKA 20", + *"City": "WROCŁAW", + *"Id": 42, + *"Name": "LX Wrocław - Wołowska 20" + *}, + *"ConfirmationInfo": null, + *"Date": "2021-06-02T07:45:00+02:00", + *"DateTo": "2021-06-02T08:15:00+02:00", + *"Doctor": { + *"Id": 111111, + *"Lastname": "SHEVCHENKO", + *"Name": "TARAS", + *"Sex": "Male", + *"Title": "lek. med." + *}, + *"DownloadLinks": [], + *"EventId": 2222222, + *"EventType": "Visit", + *"FromEreferral": false, + *"HasImpediments": false, + *"HasQuestionnaireBeforeService": false, + *"IsOverbooked": false, + *"IsPaymentRequired": false, + *"IsPreparationRequired": false, + *"IsServiceWithOverbookingRegularDistribution": true, + *"Links": [ + *{ + *"ApiVersion": 1, + *"Href": "/PatientPortalMobileAPI/api/events/reservation/Visit/2222222/detail", + *"Method": "GET", + *"Rel": "events_detail" + *}, + *{ + *"ApiVersion": 1, + *"Href": "/PatientPortalMobileAPI/api/events/Visit/2222222", + *"Method": "DELETE", + *"Rel": "delete_reservation" + *}, + *{ + *"ApiVersion": 1, + *"Href": "/PatientPortalMobileAPI/api/visits/reserved/2222222/can-term-be-changed", + *"Method": "GET", + *"Rel": "get_can_term_be_changed" + *} + *], + *"OnlinePaymentType": "Possible", + *"PaymentState": "None", + *"ReferralType": "None", + *"Status": "Reserved", + *"Title": "Internista", + *"Type": "Timeline_Visit_ReservedVisit" + *}, + *{ + *"Date": "2021-03-27T16:45:00+02:00", + *"DateTo": "2021-03-27T17:15:00+02:00", + *"Doctor": { + *"Id": 11111, + *"Lastname": "LESJA", + *"Name": "UKRAINKA", + *"Sex": "Female", + *"Title": "lek. med." + *}, + *"DownloadLinks": [], + *"EventId": 3333333, + *"EventType": "Telemedicine", + *"HasPrescription": false, + *"HasRecommendations": true, + *"HasReferrals": false, + *"IsServiceWithOverbookingRegularDistribution": true, + *"Links": [ + *{ + *"ApiVersion": 1, + *"Href": "/PatientPortalMobileAPI/api/events/reservation/Telemedicine/3333333/detail", + *"Method": "GET", + *"Rel": "events_detail" + *} + *], + *"ReferralType": "None", + *"Status": "Realized", + *"Title": "Centrum Leczenia Infekcji - konsultacja telefoniczna", + *"Type": "Timeline_Telemedicine_RealizedTelemedicineVisit" + *} + *], + *"IsEndOfList": false, + *"ServerDateTime": "2021-07-01T14:32:00+02:00" +*} + */ +case class EventsResponse(events: List[Event]) extends SerializableJsonObject + +case class Event(date: ZonedDateTime, clinic: Option[EventClinic], doctor: EventDoctor, eventId: Long, status: String, title: String) + +case class EventClinic(address: String, city: String) + +case class EventDoctor(lastname: String, name: String) diff --git a/api/src/main/scala/com/lbs/api/json/model/FacilitiesAndDoctors.scala b/api/src/main/scala/com/lbs/api/json/model/FacilitiesAndDoctors.scala new file mode 100644 index 0000000..c826301 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/FacilitiesAndDoctors.scala @@ -0,0 +1,42 @@ + +package com.lbs.api.json.model + +/** +{ + "doctors": [ + { + "academicTitle": "dr n. med.", + "facilityGroupIds": [ + 78 + ], + "firstName": "TARAS", + "id": 111111, + "isEnglishSpeaker": true, + "lastName": "SHEVCHENKO" + }, + { + "academicTitle": "lek. med.", + "facilityGroupIds": [ + 78, + 127 + ], + "firstName": "VLADIMIR", + "id": 22222, + "isEnglishSpeaker": false, + "lastName": "ZELENSKIY" + } + ], + "facilities": [ + { + "id": 78, + "name": "ul. Fabryczna 6" + }, + { + "id": 127, + "name": "ul. Kwidzyńska 6" + } + ] +} + * + */ +case class FacilitiesAndDoctors(doctors: List[Doctor], facilities: List[IdName]) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/ForgeryTokenResponse.scala b/api/src/main/scala/com/lbs/api/json/model/ForgeryTokenResponse.scala new file mode 100644 index 0000000..7d640c5 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ForgeryTokenResponse.scala @@ -0,0 +1,9 @@ + +package com.lbs.api.json.model + +/** + * { + * "token": "IDtjG_ECOd_ETYE2fwrCoTcC6bW935cn_nUh6d3BaEa-jvPlHfPLOY5AkF", + * } + */ +case class ForgeryTokenResponse(token: String) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/IdName.scala b/api/src/main/scala/com/lbs/api/json/model/Identified.scala similarity index 57% rename from api/src/main/scala/com/lbs/api/json/model/IdName.scala rename to api/src/main/scala/com/lbs/api/json/model/Identified.scala index 9e806b5..05ea2e2 100644 --- a/api/src/main/scala/com/lbs/api/json/model/IdName.scala +++ b/api/src/main/scala/com/lbs/api/json/model/Identified.scala @@ -1,11 +1,16 @@ - package com.lbs.api.json.model +trait Identified { + def id: Long + def name: String + + def toIdName: IdName = IdName(id, name) +} + +case class IdName(id: Long, name: String) extends Identified { + def optionalId: Option[Long] = Option(id).filterNot(_ == -1L) +} object IdName { def from(id: java.lang.Long, name: String): IdName = new IdName(if (id != null) id else -1, name) } - -case class IdName(id: Long, name: String) { - def optionalId: Option[Long] = Option(id).filterNot(_ == -1L) -} diff --git a/api/src/main/scala/com/lbs/api/json/model/LuxmedErrorsList.scala b/api/src/main/scala/com/lbs/api/json/model/LuxmedErrorsList.scala deleted file mode 100644 index 3c1b6cc..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/LuxmedErrorsList.scala +++ /dev/null @@ -1,8 +0,0 @@ - -package com.lbs.api.json.model - -case class LuxmedErrorsList(errors: List[LuxmedErrorsListElement]) extends SerializableJsonObject with LuxmedBaseError { - override def message: String = errors.map(_.message).mkString("; ") -} - -case class LuxmedErrorsListElement(errorCode: Int, message: String, additionalData: Map[String, String]) extends SerializableJsonObject \ No newline at end of file diff --git a/api/src/main/scala/com/lbs/api/json/model/ReservationChangetermRequest.scala b/api/src/main/scala/com/lbs/api/json/model/ReservationChangetermRequest.scala new file mode 100644 index 0000000..cc124b6 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ReservationChangetermRequest.scala @@ -0,0 +1,43 @@ + +package com.lbs.api.json.model + +import java.time.LocalTime + +/** + * { + * "existingReservationId": 987654321, + *"term": { + *"date": "2021-07-03T05:30:00.000Z", + *"doctorId": 22222, + *"eReferralId": null, + *"facilityId": 33333, + *"parentReservationId": 987654321, + *"referralId": null, + *"referralRequired": false, + *"roomId": 55555, + *"scheduleId": 666666, + *"serviceVariantId": 777777, + *"temporaryReservationId": 8888888, + *"timeFrom": "08:30", + *"valuation": { + *"alternativePrice": null, + *"contractId": 99999, + *"isExternalReferralAllowed": false, + *"isReferralRequired": false, + *"payerId": 9111111, + *"price": 0, + *"productElementId": 9222222, + *"productId": 93333333, + *"productInContractId": 9444444, + *"requireReferralForPP": false, + *"valuationType": 1 + *}, + *"valuationId": null + *} +*} + */ +case class ReservationChangetermRequest(existingReservationId: Long, term: NewTerm) extends SerializableJsonObject + +case class NewTerm(date: String, doctorId: Long, facilityId: Long, parentReservationId: Long, referralRequired: Boolean, roomId: Long, + scheduleId: Long, serviceVariantId: Long, temporaryReservationId: Long, timeFrom: LocalTime, + valuation: Valuation) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/ReservationConfirmRequest.scala b/api/src/main/scala/com/lbs/api/json/model/ReservationConfirmRequest.scala new file mode 100644 index 0000000..c1f34f5 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ReservationConfirmRequest.scala @@ -0,0 +1,38 @@ + +package com.lbs.api.json.model + +import java.time.LocalTime + + +/** + * { + * "date": "2021-05-19T08:00:00.000Z", + *"doctorId": 111111, + *"eReferralId": null, + *"facilityId": 2222, + *"parentReservationId": null, + *"referralId": null, + *"referralRequired": false, + *"roomId": 1248, + *"scheduleId": 333333, + *"serviceVariantId": 444444, + *"temporaryReservationId": 4111111, + *"timeFrom": "18:45", + *"valuation": { + *"alternativePrice": null, + *"contractId": 555555, + *"isExternalReferralAllowed": false, + *"isReferralRequired": false, false, + *"payerId": 66666, + *"price": 0.0, + *"productElementId": 7777777, + *"productId": 888888, + *"productInContractId": 9999999, + *"requireReferralForPP": false, + *"valuationType": 1 + *}, + *"valuationId": null +*} + */ +case class ReservationConfirmRequest(date: String, doctorId: Long, facilityId: Long, roomId: Long, scheduleId: Long, serviceVariantId: Long, + temporaryReservationId: Long, timeFrom: LocalTime, valuation: Valuation) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/ReservationConfirmResponse.scala b/api/src/main/scala/com/lbs/api/json/model/ReservationConfirmResponse.scala new file mode 100644 index 0000000..e9db58a --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ReservationConfirmResponse.scala @@ -0,0 +1,25 @@ + +package com.lbs.api.json.model + + +/** +{ + "errors": [], + "hasErrors": false, + "hasWarnings": false, + "value": { + "canSelfConfirm": false, + "eventType": 1, + "isTelemedicine": false, + "npsToken": "babababa-9282-1662-a525-ababbabaa", + "reservationId": 2222222, + "serviceInstanceId": 33333333 + }, + "warnings": [] +} + */ +case class ReservationConfirmResponse(errors: List[String], warnings: List[String], hasErrors: Boolean, hasWarnings: Boolean, + value: ReservationConfirmValue) extends SerializableJsonObject + +case class ReservationConfirmValue(canSelfConfirm: Boolean, eventType: Long, isTelemedicine: Boolean, npsToken: String, + reservationId: Long, serviceInstanceId: Long) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/ReservationLocktermRequest.scala b/api/src/main/scala/com/lbs/api/json/model/ReservationLocktermRequest.scala new file mode 100644 index 0000000..74fc2ea --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ReservationLocktermRequest.scala @@ -0,0 +1,42 @@ + +package com.lbs.api.json.model + + +/** + * { + * "correlationId": "00000000-0000-0000-0000-000000000000", + *"date": "2021-05-19T08:00:00.000Z", + *"doctor": { + *"academicTitle": "dr n.med.", + *"firstName": "TARAS", + *"id": 11111, + *"lastName": "SHEVCHENKO" + *}, + *"doctorId": 22222, + *"eReferralId": null, + *"facilityId": 33, + *"facilityName": "Telephone consultation", + *"impedimentText": "", + *"isAdditional": false, + *"isImpediment": false, + *"isPreparationRequired": false, + *"isTelemedicine": true, + *"parentReservationId": null, + *"preparationItems": [], + *"referralId": null, + *"referralTypeId": null, + *"roomId": 3333, + *"scheduleId": 444444, + *"serviceVariantId": 555555, + *"serviceVariantName": "Telephone consultation - General practitioner", + *"timeFrom": "12:00", + *"timeTo": "12:15" +*} + */ +case class ReservationLocktermRequest(date: String, doctor: Doctor, doctorId: Long, eReferralId: String = null, + facilityId: Long, + impedimentText:String, isAdditional: Boolean, isImpediment: Boolean, + isPreparationRequired: Boolean, isTelemedicine: Boolean, parentReservationId: String = null, + preparationItems: List[PreparationItem], referralId: String = null, referralTypeId: String = null, + roomId: Long, scheduleId: Long, serviceVariantId: Long, + timeFrom: String, timeTo: String) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/ReservationLocktermResponse.scala b/api/src/main/scala/com/lbs/api/json/model/ReservationLocktermResponse.scala new file mode 100644 index 0000000..cbcdd35 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ReservationLocktermResponse.scala @@ -0,0 +1,74 @@ + +package com.lbs.api.json.model + +import java.time.LocalTime + + +/** + * { + * "errors": [], + * "hasErrors": false, + * "hasWarnings": false, + *"value": { + *"askForReferral": false, + *"changeTermAvailable": true, + *"conflictedVisit": null, + *"doctorDetails": { + *"academicTitle": "lek. med.", + *"firstName": "TARAS", + *"genderId": 1, + *"id": 11111, + *"lastName": "SHEVCHENKO" + *}, + *"isBloodExamination": false, + *"isStomatology": false, + *"relatedVisits": [ + *{ + *"date": "2021-06-03T05:20:00", + *"doctor": { + *"academicTitle": "lek. med.", + *"firstName": "LESYA", + *"genderId": 2, + *"id": 0, + *"lastName": "UKRAINKA" + *}, + *"facilityName": "LX Wrocław - Szewska 3A", + *"isAsdk": false, + *"isTelemedicine": false, + *"payerName": null, + *"reservationId": 333333, + *"serviceInstanceId": 9999999, + *"serviceVariantId": 111111, + *"serviceVariantName": "Consultation with a general practitioner", + *"timeFrom": "07:30:00", + *"timeTo": "07:45:00" + *} + *], + *"temporaryReservationId": 222222, + *"valuations": [ + *{ + *"alternativePrice": null, + *"contractId": 333333, + *"isExternalReferralAllowed": false, + *"isReferralRequired": false, + *"payerId": 44444, + *"price": 0.0, + *"productElementId": 555555, + *"productId": 666666, + *"productInContractId": 777777, + *"requireReferralForPP": false, + *"valuationType": 1 + *} + *] + *}, + *"warnings": [] +*} + */ +case class ReservationLocktermResponse(errors: List[String], warnings: List[String], hasErrors: Boolean, hasWarnings: Boolean, + value: ReservationLocktermResponseValue) extends SerializableJsonObject + +case class ReservationLocktermResponseValue(changeTermAvailable: Boolean, conflictedVisit: Option[String], doctorDetails: Doctor, + relatedVisits: List[RelatedVisit], temporaryReservationId: Long, valuations: List[Valuation]) extends SerializableJsonObject + +case class RelatedVisit(doctor: Doctor, facilityName: String, isTelemedicine: Boolean, reservationId: Long, + timeFrom: LocalTime, timeTo: LocalTime) diff --git a/api/src/main/scala/com/lbs/api/json/model/ReservationRequest.scala b/api/src/main/scala/com/lbs/api/json/model/ReservationRequest.scala deleted file mode 100644 index 66f803e..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/ReservationRequest.scala +++ /dev/null @@ -1,28 +0,0 @@ - -package com.lbs.api.json.model - -import java.time.ZonedDateTime - - -/** -{ - "ClinicId": 6, - "DoctorId": 38509, - "PayerData": { - "ContractId": 1111111, - "PayerId": 22222, - "PayerName": "FIRMA POLAND SP. Z O.O.", - "ProductElementId": 8547100, - "ProductId": 44444, - "ProductInContractId": 555555, - "ServaAppId": 0, - "ServaId": 6621 - }, - "RoomId": 159, - "ServiceId": 6621, - "StartDateTime": "2018-06-04T11:00:00+02:00", - "TemporaryReservationId": 250303839 -} - */ -case class ReservationRequest(clinicId: Long, doctorId: Long, payerData: PayerDetails, roomId: Long, serviceId: Long, - startDateTime: ZonedDateTime, temporaryReservationId: Long) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/ReservationResponse.scala b/api/src/main/scala/com/lbs/api/json/model/ReservationResponse.scala deleted file mode 100644 index fbc5a37..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/ReservationResponse.scala +++ /dev/null @@ -1,24 +0,0 @@ - -package com.lbs.api.json.model - - -/** -{ - "PreparationInfo": { - "IsPreparationRequired": true - }, - "ReservedVisitsLimitInfo": { - "CanReserve": true, - "HasPatientLimit": false, - "MaxReservedVisitsCount": null, - "Message": "", - "ReservedVisitsCount": null - } -} - */ -case class ReservationResponse(preparationInfo: PreparationInfo, reservedVisitsLimitInfo: ReservedVisitsLimitInfo) extends SerializableJsonObject - -case class PreparationInfo(isPreparationRequired: Boolean) extends SerializableJsonObject - -case class ReservedVisitsLimitInfo(canReserve: Boolean, hasPatientLimit: Boolean, maxReservedVisitsCount: Option[Int], - message: String, reservedVisitsCount: Option[Int]) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/ReservedVisitsResponse.scala b/api/src/main/scala/com/lbs/api/json/model/ReservedVisitsResponse.scala deleted file mode 100644 index 666a738..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/ReservedVisitsResponse.scala +++ /dev/null @@ -1,44 +0,0 @@ - -package com.lbs.api.json.model - -/** - * - * { - * "ReservedVisits": [ - *{ - *"CanBeCanceled": true, - *"Clinic": { - *"Id": 6, - *"Name": "LX Wrocław - Szewska 3A" - *}, - *"DoctorName": "lek. stom. TARAS SHEVCZENKO", - *"Impediment": { - *"ImpedimentText": "", - *"IsImpediment": false - *}, - *"IsAdditional": false, - *"IsPreparationRequired": false, - *"Links": [ - *{ - *"Href": "/PatientPortalMobileAPI/api/visits/preparations/6621", - *"Method": "GET", - *"Rel": "get_preparations" - *} - *], - *"ReservationId": 888888888, - *"Service": { - *"Id": 6621, - *"Name": "Umówienie wizyty u stomatologa" - *}, - *"VisitDate": { - *"FormattedDate": "21rd May, Mon. at 3:00 pm", - *"StartDateTime": "2018-05-21T15:00:00+02:00" - *} - *} - *] -*} - */ -case class ReservedVisitsResponse(reservedVisits: List[ReservedVisit]) extends SerializableJsonObject - -case class ReservedVisit(canBeCanceled: Boolean, clinic: IdName, doctorName: String, - reservationId: Long, service: IdName, visitDate: VisitDate) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/TemporaryReservationRequest.scala b/api/src/main/scala/com/lbs/api/json/model/TemporaryReservationRequest.scala deleted file mode 100644 index 769be96..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/TemporaryReservationRequest.scala +++ /dev/null @@ -1,43 +0,0 @@ - -package com.lbs.api.json.model - -import java.time.ZonedDateTime - - -/** -{ - "ClinicId": 6, - "DoctorId": 38275, - "PayerDetailsList": [ - { - "BrandId": 2, - "ContractId": 1111111, - "PayerId": 22222, - "PayerName": "FIRMA POLAND SP. Z O.O.", - "ProductElementId": 3333333, - "ProductId": 44444, - "ProductInContractId": 555555, - "ServaAppId": 0, - "ServaId": 6666 - }, - { - "BrandId": 2, - "ContractId": 1111111, - "PayerId": 22222, - "PayerName": "FIRMA POLAND SP. Z O.O.", - "ProductElementId": 8547135, - "ProductId": 44444, - "ProductInContractId": 555555, - "ServaAppId": 1, - "ServaId": 6666 - } - ], - "ReferralRequiredByService": false, - "RoomId": 543, - "ServiceId": 6666, - "StartDateTime": "2018-02-23T11:30:00+02:00" -} - */ -case class TemporaryReservationRequest(clinicId: Long, doctorId: Long, payerDetailsList: List[PayerDetails], - referralRequiredByService: Boolean, roomId: Long, serviceId: Long, - startDateTime: ZonedDateTime) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/TemporaryReservationResponse.scala b/api/src/main/scala/com/lbs/api/json/model/TemporaryReservationResponse.scala deleted file mode 100644 index b68c188..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/TemporaryReservationResponse.scala +++ /dev/null @@ -1,6 +0,0 @@ - -package com.lbs.api.json.model - -case class TemporaryReservationResponse(hasReferralRequired: Boolean, id: Long, - informationMessages: List[String], - mustTermOfReservedVisitBeChanged: Boolean) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/TermsIndexResponse.scala b/api/src/main/scala/com/lbs/api/json/model/TermsIndexResponse.scala new file mode 100644 index 0000000..eb34fd1 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/TermsIndexResponse.scala @@ -0,0 +1,134 @@ + +package com.lbs.api.json.model + +import java.time.LocalDateTime + +/** + * + * { + * "correlationId": "00000000-0000-0000-0000-000000000000", + * "pMode": 500, + * "termsForService": { + *"additionalData": { + *"anyTermForFacilityVisit": false, + *"anyTermForTelemedicine": true, + *"isPreparationRequired": false, + *"nextTermsAvailable": false, + *"preparationItems": [], + *"previousTermsAvailable": false + *}, + *"serviceVariantId": 111111, + *"termsForDays": [ + *{ + *"correlationId": "00000000-0000-0000-0000-000000000000", + *"day": "2022-05-31T00:00:00", + *"terms": [ + *{ + *"clinic": "LX Wrocław - Fabryczna 6", + *"clinicGroup": "ul. Fabryczna 6", + *"clinicGroupId": 11, + *"clinicId": 2222, + *"dateTimeFrom": "2021-05-21T18:45:00", + *"dateTimeTo": "2021-05-21T19:00:00", + *"doctor": { + *"academicTitle": "lek. med.", + *"firstName": "TARAS", + *"genderId": 0, + *"id": 33333, + *"lastName": "GRYGORYCH" + *}, + *"impedimentText": "", + *"isAdditional": false, + *"isImpediment": false, + *"isInfectionTreatmentCenter": false, + *"isTelemedicine": true, + *"partOfDay": 3, + *"roomId": 4444, + *"scheduleId": 555555, + *"serviceId": 66666 + *}, + *{ + *"clinic": "LX Wrocław - Fabryczna 6", + *"clinicGroup": "ul. Fabryczna 6", + *"clinicGroupId": 77, + *"clinicId": 88888, + *"dateTimeFrom": "2021-05-21T18:45:00", + *"dateTimeTo": "2021-05-21T19:10:00", + *"doctor": { + *"academicTitle": "lek. med.", + *"firstName": "VASYL", + *"genderId": 0, + *"id": 99999, + *"lastName": "STUS" + *}, + *"impedimentText": "", + *"isAdditional": false, + *"isImpediment": false, + *"isInfectionTreatmentCenter": false, + *"isTelemedicine": true, + *"partOfDay": 3, + *"roomId": 11111, + *"scheduleId": 1222222, + *"serviceId": 133333 + *} + *] + *} + *], + *"termsInfoForDays": [ + *{ + *"day": "2021-05-22T00:00:00", + *"isLastDayWithLoadedTerms": true, + *"isLimitedDay": false, + *"isMoreTermsThanCounter": null, + *"message": "We can propose visits on the searched day but in other locations, at other doctors or another hour range.", + *"termsCounter": { + *"partialTermsCounters": [], + *"termsNumber": 41 + *}, + *"termsStatus": 0 + *}, + *{ + *"day": "2021-05-23T00:00:00", + *"isLastDayWithLoadedTerms": false, + *"isLimitedDay": false, + *"isMoreTermsThanCounter": null, + *"message": "Available visits have been already booked. Check later, additonal visits appear regularly.", + *"termsCounter": { + *"partialTermsCounters": [], + *"termsNumber": 0 + *}, + *"termsStatus": 5 + *}, + *{ + *"day": "2021-05-24T00:00:00", + *"isLastDayWithLoadedTerms": false, + *"isLimitedDay": false, + *"isMoreTermsThanCounter": null, + *"message": "Schedules for that day are not available yet. Check in 1 day.", + *"termsCounter": { + *"partialTermsCounters": [], + *"termsNumber": 0 + *}, + *"termsStatus": 4 + *} + *] + *} +*} + * + */ +case class TermsIndexResponse(correlationId: String, termsForService: TermsForService) extends SerializableJsonObject + +case class TermsForService(additionalData: AdditionalData, termsForDays: List[TermsForDay]) extends SerializableJsonObject + +case class PreparationItem(header: Option[String], text: Option[String]) + +case class AdditionalData(isPreparationRequired: Boolean, preparationItems: List[PreparationItem]) + +case class TermsForDay(day: LocalDateTime, terms: List[Term]) extends SerializableJsonObject + +case class Term(clinic: String, clinicId: Long, dateTimeFrom: LocalDateTime, dateTimeTo: LocalDateTime, doctor: Doctor, + impedimentText: String, isAdditional: Boolean, isImpediment: Boolean, isTelemedicine: Boolean, roomId: Long, + scheduleId: Long, serviceId: Long) extends SerializableJsonObject + +case class TermExt(additionalData: AdditionalData, term: Term) extends SerializableJsonObject + diff --git a/api/src/main/scala/com/lbs/api/json/model/Valuation.scala b/api/src/main/scala/com/lbs/api/json/model/Valuation.scala new file mode 100644 index 0000000..e1e2ac6 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/Valuation.scala @@ -0,0 +1,23 @@ +package com.lbs.api.json.model + +/** + * { + "alternativePrice": null, + "contractId": 555555, + "isExternalReferralAllowed": false, + "isReferralRequired": false, false, + "payerId": 66666, + "price": 0.0, + "productElementId": 7777777, + "productId": 888888, + "productInContractId": 9999999, + "requireReferralForPP": false, + "valuationType": 1 + } + */ + +case class Valuation(alternativePrice: Option[String], contractId: Long, isExternalReferralAllowed: Boolean, + isReferralRequired: Boolean, payerId: Long, price: Double, productElementId: Long, productId: Long, + productInContractId: Long, requireReferralForPP: Boolean, valuationType: Long) { + +} diff --git a/api/src/main/scala/com/lbs/api/json/model/ValuationsRequest.scala b/api/src/main/scala/com/lbs/api/json/model/ValuationsRequest.scala deleted file mode 100644 index 7e0d0c0..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/ValuationsRequest.scala +++ /dev/null @@ -1,43 +0,0 @@ - -package com.lbs.api.json.model - -import java.time.ZonedDateTime - - -/** -{ - "ClinicId": 6, - "DoctorId": 38275, - "PayerDetailsList": [ - { - "BrandId": 2, - "ContractId": 1111111, - "PayerId": 22222, - "PayerName": "FIRMA POLAND SP. Z O.O.", - "ProductElementId": 3333333, - "ProductId": 44444, - "ProductInContractId": 555555, - "ServaAppId": 0, - "ServaId": 6666 - }, - { - "BrandId": 2, - "ContractId": 1111111, - "PayerId": 22222, - "PayerName": "FIRMA POLAND SP. Z O.O.", - "ProductElementId": 8547135, - "ProductId": 44444, - "ProductInContractId": 555555, - "ServaAppId": 1, - "ServaId": 6666 - } - ], - "ReferralRequiredByService": false, - "RoomId": 543, - "ServiceId": 6666, - "StartDateTime": "2018-02-23T11:30:00+02:00" -} - */ -case class ValuationsRequest(clinicId: Long, doctorId: Long, payerDetailsList: List[PayerDetails], - referralRequiredByService: Boolean, roomId: Long, serviceId: Long, - startDateTime: ZonedDateTime) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/ValuationsResponse.scala b/api/src/main/scala/com/lbs/api/json/model/ValuationsResponse.scala deleted file mode 100644 index d654676..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/ValuationsResponse.scala +++ /dev/null @@ -1,41 +0,0 @@ - -package com.lbs.api.json.model - - -/** -{ - "OptionsQuestion": "Would you like to confirm your appointment booking?", - "VisitTermVariants": [ - { - "CanBeReserve": true, - "InfoMessage": "During the appointment, the physician will indicate the services to be provided and will inform you of the relevant fee, if any. The services will be provided in accordance with the scope of the agreement.", - "IsStomatology": true, - "OptionMessage": "I do not have the required referral", - "PaymentMessage": "", - "ReferralRequired": false, - "ValuationDetail": { - "PayerData": { - "BrandId": null, - "ContractId": 1111111, - "PayerId": 22222, - "PayerName": "FIRMA POLAND SP. Z O.O.", - "ProductElementId": 8547100, - "ProductId": 44444, - "ProductInContractId": 555555, - "ServaAppId": 0, - "ServaId": 6621 - }, - "Price": 0.0, - "ValuationType": 1 - }, - "WarningMessage": "" - } - ] -} - */ -case class ValuationsResponse(optionsQuestion: Option[String], visitTermVariants: List[VisitTermVariant]) extends SerializableJsonObject - -case class VisitTermVariant(canBeReserve: Boolean, infoMessage: String, isStomatology: Boolean, optionMessage: String, paymentMessage: String, - referralRequired: Boolean, valuationDetail: ValuationDetail, warningMessage: String) extends SerializableJsonObject - -case class ValuationDetail(payerData: PayerDetails, price: BigDecimal, valuationType: Int) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/VisitsHistoryResponse.scala b/api/src/main/scala/com/lbs/api/json/model/VisitsHistoryResponse.scala deleted file mode 100644 index 52782e7..0000000 --- a/api/src/main/scala/com/lbs/api/json/model/VisitsHistoryResponse.scala +++ /dev/null @@ -1,77 +0,0 @@ - -package com.lbs.api.json.model - -/** -{ - "AreMoreVisits": false, - "HistoricVisits": [ - { - "ClinicName": "LX Wrocław - Szewska 3A", - "DoctorName": "lek. stom. TARAS SHEVCZENKO", - "HasRecommendations": false, - "HasReferrals": false, - "IsAdditional": false, - "Links": [ - { - "Href": "/PatientPortalMobileAPI/api/visits/recommendations/222222222", - "Method": "GET", - "Rel": "get_recommendations" - } - ], - "QuestionToVisit": { - "IsAnswered": false, - "IsAsked": false, - "IsQuestionToVisitAvailable": false - }, - "RateVisit": { - "IsRatingAvailable": false, - "IsVisitRated": false - }, - "ReservationId": 222222222, - "Service": { - "Id": 6621, - "Name": "Umówienie wizyty u stomatologa" - }, - "VisitDate": { - "FormattedDate": "17th Jan 2018, at 1:00 pm", - "StartDateTime": "2018-01-17T13:00:00+02:00" - } - }, - { - "ClinicName": "LX Wrocław - Szewska 3A", - "DoctorName": "lek. stom. TARAS SHEVCZENKO", - "HasRecommendations": false, - "HasReferrals": false, - "IsAdditional": false, - "Links": [ - { - "Href": "/PatientPortalMobileAPI/api/visits/recommendations/999999999", - "Method": "GET", - "Rel": "get_recommendations" - } - ], - "QuestionToVisit": { - "IsAnswered": false, - "IsAsked": false, - "IsQuestionToVisitAvailable": false - }, - "RateVisit": { - "IsRatingAvailable": false, - "IsVisitRated": false - }, - "ReservationId": 999999999, - "Service": { - "Id": 3589, - "Name": "Wypełnienie ubytku korony zęba na 2 powierzchniach" - }, - "VisitDate": { - "FormattedDate": "17th Jan 2018, at 1:00 pm", - "StartDateTime": "2018-01-17T13:00:00+02:00" - } - } - ] -} - */ -case class VisitsHistoryResponse(areMoreVisits: Boolean, historicVisits: List[HistoricVisit]) extends SerializableJsonObject - -case class HistoricVisit(clinicName: String, doctorName: String, reservationId: Long, service: IdName, visitDate: VisitDate) diff --git a/api/src/main/scala/com/lbs/api/json/model/XsrfToken.scala b/api/src/main/scala/com/lbs/api/json/model/XsrfToken.scala new file mode 100644 index 0000000..d912aa5 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/XsrfToken.scala @@ -0,0 +1,5 @@ +package com.lbs.api.json.model + +import java.net.HttpCookie + +case class XsrfToken(token: String, cookies: Seq[HttpCookie]) diff --git a/api/src/main/scala/com/lbs/api/package.scala b/api/src/main/scala/com/lbs/api/package.scala index 2892963..1812967 100644 --- a/api/src/main/scala/com/lbs/api/package.scala +++ b/api/src/main/scala/com/lbs/api/package.scala @@ -2,48 +2,11 @@ package com.lbs import cats.MonadError -import cats.implicits._ -import com.lbs.api.json.model.{AvailableTermsResponse, ReservationFilterResponse, ReservedVisitsResponse, VisitsHistoryResponse} -import com.softwaremill.quicklens._ import scala.language.higherKinds -import scala.util.matching.Regex package object api { type ThrowableMonad[F[_]] = MonadError[F, Throwable] - 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 def cleanupDoctorName(name: String) = DoctorPrefixes.replaceFirstIn(name, "") - - trait ResponseMutator[T] { - def mutate(response: T): T - } - - implicit class ResponseOps[T: ResponseMutator, F[_] : ThrowableMonad](response: F[T]) { - def mutate: F[T] = { - val mutator = implicitly[ResponseMutator[T]] - response.map(mutator.mutate) - } - } - - implicit val ReservedVisitsResponseMutator: ResponseMutator[ReservedVisitsResponse] = (response: ReservedVisitsResponse) => { - response.modify(_.reservedVisits.each.doctorName).using(cleanupDoctorName) - } - - implicit val VisitsHistoryResponseMutator: ResponseMutator[VisitsHistoryResponse] = (response: VisitsHistoryResponse) => { - response.modify(_.historicVisits.each.doctorName).using(cleanupDoctorName) - } - - implicit val ReservationFilterResponseMutator: ResponseMutator[ReservationFilterResponse] = (response: ReservationFilterResponse) => { - response.modify(_.doctors.each.name).using(cleanupDoctorName) - } - - implicit val AvailableTermsResponseMutator: ResponseMutator[AvailableTermsResponse] = (response: AvailableTermsResponse) => { - response.modify(_.availableVisitsTermPresentation.each.doctor.name).using(cleanupDoctorName) - } - } - } diff --git a/api/src/test/scala/com/lbs/api/ApiResponseMutatorsSpec.scala b/api/src/test/scala/com/lbs/api/ApiResponseMutatorsSpec.scala deleted file mode 100644 index a88884e..0000000 --- a/api/src/test/scala/com/lbs/api/ApiResponseMutatorsSpec.scala +++ /dev/null @@ -1,86 +0,0 @@ -package com.lbs.api - -import com.lbs.api.json.model.{AvailableTermsResponse, AvailableVisitsTermPresentation, HistoricVisit, IdName, ReservationFilterResponse, ReservedVisit, ReservedVisitsResponse, VisitsHistoryResponse} -import org.scalatest.{FunSuiteLike, Matchers} - -class ApiResponseMutatorsSpec extends FunSuiteLike with Matchers { - test("ReservationFilterResponseMutator") { - val mutated = - ApiResponseMutators.ReservationFilterResponseMutator.mutate( - ReservationFilterResponse( - cities = Nil, - clinics = Nil, - defaultPayer = None, - doctors = List(IdName(1, "AGNIESZKA dr n. med.")), - languages = Nil, - payers = Nil, - services = Nil - ) - ) - - assert(mutated.doctors === List(IdName(1, "AGNIESZKA"))) - } - - test("ReservedVisitsResponseMutator") { - val mutated = - ApiResponseMutators.ReservedVisitsResponseMutator.mutate( - ReservedVisitsResponse( - List( - ReservedVisit( - canBeCanceled = false, - clinic = null, - doctorName = "AGNIESZKA dr n. med.", - reservationId = 1L, - service = null, - visitDate = null - ) - ) - ) - ) - - assert(mutated.reservedVisits.head.doctorName === "AGNIESZKA") - } - - test("VisitsHistoryResponseMutator") { - val mutated = - ApiResponseMutators.VisitsHistoryResponseMutator.mutate( - VisitsHistoryResponse( - areMoreVisits = false, - historicVisits = List( - HistoricVisit( - clinicName = null, - doctorName = "AGNIESZKA dr n. med.", - reservationId = 1L, - service = null, - visitDate = null - ) - ) - ) - ) - - assert(mutated.historicVisits.head.doctorName === "AGNIESZKA") - } - - test("AvailableTermsResponseMutator") { - val mutated = - ApiResponseMutators.AvailableTermsResponseMutator.mutate( - AvailableTermsResponse( - availableVisitsTermPresentation = List( - AvailableVisitsTermPresentation( - clinic = null, - doctor = IdName(1, "AGNIESZKA dr n. med."), - payerDetailsList = Nil, - referralRequiredByProduct = false, - referralRequiredByService = false, - roomId = 1L, - scheduleId = 1L, - serviceId = 1L, - visitDate = null - ) - ) - ) - ) - - assert(mutated.availableVisitsTermPresentation.head.doctor === IdName(1, "AGNIESZKA")) - } -} diff --git a/api/src/test/scala/com/lbs/api/json/model/CommonSpec.scala b/api/src/test/scala/com/lbs/api/json/model/CommonSpec.scala deleted file mode 100644 index 3cde644..0000000 --- a/api/src/test/scala/com/lbs/api/json/model/CommonSpec.scala +++ /dev/null @@ -1,21 +0,0 @@ - -package com.lbs.api.json.model - -import org.scalatest.Matchers - -trait CommonSpec { - _: Matchers => - - private type SimpleEntity = {val id: Long; val name: String} - - protected def testSimpleEntity(simpleEntity: SimpleEntity, expectedId: Long, expectedName: String): Unit = { - simpleEntity.id should be(expectedId) - simpleEntity.name should be(expectedName) - } - - protected def testSimpleEntities(simpleEntities: List[SimpleEntity], expectedSize: Int, expectedId: Long, expectedName: String): Unit = { - simpleEntities.size should be(expectedSize) - val simpleEntity = simpleEntities.head - testSimpleEntity(simpleEntity, expectedId, expectedName) - } -} diff --git a/api/src/test/scala/com/lbs/api/json/model/LoginResponseSpec.scala b/api/src/test/scala/com/lbs/api/json/model/LoginResponseSpec.scala deleted file mode 100644 index 4648492..0000000 --- a/api/src/test/scala/com/lbs/api/json/model/LoginResponseSpec.scala +++ /dev/null @@ -1,26 +0,0 @@ - -package com.lbs.api.json.model - -import com.lbs.api.json.JsonSerializer.extensions._ -import org.scalatest.{FunSuiteLike, Matchers} - -class LoginResponseSpec extends FunSuiteLike with Matchers { - test("deserialization") { - val json = - """ - |{ - | "access_token": "RmC6qccJMJ1uVhqJZ-6sBYdfT_LznEoGuH2di0", - | "expires_in": 599, - | "refresh_token": "7854cd0b-8545-483e-88d7-d07eda90995d", - | "token_type": "bearer" - |} - """.stripMargin - - val response = json.as[LoginResponse] - - response.accessToken should be("RmC6qccJMJ1uVhqJZ-6sBYdfT_LznEoGuH2di0") - response.expiresIn should be(599) - response.refreshToken should be("7854cd0b-8545-483e-88d7-d07eda90995d") - response.tokenType should be("bearer") - } -} diff --git a/api/src/test/scala/com/lbs/api/json/model/LuxmedErrorsListSpec.scala b/api/src/test/scala/com/lbs/api/json/model/LuxmedErrorsListSpec.scala deleted file mode 100644 index 7451eab..0000000 --- a/api/src/test/scala/com/lbs/api/json/model/LuxmedErrorsListSpec.scala +++ /dev/null @@ -1,18 +0,0 @@ -package com.lbs.api.json.model - -import com.lbs.api.json.JsonSerializer.extensions._ -import org.scalatest.{FunSuiteLike, Matchers} - -class LuxmedErrorsListSpec extends FunSuiteLike with Matchers with CommonSpec { - - test("deserialization") { - val json = - """ - |{"Errors":[{"ErrorCode":16000006,"Message":"You have already booked this service","AdditionalData":{}}]} - """.stripMargin - - val response = json.as[LuxmedErrorsList] - - response should be(LuxmedErrorsList(List(LuxmedErrorsListElement(16000006, "You have already booked this service", Map.empty)))) - } -} diff --git a/api/src/test/scala/com/lbs/api/json/model/LuxmedErrorsMapSpec.scala b/api/src/test/scala/com/lbs/api/json/model/LuxmedErrorsMapSpec.scala deleted file mode 100644 index 0e64323..0000000 --- a/api/src/test/scala/com/lbs/api/json/model/LuxmedErrorsMapSpec.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.lbs.api.json.model - -import com.lbs.api.json.JsonSerializer.extensions._ -import org.scalatest.{FunSuiteLike, Matchers} - -class LuxmedErrorsMapSpec extends FunSuiteLike with Matchers with CommonSpec { - test("deserialization") { - val json = - """ - |{"Errors":{"ToDate.Date":["'To Date. Date' must be greater than or equal to '06/04/2018 00:00:00'."]}} - """.stripMargin - - val response = json.as[LuxmedErrorsMap] - - response should be(LuxmedErrorsMap(Map("toDate.Date" -> List("'To Date. Date' must be greater than or equal to '06/04/2018 00:00:00'.")))) - } -} diff --git a/api/src/test/scala/com/lbs/api/json/model/ReservationFilterResponseSpec.scala b/api/src/test/scala/com/lbs/api/json/model/ReservationFilterResponseSpec.scala deleted file mode 100644 index 82891d9..0000000 --- a/api/src/test/scala/com/lbs/api/json/model/ReservationFilterResponseSpec.scala +++ /dev/null @@ -1,84 +0,0 @@ - -package com.lbs.api.json.model - -import com.lbs.api.json.JsonSerializer.extensions._ -import org.scalatest.{FunSuiteLike, Matchers} - -class ReservationFilterResponseSpec extends FunSuiteLike with Matchers with CommonSpec { - test("deserialization") { - val json = - """ - |{ - | "Cities": [ - | { - | "Id": 5, - | "Name": "Wrocław" - | } - | ], - | "Clinics": [ - | { - | "Id": 1405, - | "Name": "Legnicka 40" - | }, - | { - | "Id": 7, - | "Name": "Kwidzyńska 6" - | } - | ], - | "DefaultPayer": { - | "Id": 22222, - | "Name": "FIRMA" - | }, - | "Doctors": [ - | { - | "Id": 38275, - | "Name": "ANNA ABRAMCZYK" - | }, - | { - | "Id": 15565, - | "Name": "ANDRZEJ ANDEWSKI" - | } - | ], - | "Languages": [ - | { - | "Id": 11, - | "Name": "english" - | }, - | { - | "Id": 10, - | "Name": "polish" - | } - | ], - | "Payers": [ - | { - | "Id": 22222, - | "Name": "FIRMA" - | } - | ], - | "Services": [ - | { - | "Id": 5857, - | "Name": "Audiometr standardowy" - | }, - | { - | "Id": 7976, - | "Name": "Audiometr standardowy - audiometria nadprogowa" - | } - | ] - |} - """.stripMargin - - val response = json.as[ReservationFilterResponse] - - testSimpleEntities(response.cities, 1, 5L, "Wrocław") - testSimpleEntities(response.clinics, 2, 1405L, "Legnicka 40") - response.defaultPayer should be(_: Some[IdName]) - testSimpleEntity(response.defaultPayer.get, 22222L, "FIRMA") - testSimpleEntities(response.doctors, 2, 38275L, "ANNA ABRAMCZYK") - testSimpleEntities(response.languages, 2, 11L, "english") - testSimpleEntities(response.payers, 1, 22222L, "FIRMA") - testSimpleEntities(response.services, 2, 5857L, "Audiometr standardowy") - } - - -} diff --git a/api/src/test/scala/com/lbs/api/json/model/ReservedVisitsResponseSpec.scala b/api/src/test/scala/com/lbs/api/json/model/ReservedVisitsResponseSpec.scala deleted file mode 100644 index af0bc5a..0000000 --- a/api/src/test/scala/com/lbs/api/json/model/ReservedVisitsResponseSpec.scala +++ /dev/null @@ -1,62 +0,0 @@ - -package com.lbs.api.json.model - -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - -import com.lbs.api.json.JsonSerializer.extensions._ -import org.scalatest.{FunSuiteLike, Matchers} - -class ReservedVisitsResponseSpec extends FunSuiteLike with Matchers with CommonSpec { - test("deserialization") { - val json = - """ - |{ - | "ReservedVisits": [ - | { - | "CanBeCanceled": true, - | "Clinic": { - | "Id": 6, - | "Name": "Szewska 3A" - | }, - | "DoctorName": "TARAS SHEVCZENKO", - | "Impediment": { - | "ImpedimentText": "", - | "IsImpediment": false - | }, - | "IsAdditional": false, - | "IsPreparationRequired": false, - | "Links": [ - | { - | "Href": "/PatientPortalMobileAPI/api/visits/preparations/6621", - | "Method": "GET", - | "Rel": "get_preparations" - | } - | ], - | "ReservationId": 888888888, - | "Service": { - | "Id": 6621, - | "Name": "stomatolog" - | }, - | "VisitDate": { - | "FormattedDate": "21rd May, Mon. at 3:00 pm", - | "StartDateTime": "2018-05-21T15:00:00+02:00" - | } - | } - | ] - |} - """.stripMargin - - val response = json.as[ReservedVisitsResponse] - - response.reservedVisits.size should be(1) - val reservedVisit = response.reservedVisits.head - reservedVisit.canBeCanceled should be(true) - testSimpleEntity(reservedVisit.clinic, 6L, "Szewska 3A") - reservedVisit.doctorName should be("TARAS SHEVCZENKO") - reservedVisit.reservationId should be(888888888L) - testSimpleEntity(reservedVisit.service, 6621L, "stomatolog") - reservedVisit.visitDate.formattedDate should be("21rd May, Mon. at 3:00 pm") - reservedVisit.visitDate.startDateTime should be(ZonedDateTime.parse("2018-05-21T15:00:00+02:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME)) - } -} diff --git a/api/src/test/scala/com/lbs/api/json/model/TemporaryReservationRequestSpec.scala b/api/src/test/scala/com/lbs/api/json/model/TemporaryReservationRequestSpec.scala deleted file mode 100644 index da6aed1..0000000 --- a/api/src/test/scala/com/lbs/api/json/model/TemporaryReservationRequestSpec.scala +++ /dev/null @@ -1,49 +0,0 @@ - -package com.lbs.api.json.model - -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - -import com.lbs.api.json.JsonSerializer.extensions._ -import org.scalatest.{FunSuiteLike, Matchers} - -class TemporaryReservationRequestSpec extends FunSuiteLike with Matchers with CommonSpec { - test("serialization") { - val json = - """ - |{ - | "ClinicId": 6, - | "DoctorId": 38275, - | "PayerDetailsList": [ - | { - | "BrandId": 2, - | "ContractId": 1111111, - | "PayerId": 22222, - | "PayerName": "FIRMA", - | "ProductElementId": 3333333, - | "ProductId": 44444, - | "ProductInContractId": 555555, - | "ServaAppId": 0, - | "ServaId": 6666 - | } - | ], - | "ReferralRequiredByService": false, - | "RoomId": 543, - | "ServiceId": 6666, - | "StartDateTime": "2018-02-23T11:30:00+02:00" - |} - """.stripMargin - - val request = TemporaryReservationRequest(clinicId = 6L, doctorId = 38275L, payerDetailsList = List( - PayerDetails(brandId = Some(2L), contractId = 1111111L, payerId = 22222L, payerName = "FIRMA", - productElementId = Some(3333333L), productId = 44444L, productInContractId = 555555L, servaAppId = 0L, servaId = 6666L) - ), referralRequiredByService = false, roomId = 543L, serviceId = 6666L, - startDateTime = ZonedDateTime.parse("2018-02-23T11:30:00+02:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME)) - - val requestJson = request.asJson - val requestActual = requestJson.as[TemporaryReservationRequest] - val requestExpected = json.as[TemporaryReservationRequest] - - requestActual should be(requestExpected) - } -} diff --git a/api/src/test/scala/com/lbs/api/json/model/ValuationsRequestSpec.scala b/api/src/test/scala/com/lbs/api/json/model/ValuationsRequestSpec.scala deleted file mode 100644 index 0ef4c23..0000000 --- a/api/src/test/scala/com/lbs/api/json/model/ValuationsRequestSpec.scala +++ /dev/null @@ -1,49 +0,0 @@ - -package com.lbs.api.json.model - -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - -import com.lbs.api.json.JsonSerializer.extensions._ -import org.scalatest.{FunSuiteLike, Matchers} - -class ValuationsRequestSpec extends FunSuiteLike with Matchers with CommonSpec { - test("serialization") { - val json = - """ - |{ - | "ClinicId": 6, - | "DoctorId": 38275, - | "PayerDetailsList": [ - | { - | "BrandId": 2, - | "ContractId": 1111111, - | "PayerId": 22222, - | "PayerName": "FIRMA", - | "ProductElementId": 3333333, - | "ProductId": 44444, - | "ProductInContractId": 555555, - | "ServaAppId": 0, - | "ServaId": 6666 - | } - | ], - | "ReferralRequiredByService": false, - | "RoomId": 543, - | "ServiceId": 6666, - | "StartDateTime": "2018-02-23T11:30:00+02:00" - |} - """.stripMargin - - val request = ValuationsRequest(clinicId = 6L, doctorId = 38275L, payerDetailsList = List( - PayerDetails(brandId = Some(2L), contractId = 1111111L, payerId = 22222L, payerName = "FIRMA", - productElementId = Some(3333333L), productId = 44444L, productInContractId = 555555L, servaAppId = 0L, servaId = 6666L) - ), referralRequiredByService = false, roomId = 543L, serviceId = 6666L, - startDateTime = ZonedDateTime.parse("2018-02-23T11:30:00+02:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME)) - - val requestJson = request.asJson - val requestActual = requestJson.as[ValuationsRequest] - val requestExpected = json.as[ValuationsRequest] - - requestActual should be(requestExpected) - } -} diff --git a/api/src/test/scala/com/lbs/api/json/model/ValuationsResponseSpec.scala b/api/src/test/scala/com/lbs/api/json/model/ValuationsResponseSpec.scala deleted file mode 100644 index f85df49..0000000 --- a/api/src/test/scala/com/lbs/api/json/model/ValuationsResponseSpec.scala +++ /dev/null @@ -1,94 +0,0 @@ -package com.lbs.api.json.model - -import com.lbs.api.json.JsonSerializer.extensions._ -import org.scalatest.{FunSuiteLike, Matchers} - -class ValuationsResponseSpec extends FunSuiteLike with Matchers with CommonSpec { - test("deserialization") { - val json = - """ - |{ - | "VisitTermVariants": [ - | { - | "ValuationDetail": { - | "PayerData": { - | "PayerId": 12345, - | "PayerName": "ZBIGNEW", - | "ContractId": 123456, - | "ProductInContractId": 234567, - | "ProductId": 34567, - | "BrandId": null, - | "ProductElementId": 2536352, - | "ServaId": 12345, - | "ServaAppId": 0 - | }, - | "ValuationType": 1, - | "Price": 0.000000 - | }, - | "InfoMessage": "Package-covered service", - | "PaymentMessage": "Price with a valid referral: 0.00 zł \r\nPrice without a valid referral: 10.30 zł", - | "OptionMessage": "Yes, from a LUX MED Group or a subcontractor facility physician", - | "WarningMessage": "In accordance with your medical package terms and conditions, you must have a valid referral from a LUX MED Group or a subcontractor facility physician", - | "CanBeReserve": true, - | "ReferralRequired": true, - | "IsStomatology": false - | }, - | { - | "ValuationDetail": { - | "PayerData": { - | "PayerId": 123456, - | "PayerName": "ZBIGNEW", - | "ContractId": 12345, - | "ProductInContractId": 345678, - | "ProductId": 23467, - | "BrandId": null, - | "ProductElementId": null, - | "ServaId": 1234, - | "ServaAppId": 0 - | }, - | "ValuationType": 4, - | "Price": 10.30 - | }, - | "InfoMessage": "Out-of-package service", - | "PaymentMessage": "Price: 10.30 zł", - | "OptionMessage": "I do not have the required referral", - | "WarningMessage": "", - | "CanBeReserve": true, - | "ReferralRequired": false, - | "IsStomatology": false - | } - | ], - | "OptionsQuestion": "Do you have a valid referral for this service?", - | "IsReferralRequired": false - |} - """.stripMargin - - val responseActual = json.as[ValuationsResponse] - - val responseExpected = ValuationsResponse( - Some("Do you have a valid referral for this service?"), - List( - VisitTermVariant( - canBeReserve = true, - "Package-covered service", - isStomatology = false, - "Yes, from a LUX MED Group or a subcontractor facility physician", "Price with a valid referral: 0.00 zł \r\nPrice without a valid referral: 10.30 zł", - referralRequired = true, - ValuationDetail(PayerDetails(None, 123456, 12345, "ZBIGNEW", Some(2536352), 34567, 234567, 0, 12345), 0.0, 1), - "In accordance with your medical package terms and conditions, you must have a valid referral from a LUX MED Group or a subcontractor facility physician" - ), - VisitTermVariant( - canBeReserve = true, - "Out-of-package service", - isStomatology = false, - "I do not have the required referral", - "Price: 10.30 zł", - referralRequired = false, - ValuationDetail(PayerDetails(None, 12345, 123456, "ZBIGNEW", None, 23467, 345678, 0, 1234), 10.3, 4), - "") - ) - ) - - responseActual should be(responseExpected) - } -} diff --git a/api/src/test/scala/com/lbs/api/json/model/VisitsHistoryResponseSpec.scala b/api/src/test/scala/com/lbs/api/json/model/VisitsHistoryResponseSpec.scala deleted file mode 100644 index 570f041..0000000 --- a/api/src/test/scala/com/lbs/api/json/model/VisitsHistoryResponseSpec.scala +++ /dev/null @@ -1,97 +0,0 @@ - -package com.lbs.api.json.model - -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - -import com.lbs.api.json.JsonSerializer.extensions._ -import org.scalatest.{FunSuiteLike, Matchers} - -class VisitsHistoryResponseSpec extends FunSuiteLike with Matchers with CommonSpec { - test("deserialization") { - val json = - """ - |{ - | "AreMoreVisits": false, - | "HistoricVisits": [ - | { - | "ClinicName": "Szewska 3A", - | "DoctorName": "TARAS SHEVCZENKO", - | "HasRecommendations": false, - | "HasReferrals": false, - | "IsAdditional": false, - | "Links": [ - | { - | "Href": "/PatientPortalMobileAPI/api/visits/recommendations/222222222", - | "Method": "GET", - | "Rel": "get_recommendations" - | } - | ], - | "QuestionToVisit": { - | "IsAnswered": false, - | "IsAsked": false, - | "IsQuestionToVisitAvailable": false - | }, - | "RateVisit": { - | "IsRatingAvailable": false, - | "IsVisitRated": false - | }, - | "ReservationId": 222222222, - | "Service": { - | "Id": 6621, - | "Name": "stomatolog" - | }, - | "VisitDate": { - | "FormattedDate": "17th Jan 2018, at 1:00 pm", - | "StartDateTime": "2018-01-17T13:00:00+02:00" - | } - | }, - | { - | "ClinicName": "LX Wrocław - Szewska 3A", - | "DoctorName": "lek. stom. TARAS SHEVCZENKO", - | "HasRecommendations": false, - | "HasReferrals": false, - | "IsAdditional": false, - | "Links": [ - | { - | "Href": "/PatientPortalMobileAPI/api/visits/recommendations/999999999", - | "Method": "GET", - | "Rel": "get_recommendations" - | } - | ], - | "QuestionToVisit": { - | "IsAnswered": false, - | "IsAsked": false, - | "IsQuestionToVisitAvailable": false - | }, - | "RateVisit": { - | "IsRatingAvailable": false, - | "IsVisitRated": false - | }, - | "ReservationId": 999999999, - | "Service": { - | "Id": 3589, - | "Name": "Wypełnienie ubytku korony zęba na 2 powierzchniach" - | }, - | "VisitDate": { - | "FormattedDate": "17th Jan 2018, at 1:00 pm", - | "StartDateTime": "2018-01-17T13:00:00+02:00" - | } - | } - | ] - |} - """.stripMargin - - val response = json.as[VisitsHistoryResponse] - - response.areMoreVisits should be(false) - response.historicVisits.size should be(2) - val historicVisit = response.historicVisits.head - historicVisit.clinicName should be("Szewska 3A") - historicVisit.doctorName should be("TARAS SHEVCZENKO") - historicVisit.reservationId should be(222222222L) - testSimpleEntity(historicVisit.service, 6621L, "stomatolog") - historicVisit.visitDate.formattedDate should be("17th Jan 2018, at 1:00 pm") - historicVisit.visitDate.startDateTime should be(ZonedDateTime.parse("2018-01-17T13:00:00+02:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME)) - } -} diff --git a/bot/build.gradle b/bot/build.gradle index eabfee6..e31c7e4 100644 --- a/bot/build.gradle +++ b/bot/build.gradle @@ -1,6 +1,6 @@ dependencies { compile project(':common') - compile group: "com.bot4s", name: "telegram-core_2.12", version: "5.1.0" - compile group: "com.bot4s", name: "telegram-akka_2.12", version: "4.0.0-RC2" + compile group: "com.bot4s", name: "telegram-core_2.12", version: "5.6.0" + compile group: "com.bot4s", name: "telegram-akka_2.12", version: "5.6.0" } \ No newline at end of file diff --git a/bot/src/main/scala/com/lbs/bot/telegram/TelegramClient.scala b/bot/src/main/scala/com/lbs/bot/telegram/TelegramClient.scala index a7e8bef..e50a5d7 100644 --- a/bot/src/main/scala/com/lbs/bot/telegram/TelegramClient.scala +++ b/bot/src/main/scala/com/lbs/bot/telegram/TelegramClient.scala @@ -2,22 +2,19 @@ package com.lbs.bot.telegram import cats.implicits.toFunctorOps -import com.bot4s.telegram.api.RequestHandler import com.bot4s.telegram.api.declarative.{Callbacks, Commands} -import com.bot4s.telegram.clients.FutureSttpClient +import com.bot4s.telegram.api.{AkkaTelegramBot, RequestHandler} +import com.bot4s.telegram.clients.AkkaHttpClient import com.bot4s.telegram.future.{Polling, TelegramBot => TelegramBoT} import com.bot4s.telegram.methods._ import com.bot4s.telegram.models.{InlineKeyboardMarkup, InputFile, Message} import com.lbs.common.Logger -import sttp.client3.SttpBackend -import sttp.client3.okhttp.OkHttpFutureBackend import scala.concurrent.Future -class TelegramClient(onReceive: TelegramEvent => Unit, botToken: String) extends TelegramBoT with Polling with Commands[Future] with Callbacks[Future] with Logger { +class TelegramClient(onReceive: TelegramEvent => Unit, botToken: String) extends AkkaTelegramBot with TelegramBoT with Polling with Commands[Future] with Callbacks[Future] with Logger { - private implicit val backend: SttpBackend[Future, Any] = OkHttpFutureBackend() - override val client: RequestHandler[Future] = new FutureSttpClient(botToken) + override val client: RequestHandler[Future] = new AkkaHttpClient(botToken) def sendMessage(chatId: Long, text: String): Future[Message] = loggingRequest(SendMessage(chatId, text, parseMode = Some(ParseMode.HTML))) @@ -32,7 +29,7 @@ class TelegramClient(onReceive: TelegramEvent => Unit, botToken: String) extends loggingRequest(EditMessageText(Some(chatId), Some(messageId), text = text, parseMode = Some(ParseMode.HTML), replyMarkup = replyMarkup)) def sendFile(chatId: Long, filename: String, contents: Array[Byte], caption: Option[String] = None): Future[Message] = - loggingRequest(SendDocument(chatId, InputFile(filename, contents), caption)) + loggingRequest(SendDocument(chatId, InputFile(filename, contents), caption = caption)) private def loggingRequest[R: Manifest](req: Request[R]): Future[R] = { debug(s"Sending telegram request: $req") diff --git a/common/src/main/scala/com/lbs/common/Implicits.scala b/common/src/main/scala/com/lbs/common/Implicits.scala index 49294e6..ff6abb8 100644 --- a/common/src/main/scala/com/lbs/common/Implicits.scala +++ b/common/src/main/scala/com/lbs/common/Implicits.scala @@ -2,7 +2,6 @@ package com.lbs.common import java.util.Optional - import scala.language.implicitConversions object Implicits { diff --git a/common/src/main/scala/com/lbs/common/Scheduler.scala b/common/src/main/scala/com/lbs/common/Scheduler.scala index 25f71d9..9f3a409 100644 --- a/common/src/main/scala/com/lbs/common/Scheduler.scala +++ b/common/src/main/scala/com/lbs/common/Scheduler.scala @@ -2,7 +2,6 @@ package com.lbs.common import java.util.concurrent.{Executors, ScheduledFuture} - import scala.concurrent.duration.FiniteDuration class Scheduler(poolSize: Int) extends Logger { diff --git a/server/src/main/scala/com/lbs/server/BootConfig.scala b/server/src/main/scala/com/lbs/server/BootConfig.scala index e02d9f3..f4df7a7 100644 --- a/server/src/main/scala/com/lbs/server/BootConfig.scala +++ b/server/src/main/scala/com/lbs/server/BootConfig.scala @@ -2,7 +2,7 @@ package com.lbs.server import akka.actor.ActorSystem -import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit} +import com.lbs.api.json.model.{Event, TermExt} import com.lbs.bot.Bot import com.lbs.bot.telegram.TelegramBot import com.lbs.server.conversation._ @@ -74,12 +74,12 @@ class BootConfig { userId => new MonitoringsHistory(userId, bot, monitoringService, localization, monitoringsHistoryPagerFactory, bookWithTemplateFactory)(actorSystem) @Bean - def historyFactory: UserIdTo[History] = - userId => new History(userId, bot, apiService, localization, historyPagerFactory)(actorSystem) + def historyFactory: UserIdTo[HistoryViewer] = + userId => new HistoryViewer(userId, bot, apiService, localization, historyPagerFactory)(actorSystem) @Bean - def visitsFactory: UserIdTo[Visits] = - userId => new Visits(userId, bot, apiService, localization, visitsPagerFactory)(actorSystem) + def reservedVisitsFactory: UserIdTo[ReservedVisitsViewer] = + userId => new ReservedVisitsViewer(userId, bot, apiService, localization, reservedVisitsPagerFactory)(actorSystem) @Bean def settingsFactory: UserIdTo[Settings] = @@ -92,7 +92,7 @@ class BootConfig { @Bean def chatFactory: UserIdTo[Chat] = userId => new Chat(userId, dataService, monitoringService, bookFactory, helpFactory, - monitoringsFactory, monitoringsHistoryFactory, historyFactory, visitsFactory, settingsFactory, accountFactory)(actorSystem) + monitoringsFactory, monitoringsHistoryFactory, historyFactory, reservedVisitsFactory, settingsFactory, accountFactory)(actorSystem) @Bean def datePickerFactory: UserIdWithOriginatorTo[DatePicker] = (userId, originator) => @@ -107,24 +107,24 @@ class BootConfig { new StaticData(userId, bot, localization, originator)(actorSystem) @Bean - def termsPagerFactory: UserIdWithOriginatorTo[Pager[AvailableVisitsTermPresentation]] = (userId, originator) => - new Pager[AvailableVisitsTermPresentation](userId, bot, - (term: AvailableVisitsTermPresentation, page: Int, index: Int) => lang(userId).termEntry(term, page, index), + def termsPagerFactory: UserIdWithOriginatorTo[Pager[TermExt]] = (userId, originator) => + new Pager[TermExt](userId, bot, + (term: TermExt, page: Int, index: Int) => lang(userId).termEntry(term, page, index), (page: Int, pages: Int) => lang(userId).termsHeader(page, pages), Some("book"), localization, originator)(actorSystem) @Bean - def visitsPagerFactory: UserIdWithOriginatorTo[Pager[ReservedVisit]] = (userId, originator) => - new Pager[ReservedVisit](userId, bot, - (visit: ReservedVisit, page: Int, index: Int) => lang(userId).upcomingVisitEntry(visit, page, index), - (page: Int, pages: Int) => lang(userId).upcomingVisitsHeader(page, pages), + def reservedVisitsPagerFactory: UserIdWithOriginatorTo[Pager[Event]] = (userId, originator) => + new Pager[Event](userId, bot, + (visit: Event, page: Int, index: Int) => lang(userId).reservedVisitEntry(visit, page, index), + (page: Int, pages: Int) => lang(userId).reservedVisitsHeader(page, pages), Some("cancel"), localization, originator)(actorSystem) @Bean - def historyPagerFactory: UserIdWithOriginatorTo[Pager[HistoricVisit]] = (userId, originator) => - new Pager[HistoricVisit](userId, bot, - (visit: HistoricVisit, page: Int, index: Int) => lang(userId).historyEntry(visit, page, index), + def historyPagerFactory: UserIdWithOriginatorTo[Pager[Event]] = (userId, originator) => + new Pager[Event](userId, bot, + (event: Event, page: Int, index: Int) => lang(userId).historyEntry(event, page, index), (page: Int, pages: Int) => lang(userId).historyHeader(page, pages), None, localization, originator)(actorSystem) 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 47b7907..f51cd4e 100644 --- a/server/src/main/scala/com/lbs/server/conversation/Book.scala +++ b/server/src/main/scala/com/lbs/server/conversation/Book.scala @@ -1,8 +1,6 @@ package com.lbs.server.conversation -import java.time.{LocalTime, ZonedDateTime} - import akka.actor.ActorSystem import com.lbs.api.json.model._ import com.lbs.bot._ @@ -20,9 +18,11 @@ import com.lbs.server.service.{ApiService, DataService, MonitoringService} import com.lbs.server.util.MessageExtractors._ import com.lbs.server.util.ServerModelConverters._ +import java.time.{LocalDateTime, LocalTime} + class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: DataService, monitoringService: MonitoringService, val localization: Localization, datePickerFactory: UserIdWithOriginatorTo[DatePicker], timePickerFactory: UserIdWithOriginatorTo[TimePicker], - staticDataFactory: UserIdWithOriginatorTo[StaticData], termsPagerFactory: UserIdWithOriginatorTo[Pager[AvailableVisitsTermPresentation]])(implicit val actorSystem: ActorSystem) extends Conversation[BookingData] with StaticDataForBooking with Localizable { + staticDataFactory: UserIdWithOriginatorTo[StaticData], termsPagerFactory: UserIdWithOriginatorTo[Pager[TermExt]])(implicit val actorSystem: ActorSystem) extends Conversation[BookingData] with StaticDataForBooking with Localizable { private val datePicker = datePickerFactory(userId, self) private val timePicker = timePickerFactory(userId, self) @@ -33,66 +33,37 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da private def askCity: Step = staticData(cityConfig) { bd: BookingData => - withFunctions( + withFunctions[DictionaryCity]( latestOptions = dataService.getLatestCities(userId.accountId), staticOptions = apiService.getAllCities(userId.accountId), - applyId = id => bd.copy(cityId = id)) - }(requestNext = askClinic) - - private def askClinic: Step = - staticData(clinicConfig) { bd: BookingData => - withFunctions( - latestOptions = dataService.getLatestClinicsByCityId(userId.accountId, bd.cityId.id), - staticOptions = apiService.getAllClinics(userId.accountId, bd.cityId.id), - applyId = id => bd.copy(clinicId = id)) + applyId = id => bd.copy(cityId = id.toIdName)) }(requestNext = askService) private def askService: Step = staticData(serviceConfig) { bd: BookingData => - withFunctions( - latestOptions = dataService.getLatestServicesByCityIdAndClinicId(userId.accountId, bd.cityId.id, bd.clinicId.optionalId), - staticOptions = apiService.getAllServices(userId.accountId, bd.cityId.id, bd.clinicId.optionalId), - applyId = id => bd.copy(serviceId = id)) + withFunctions[DictionaryServiceVariants]( + latestOptions = dataService.getLatestServicesByCityIdAndClinicId(userId.accountId, bd.cityId.id, None), + staticOptions = apiService.getAllServices(userId.accountId), + applyId = id => bd.copy(serviceId = id.toIdName)) + }(requestNext = askClinic) + + private def askClinic: Step = + staticData(clinicConfig) { bd: BookingData => + withFunctions[IdName]( + latestOptions = dataService.getLatestClinicsByCityId(userId.accountId, bd.cityId.id), + staticOptions = apiService.getAllFacilities(userId.accountId, bd.cityId.id, bd.serviceId.id), + applyId = id => bd.copy(clinicId = id)) }(requestNext = askDoctor) private def askDoctor: Step = staticData(doctorConfig) { bd: BookingData => - withFunctions( + withFunctions[IdName]( latestOptions = dataService.getLatestDoctorsByCityIdAndClinicIdAndServiceId(userId.accountId, bd.cityId.id, bd.clinicId.optionalId, bd.serviceId.id), - staticOptions = apiService.getAllDoctors(userId.accountId, bd.cityId.id, bd.clinicId.optionalId, bd.serviceId.id), - applyId = id => bd.copy(doctorId = id)) - }(requestNext = determinePayer) - - private def determinePayer: Step = - process { bookingData => - val response = apiService.getPayers(userId.accountId, bookingData.cityId.id, bookingData.clinicId.optionalId, bookingData.serviceId.id) - response match { - case Left(ex) => - warn(s"Can't determine payers for account ${userId.accountId}, city ${bookingData.cityId.id}, " + - s"clinic ${bookingData.clinicId.optionalId} and service ${bookingData.serviceId.id}", ex) - bot.sendMessage(userId.source, lang.canNotDetectPayer(ex.getMessage)) - end() - case Right((defaultPayerMaybe, payers)) => - defaultPayerMaybe match { - case Some(defaultPayer) => - goto(requestDateFrom) using bookingData.copy(payerId = defaultPayer.id) - case None => - goto(askPayer) using bookingData.copy(payers = payers) - } - } - } - - private def askPayer: Step = - ask { bookingData => - bot.sendMessage( - userId.source, - lang.pleaseChoosePayer, - inlineKeyboard = createInlineKeyboard(bookingData.payers.map(payer => Button(payer.name, payer.id))) - ) - } onReply { - case Msg(CallbackCommand(LongString(payerId)), bookingData) => - goto(requestDateFrom) using bookingData.copy(payerId = payerId) - } + staticOptions = apiService.getAllDoctors(userId.accountId, bd.cityId.id, bd.serviceId.id) + .map(_.filterNot(_.facilityGroupIds.exists(z => bd.clinicId == null || z.contains(bd.clinicId.id))) + .map(_.toIdName)), + applyId = id => bd.copy(doctorId = id.toIdName)) + }(requestNext = requestDateFrom) private def requestDateFrom: Step = ask { bookingData => @@ -103,7 +74,7 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da case Msg(cmd: Command, _) => datePicker ! cmd stay() - case Msg(date: ZonedDateTime, bookingData: BookingData) => + case Msg(date: LocalDateTime, bookingData: BookingData) => goto(requestDateTo) using bookingData.copy(dateFrom = date) } @@ -116,7 +87,7 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da case Msg(cmd: Command, _) => datePicker ! cmd stay() - case Msg(date: ZonedDateTime, bookingData: BookingData) => + case Msg(date: LocalDateTime, bookingData: BookingData) => goto(requestTimeFrom) using bookingData.copy(dateTo = date) } @@ -158,33 +129,42 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da case Msg(CallbackCommand(Tags.FindTerms), _) => goto(requestTerm) case Msg(CallbackCommand(Tags.ModifyDate), bookingData) => - goto(requestDateFrom) using bookingData.copy(dateFrom = ZonedDateTime.now(), - dateTo = ZonedDateTime.now().plusDays(1L)) + goto(requestDateFrom) using bookingData.copy(dateFrom = LocalDateTime.now(), + dateTo = LocalDateTime.now().plusDays(1L)) } private def requestTerm: Step = ask { bookingData => - val availableTerms = apiService.getAvailableTerms(userId.accountId, bookingData.payerId, bookingData.cityId.id, + val availableTerms = apiService.getAvailableTerms(userId.accountId, bookingData.cityId.id, bookingData.clinicId.optionalId, bookingData.serviceId.id, bookingData.doctorId.optionalId, - bookingData.dateFrom, Some(bookingData.dateTo), timeFrom = bookingData.timeFrom, timeTo = bookingData.timeTo) + bookingData.dateFrom, bookingData.dateTo, timeFrom = bookingData.timeFrom, timeTo = bookingData.timeTo) termsPager.restart() termsPager ! availableTerms.map(new SimpleItemsProvider(_)) } onReply { case Msg(cmd: Command, _) => termsPager ! cmd stay() - case Msg(term: AvailableVisitsTermPresentation, bookingData) => - val response = apiService.temporaryReservation(userId.accountId, term.mapTo[TemporaryReservationRequest], term.mapTo[ValuationsRequest]) + case Msg(term: TermExt, bookingData) => + val response = for { + xsrfToken <- apiService.getXsrfToken(userId.accountId) + lockTermResponse <- apiService.reservationLockterm(userId.accountId, xsrfToken, term.mapTo[ReservationLocktermRequest]) + } yield (lockTermResponse, xsrfToken) response match { case Left(ex) => - warn(s"Service [${bookingData.serviceId.name}] is already booked. Ask to update term", ex) - bot.sendMessage(userId.source, lang.visitAlreadyExists, - inlineKeyboard = createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes)))) - goto(awaitRebookDecision) using bookingData.copy(term = Some(term)) - case Right((temporaryReservation, valuations)) => - bot.sendMessage(userId.source, lang.confirmAppointment(term, valuations), - inlineKeyboard = createInlineKeyboard(Seq(Button(lang.cancel, Tags.Cancel), Button(lang.book, Tags.Book)))) - goto(awaitReservation) using bookingData.copy(term = Some(term), temporaryReservationId = Some(temporaryReservation.id), valuations = Some(valuations)) + error("Can not lock term", ex) + bot.sendMessage(userId.source, ex.getMessage) + end() + case Right((reservationLocktermResponse, xsrfToken)) => + if (reservationLocktermResponse.value.changeTermAvailable) { + warn(s"Service [${bookingData.serviceId.name}] is already booked. Ask to update term") + bot.sendMessage(userId.source, lang.visitAlreadyExists, + inlineKeyboard = createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes)))) + goto(awaitRebookDecision) using bookingData.copy(term = Some(term), xsrfToken = Some(xsrfToken), reservationLocktermResponse = Some(reservationLocktermResponse)) + } else { + bot.sendMessage(userId.source, lang.confirmAppointment(term), + inlineKeyboard = createInlineKeyboard(Seq(Button(lang.cancel, Tags.Cancel), Button(lang.book, Tags.Book)))) + goto(awaitReservation) using bookingData.copy(term = Some(term), xsrfToken = Some(xsrfToken), reservationLocktermResponse = Some(reservationLocktermResponse)) + } } case Msg(Pager.NoItemsFound, _) => goto(askNoTermsAction) @@ -196,8 +176,8 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da createInlineKeyboard(Seq(Button(lang.modifyDate, Tags.ModifyDate), Button(lang.createMonitoring, Tags.CreateMonitoring)))) } onReply { case Msg(CallbackCommand(Tags.ModifyDate), bookingData) => - goto(requestDateFrom) using bookingData.copy(dateFrom = ZonedDateTime.now(), - dateTo = ZonedDateTime.now().plusDays(1L)) + goto(requestDateFrom) using bookingData.copy(dateFrom = LocalDateTime.now(), + dateTo = LocalDateTime.now().plusDays(1L)) case Msg(CallbackCommand(Tags.CreateMonitoring), bookingData) => val settingsMaybe = dataService.findSettings(userId.userId) val (defaultOffset, askOffset) = settingsMaybe match { @@ -212,7 +192,8 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da private def awaitRebookDecision: Step = monologue { case Msg(CallbackCommand(Tags.Yes), bookingData: BookingData) => - apiService.updateReservedVisit(userId.accountId, bookingData.term.get) match { + apiService.reservationChangeTerm(userId.accountId, bookingData.xsrfToken.get, + (bookingData.reservationLocktermResponse.get, bookingData.term.get).mapTo[ReservationChangetermRequest]) match { case Right(success) => debug(s"Successfully confirmed: $success") bot.sendMessage(userId.source, lang.appointmentIsConfirmed) @@ -229,7 +210,7 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da private def awaitReservation: Step = monologue { case Msg(CallbackCommand(Tags.Cancel), bookingData: BookingData) => - apiService.deleteTemporaryReservation(userId.accountId, bookingData.temporaryReservationId.get) + apiService.deleteTemporaryReservation(userId.accountId, bookingData.xsrfToken.get, bookingData.reservationLocktermResponse.get.value.temporaryReservationId) stay() case Msg(CallbackCommand(Tags.Book), bookingData: BookingData) => makeReservation(bookingData) @@ -238,15 +219,13 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da private def makeReservation(bookingData: BookingData): Unit = { val reservationRequestMaybe = for { - tmpReservationId <- bookingData.temporaryReservationId - valuations <- bookingData.valuations - visitTermVariant <- valuations.visitTermVariants.headOption + reservationLocktermResponse <- bookingData.reservationLocktermResponse term <- bookingData.term - } yield (tmpReservationId, visitTermVariant, term).mapTo[ReservationRequest] + } yield (reservationLocktermResponse, term).mapTo[ReservationConfirmRequest] reservationRequestMaybe match { case Some(reservationRequest) => - apiService.reservation(userId.accountId, reservationRequest) match { + apiService.reservationConfirm(userId.accountId, bookingData.xsrfToken.get, reservationRequest) match { case Left(ex) => error("Error during reservation", ex) bot.sendMessage(userId.source, ex.getMessage) @@ -322,11 +301,11 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da 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), + serviceId: IdName = null, doctorId: IdName = null, dateFrom: LocalDateTime = LocalDateTime.now(), + dateTo: LocalDateTime = LocalDateTime.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, offset: Int = 0, payerId: Long = 0, payers: Seq[IdName] = Seq()) + term: Option[TermExt] = None, reservationLocktermResponse: Option[ReservationLocktermResponse] = None, + offset: Int = 0, payerId: Long = 0, payers: Seq[IdName] = Seq(), xsrfToken: Option[XsrfToken] = None) object Tags { val Cancel = "cancel" diff --git a/server/src/main/scala/com/lbs/server/conversation/BookWithTemplate.scala b/server/src/main/scala/com/lbs/server/conversation/BookWithTemplate.scala index 571bb61..075a21e 100644 --- a/server/src/main/scala/com/lbs/server/conversation/BookWithTemplate.scala +++ b/server/src/main/scala/com/lbs/server/conversation/BookWithTemplate.scala @@ -1,8 +1,6 @@ package com.lbs.server.conversation -import java.time.{LocalTime, ZonedDateTime} - import akka.actor.ActorSystem import com.lbs.api.json.model._ import com.lbs.bot._ @@ -19,9 +17,11 @@ import com.lbs.server.service.{ApiService, DataService, MonitoringService} import com.lbs.server.util.MessageExtractors._ import com.lbs.server.util.ServerModelConverters._ +import java.time.{LocalDateTime, LocalTime} + class BookWithTemplate(val userId: UserId, bot: Bot, apiService: ApiService, dataService: DataService, monitoringService: MonitoringService, val localization: Localization, datePickerFactory: UserIdWithOriginatorTo[DatePicker], timePickerFactory: UserIdWithOriginatorTo[TimePicker], - termsPagerFactory: UserIdWithOriginatorTo[Pager[AvailableVisitsTermPresentation]])(implicit val actorSystem: ActorSystem) extends Conversation[BookingData] with Localizable { + termsPagerFactory: UserIdWithOriginatorTo[Pager[TermExt]])(implicit val actorSystem: ActorSystem) extends Conversation[BookingData] with Localizable { private val datePicker = datePickerFactory(userId, self) private val timePicker = timePickerFactory(userId, self) @@ -37,42 +37,11 @@ class BookWithTemplate(val userId: UserId, bot: Bot, apiService: ApiService, dat clinicId = IdName.from(monitoring.clinicId, monitoring.clinicName), serviceId = IdName.from(monitoring.serviceId, monitoring.serviceName), doctorId = IdName.from(monitoring.doctorId, monitoring.doctorName), - dateFrom = monitoring.dateFrom, - dateTo = monitoring.dateTo, + dateFrom = monitoring.dateFrom.toLocalDateTime, + dateTo = monitoring.dateTo.toLocalDateTime, timeFrom = monitoring.timeFrom, timeTo = monitoring.timeTo) - goto(determinePayer) using bookingData - } - - private def determinePayer: Step = - process { bookingData => - val response = apiService.getPayers(userId.accountId, bookingData.cityId.id, bookingData.clinicId.optionalId, bookingData.serviceId.id) - response match { - case Left(ex) => - warn(s"Can't determine payers for account ${userId.accountId}, city ${bookingData.cityId.id}, " + - s"clinic ${bookingData.clinicId.optionalId} and service ${bookingData.serviceId.id}", ex) - bot.sendMessage(userId.source, lang.canNotDetectPayer(ex.getMessage)) - end() - case Right((defaultPayerMaybe, payers)) => - defaultPayerMaybe match { - case Some(defaultPayer) => - goto(requestDateFrom) using bookingData.copy(payerId = defaultPayer.id) - case None => - goto(askPayer) using bookingData.copy(payers = payers) - } - } - } - - private def askPayer: Step = - ask { bookingData => - bot.sendMessage( - userId.source, - lang.pleaseChoosePayer, - inlineKeyboard = createInlineKeyboard(bookingData.payers.map(payer => Button(payer.name, payer.id))) - ) - } onReply { - case Msg(CallbackCommand(LongString(payerId)), bookingData) => - goto(requestDateFrom) using bookingData.copy(payerId = payerId) + goto(requestDateFrom) using bookingData } private def requestDateFrom: Step = @@ -84,7 +53,7 @@ class BookWithTemplate(val userId: UserId, bot: Bot, apiService: ApiService, dat case Msg(cmd: Command, _) => datePicker ! cmd stay() - case Msg(date: ZonedDateTime, bookingData: BookingData) => + case Msg(date: LocalDateTime, bookingData: BookingData) => goto(requestDateTo) using bookingData.copy(dateFrom = date) } @@ -97,7 +66,7 @@ class BookWithTemplate(val userId: UserId, bot: Bot, apiService: ApiService, dat case Msg(cmd: Command, _) => datePicker ! cmd stay() - case Msg(date: ZonedDateTime, bookingData: BookingData) => + case Msg(date: LocalDateTime, bookingData: BookingData) => goto(requestTimeFrom) using bookingData.copy(dateTo = date) } @@ -139,33 +108,36 @@ class BookWithTemplate(val userId: UserId, bot: Bot, apiService: ApiService, dat case Msg(CallbackCommand(Tags.FindTerms), _) => goto(requestTerm) case Msg(CallbackCommand(Tags.ModifyDate), bookingData) => - goto(requestDateFrom) using bookingData.copy(dateFrom = ZonedDateTime.now(), - dateTo = ZonedDateTime.now().plusDays(1L)) + goto(requestDateFrom) using bookingData.copy(dateFrom = LocalDateTime.now(), + dateTo = LocalDateTime.now().plusDays(1L)) } private def requestTerm: Step = ask { bookingData => - val availableTerms = apiService.getAvailableTerms(userId.accountId, bookingData.payerId, bookingData.cityId.id, + val availableTerms = apiService.getAvailableTerms(userId.accountId, bookingData.cityId.id, bookingData.clinicId.optionalId, bookingData.serviceId.id, bookingData.doctorId.optionalId, - bookingData.dateFrom, Some(bookingData.dateTo), timeFrom = bookingData.timeFrom, timeTo = bookingData.timeTo) + bookingData.dateFrom, bookingData.dateTo, timeFrom = bookingData.timeFrom, timeTo = bookingData.timeTo) termsPager.restart() termsPager ! availableTerms.map(new SimpleItemsProvider(_)) } onReply { case Msg(cmd: Command, _) => termsPager ! cmd stay() - case Msg(term: AvailableVisitsTermPresentation, bookingData) => - val response = apiService.temporaryReservation(userId.accountId, term.mapTo[TemporaryReservationRequest], term.mapTo[ValuationsRequest]) + case Msg(term: TermExt, bookingData) => + val response = for { + xsrfToken <- apiService.getXsrfToken(userId.accountId) + lockTermResponse <- apiService.reservationLockterm(userId.accountId, xsrfToken, term.mapTo[ReservationLocktermRequest]) + } yield (lockTermResponse, xsrfToken) response match { case Left(ex) => warn(s"Service [${bookingData.serviceId.name}] is already booked. Ask to update term", ex) bot.sendMessage(userId.source, lang.visitAlreadyExists, inlineKeyboard = createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes)))) goto(awaitRebookDecision) using bookingData.copy(term = Some(term)) - case Right((temporaryReservation, valuations)) => - bot.sendMessage(userId.source, lang.confirmAppointment(term, valuations), + case Right((reservationLocktermResponse, xsrfToken)) => + bot.sendMessage(userId.source, lang.confirmAppointment(term), inlineKeyboard = createInlineKeyboard(Seq(Button(lang.cancel, Tags.Cancel), Button(lang.book, Tags.Book)))) - goto(awaitReservation) using bookingData.copy(term = Some(term), temporaryReservationId = Some(temporaryReservation.id), valuations = Some(valuations)) + goto(awaitReservation) using bookingData.copy(term = Some(term), xsrfToken = Some(xsrfToken), reservationLocktermResponse = Some(reservationLocktermResponse)) } case Msg(Pager.NoItemsFound, _) => goto(askNoTermsAction) @@ -177,8 +149,8 @@ class BookWithTemplate(val userId: UserId, bot: Bot, apiService: ApiService, dat createInlineKeyboard(Seq(Button(lang.modifyDate, Tags.ModifyDate), Button(lang.createMonitoring, Tags.CreateMonitoring)))) } onReply { case Msg(CallbackCommand(Tags.ModifyDate), bookingData) => - goto(requestDateFrom) using bookingData.copy(dateFrom = ZonedDateTime.now(), - dateTo = ZonedDateTime.now().plusDays(1L)) + goto(requestDateFrom) using bookingData.copy(dateFrom = LocalDateTime.now(), + dateTo = LocalDateTime.now().plusDays(1L)) case Msg(CallbackCommand(Tags.CreateMonitoring), bookingData) => val settingsMaybe = dataService.findSettings(userId.userId) val (defaultOffset, askOffset) = settingsMaybe match { @@ -193,7 +165,7 @@ class BookWithTemplate(val userId: UserId, bot: Bot, apiService: ApiService, dat private def awaitRebookDecision: Step = monologue { case Msg(CallbackCommand(Tags.Yes), bookingData: BookingData) => - apiService.updateReservedVisit(userId.accountId, bookingData.term.get) match { + apiService.reservationChangeTerm(userId.accountId, bookingData.xsrfToken.get, (bookingData.reservationLocktermResponse.get, bookingData.term.get).mapTo[ReservationChangetermRequest]) match { case Right(success) => debug(s"Successfully confirmed: $success") bot.sendMessage(userId.source, lang.appointmentIsConfirmed) @@ -210,7 +182,7 @@ class BookWithTemplate(val userId: UserId, bot: Bot, apiService: ApiService, dat private def awaitReservation: Step = monologue { case Msg(CallbackCommand(Tags.Cancel), bookingData: BookingData) => - apiService.deleteTemporaryReservation(userId.accountId, bookingData.temporaryReservationId.get) + apiService.deleteTemporaryReservation(userId.accountId, bookingData.xsrfToken.get, bookingData.reservationLocktermResponse.get.value.temporaryReservationId) stay() case Msg(CallbackCommand(Tags.Book), bookingData: BookingData) => makeReservation(bookingData) @@ -219,15 +191,13 @@ class BookWithTemplate(val userId: UserId, bot: Bot, apiService: ApiService, dat private def makeReservation(bookingData: BookingData): Unit = { val reservationRequestMaybe = for { - tmpReservationId <- bookingData.temporaryReservationId - valuations <- bookingData.valuations - visitTermVariant <- valuations.visitTermVariants.headOption + reservationLocktermResponse <- bookingData.reservationLocktermResponse term <- bookingData.term - } yield (tmpReservationId, visitTermVariant, term).mapTo[ReservationRequest] + } yield (reservationLocktermResponse, term).mapTo[ReservationConfirmRequest] reservationRequestMaybe match { case Some(reservationRequest) => - apiService.reservation(userId.accountId, reservationRequest) match { + apiService.reservationConfirm(userId.accountId, bookingData.xsrfToken.get, reservationRequest) match { case Left(ex) => error("Error during reservation", ex) bot.sendMessage(userId.source, ex.getMessage) diff --git a/server/src/main/scala/com/lbs/server/conversation/Chat.scala b/server/src/main/scala/com/lbs/server/conversation/Chat.scala index fe27921..97d2871 100644 --- a/server/src/main/scala/com/lbs/server/conversation/Chat.scala +++ b/server/src/main/scala/com/lbs/server/conversation/Chat.scala @@ -13,8 +13,8 @@ import com.lbs.server.util.MessageExtractors._ import scala.util.matching.Regex class Chat(val userId: UserId, dataService: DataService, monitoringService: MonitoringService, bookingFactory: UserIdTo[Book], - helpFactory: UserIdTo[Help], monitoringsFactory: UserIdTo[Monitorings], monitoringsHistoryFactory: UserIdTo[MonitoringsHistory], historyFactory: UserIdTo[History], - visitsFactory: UserIdTo[Visits], settingsFactory: UserIdTo[Settings], accountFactory: UserIdTo[Account])(val actorSystem: ActorSystem) extends Conversation[Unit] with Logger { + helpFactory: UserIdTo[Help], monitoringsFactory: UserIdTo[Monitorings], monitoringsHistoryFactory: UserIdTo[MonitoringsHistory], historyFactory: UserIdTo[HistoryViewer], + visitsFactory: UserIdTo[ReservedVisitsViewer], settingsFactory: UserIdTo[Settings], accountFactory: UserIdTo[Account])(val actorSystem: ActorSystem) extends Conversation[Unit] with Logger { private val book = bookingFactory(userId) private val help = helpFactory(userId) diff --git a/server/src/main/scala/com/lbs/server/conversation/DatePicker.scala b/server/src/main/scala/com/lbs/server/conversation/DatePicker.scala index 234b409..81e96b1 100644 --- a/server/src/main/scala/com/lbs/server/conversation/DatePicker.scala +++ b/server/src/main/scala/com/lbs/server/conversation/DatePicker.scala @@ -1,9 +1,6 @@ package com.lbs.server.conversation -import java.time.format.TextStyle -import java.time.{LocalTime, ZonedDateTime} - import akka.actor.ActorSystem import com.lbs.bot.model.Button import com.lbs.bot.{Bot, _} @@ -14,18 +11,20 @@ import com.lbs.server.lang.{Localizable, Localization} import com.lbs.server.util.DateTimeUtil._ import com.lbs.server.util.MessageExtractors.{CallbackCommand, TextCommand} +import java.time.format.TextStyle +import java.time.{LocalDateTime, LocalTime} import scala.util.control.NonFatal /** - * Date picker Inline Keyboard - * - * ⬆ ⬆ ⬆ - * dd MM yyyy - * ⬇ ⬇ ⬇ - * - */ + * Date picker Inline Keyboard + * + * ⬆ ⬆ ⬆ + * dd MM yyyy + * ⬇ ⬇ ⬇ + * + */ class DatePicker(val userId: UserId, val bot: Bot, val localization: Localization, originator: Interactional) - (val actorSystem: ActorSystem) extends Conversation[ZonedDateTime] with Localizable { + (val actorSystem: ActorSystem) extends Conversation[LocalDateTime] with Localizable { private var mode: Mode = DateFromMode @@ -36,7 +35,7 @@ class DatePicker(val userId: UserId, val bot: Bot, val localization: Localizatio case Msg(newMode: Mode, _) => mode = newMode stay() - case Msg(initialDate: ZonedDateTime, _) => + case Msg(initialDate: LocalDateTime, _) => goto(requestDate) using initialDate } @@ -52,7 +51,7 @@ class DatePicker(val userId: UserId, val bot: Bot, val localization: Localizatio val (message, updatedDate) = mode match { case DateFromMode => val startOfTheDay = finalDate.`with`(LocalTime.MIN) - val dateFrom = if (startOfTheDay.isBefore(ZonedDateTime.now())) finalDate else startOfTheDay + val dateFrom = if (startOfTheDay.isBefore(LocalDateTime.now())) finalDate else startOfTheDay lang.dateFromIs(dateFrom) -> dateFrom case DateToMode => val dateTo = finalDate.`with`(LocalTime.MAX).minusHours(2) @@ -85,7 +84,7 @@ class DatePicker(val userId: UserId, val bot: Bot, val localization: Localizatio stay() using modifiedDate } - private def modifyDate(date: ZonedDateTime, tag: String) = { + private def modifyDate(date: LocalDateTime, tag: String) = { val dateModifier = tag match { case Tags.DayInc => date.plusDays _ case Tags.MonthInc => date.plusMonths _ @@ -97,7 +96,7 @@ class DatePicker(val userId: UserId, val bot: Bot, val localization: Localizatio dateModifier(1) } - private def dateButtons(date: ZonedDateTime) = { + private def dateButtons(date: LocalDateTime) = { val day = date.getDayOfMonth.toString val dayOfWeek = date.getDayOfWeek.getDisplayName(TextStyle.SHORT, lang.locale) val month = date.getMonth.getDisplayName(TextStyle.SHORT, lang.locale) diff --git a/server/src/main/scala/com/lbs/server/conversation/History.scala b/server/src/main/scala/com/lbs/server/conversation/HistoryViewer.scala similarity index 66% rename from server/src/main/scala/com/lbs/server/conversation/History.scala rename to server/src/main/scala/com/lbs/server/conversation/HistoryViewer.scala index 784bc1f..b19d4d8 100644 --- a/server/src/main/scala/com/lbs/server/conversation/History.scala +++ b/server/src/main/scala/com/lbs/server/conversation/HistoryViewer.scala @@ -2,7 +2,7 @@ package com.lbs.server.conversation import akka.actor.ActorSystem -import com.lbs.api.json.model.HistoricVisit +import com.lbs.api.json.model.Event import com.lbs.bot.Bot import com.lbs.bot.model.Command import com.lbs.server.conversation.Login.UserId @@ -11,8 +11,8 @@ import com.lbs.server.conversation.base.Conversation import com.lbs.server.lang.{Localizable, Localization} import com.lbs.server.service.ApiService -class History(val userId: UserId, bot: Bot, apiService: ApiService, val localization: Localization, - historyPagerFactory: UserIdWithOriginatorTo[Pager[HistoricVisit]])(val actorSystem: ActorSystem) extends Conversation[Unit] with Localizable { +class HistoryViewer(val userId: UserId, bot: Bot, apiService: ApiService, val localization: Localization, + historyPagerFactory: UserIdWithOriginatorTo[Pager[Event]])(val actorSystem: ActorSystem) extends Conversation[Unit] with Localizable { private val historyPager = historyPagerFactory(userId, self) @@ -20,7 +20,7 @@ class History(val userId: UserId, bot: Bot, apiService: ApiService, val localiza def prepareData: Step = process { _ => - val visits = apiService.visitsHistory(userId.accountId) + val visits = apiService.history(userId.accountId) historyPager.restart() historyPager ! visits.map(new SimpleItemsProvider(_)) goto(processResponseFromPager) @@ -32,9 +32,9 @@ class History(val userId: UserId, bot: Bot, apiService: ApiService, val localiza historyPager ! cmd stay() case Msg(Pager.NoItemsFound, _) => - bot.sendMessage(userId.source, lang.visitsHistoryIsEmpty) + bot.sendMessage(userId.source, lang.eventsListIsEmpty) end() - case Msg(_: HistoricVisit, _) => + case Msg(_: Event, _) => end() } diff --git a/server/src/main/scala/com/lbs/server/conversation/Login.scala b/server/src/main/scala/com/lbs/server/conversation/Login.scala index acc98f9..a42e5ca 100644 --- a/server/src/main/scala/com/lbs/server/conversation/Login.scala +++ b/server/src/main/scala/com/lbs/server/conversation/Login.scala @@ -41,15 +41,14 @@ class Login(source: MessageSource, bot: Bot, dataService: DataService, apiServic } onReply { case Msg(MessageExtractors.TextCommand(plainPassword), username) => val password = textEncryptor.encrypt(plainPassword) - val loginResult = apiService.login(username, password) - loginResult match { + apiService.fullLogin(username, password) match { case Left(error) => bot.sendMessage(source, error.getMessage) goto(requestUsername) - case Right(loggedIn) => + case Right(session) => val credentials = dataService.saveCredentials(source, username, password) userId = UserId(credentials.userId, credentials.accountId, source) - apiService.addSession(credentials.accountId, loggedIn.accessToken, loggedIn.tokenType) + apiService.addSession(credentials.accountId, session) bot.sendMessage(source, lang.loginAndPasswordAreOk) originator ! LoggedIn(forwardCommand, credentials.userId, credentials.accountId) end() diff --git a/server/src/main/scala/com/lbs/server/conversation/Visits.scala b/server/src/main/scala/com/lbs/server/conversation/ReservedVisitsViewer.scala similarity index 70% rename from server/src/main/scala/com/lbs/server/conversation/Visits.scala rename to server/src/main/scala/com/lbs/server/conversation/ReservedVisitsViewer.scala index 51853f0..393218f 100644 --- a/server/src/main/scala/com/lbs/server/conversation/Visits.scala +++ b/server/src/main/scala/com/lbs/server/conversation/ReservedVisitsViewer.scala @@ -2,18 +2,18 @@ package com.lbs.server.conversation import akka.actor.ActorSystem -import com.lbs.api.json.model.ReservedVisit +import com.lbs.api.json.model.Event import com.lbs.bot.model.{Button, Command} import com.lbs.bot.{Bot, _} import com.lbs.server.conversation.Login.UserId import com.lbs.server.conversation.Pager.SimpleItemsProvider -import com.lbs.server.conversation.Visits.Tags +import com.lbs.server.conversation.ReservedVisitsViewer.Tags import com.lbs.server.conversation.base.Conversation import com.lbs.server.lang.{Localizable, Localization} import com.lbs.server.service.ApiService -class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localization: Localization, - visitsPagerFactory: UserIdWithOriginatorTo[Pager[ReservedVisit]])(val actorSystem: ActorSystem) extends Conversation[ReservedVisit] with Localizable { +class ReservedVisitsViewer(val userId: UserId, bot: Bot, apiService: ApiService, val localization: Localization, + visitsPagerFactory: UserIdWithOriginatorTo[Pager[Event]])(val actorSystem: ActorSystem) extends Conversation[Event] with Localizable { private val reservedVisitsPager = visitsPagerFactory(userId, self) @@ -21,7 +21,7 @@ class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localizat def prepareData: Step = process { _ => - val visits = apiService.reservedVisits(userId.accountId) + val visits = apiService.reserved(userId.accountId) reservedVisitsPager.restart() reservedVisitsPager ! visits.map(new SimpleItemsProvider(_)) goto(processResponseFromPager) @@ -35,7 +35,7 @@ class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localizat case Msg(Pager.NoItemsFound, _) => bot.sendMessage(userId.source, lang.noUpcomingVisits) end() - case Msg(visit: ReservedVisit, _) => + case Msg(visit: Event, _) => goto(askToCancelVisit) using visit } @@ -47,10 +47,10 @@ class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localizat case Msg(Command(_, _, Some(Tags.No)), _) => bot.sendMessage(userId.source, lang.appointmentWasNotCancelled) end() - case Msg(Command(_, _, Some(Tags.Yes)), visit: ReservedVisit) => - apiService.deleteReservation(userId.accountId, visit.reservationId) match { + case Msg(Command(_, _, Some(Tags.Yes)), visit: Event) => + apiService.deleteReservation(userId.accountId, visit.eventId) match { case Left(ex) => bot.sendMessage(userId.source, lang.unableToCancelUpcomingVisit(ex.getMessage)) - case Right(r) => bot.sendMessage(userId.source, lang.appointmentHasBeenCancelled) + case Right(_) => bot.sendMessage(userId.source, lang.appointmentHasBeenCancelled) } end() } @@ -60,7 +60,7 @@ class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localizat } } -object Visits { +object ReservedVisitsViewer { object Tags { val Yes = "yes" diff --git a/server/src/main/scala/com/lbs/server/conversation/Settings.scala b/server/src/main/scala/com/lbs/server/conversation/Settings.scala index e3cc3ff..f62c02a 100644 --- a/server/src/main/scala/com/lbs/server/conversation/Settings.scala +++ b/server/src/main/scala/com/lbs/server/conversation/Settings.scala @@ -8,9 +8,9 @@ import com.lbs.server.conversation.Login.UserId import com.lbs.server.conversation.Settings._ import com.lbs.server.conversation.base.Conversation import com.lbs.server.lang.{Lang, Localizable, Localization} +import com.lbs.server.repository.model import com.lbs.server.service.DataService import com.lbs.server.util.MessageExtractors.{CallbackCommand, IntString, TextCommand} -import com.lbs.server.repository.model class Settings(val userId: UserId, bot: Bot, dataService: DataService, val localization: Localization)(val actorSystem: ActorSystem) extends Conversation[Unit] with Localizable { diff --git a/server/src/main/scala/com/lbs/server/conversation/StaticData.scala b/server/src/main/scala/com/lbs/server/conversation/StaticData.scala index 37459bf..7db57d6 100644 --- a/server/src/main/scala/com/lbs/server/conversation/StaticData.scala +++ b/server/src/main/scala/com/lbs/server/conversation/StaticData.scala @@ -2,7 +2,7 @@ package com.lbs.server.conversation import akka.actor.ActorSystem -import com.lbs.api.json.model.IdName +import com.lbs.api.json.model.{IdName, Identified} import com.lbs.bot.model.{Button, Command, TaggedButton} import com.lbs.bot.{Bot, _} import com.lbs.server.ThrowableOr @@ -79,6 +79,6 @@ object StaticData { case class FindOptions(searchText: String) - case class FoundOptions(option: ThrowableOr[List[IdName]]) + case class FoundOptions[T <: Identified](option: ThrowableOr[List[T]]) } \ No newline at end of file diff --git a/server/src/main/scala/com/lbs/server/conversation/StaticDataForBooking.scala b/server/src/main/scala/com/lbs/server/conversation/StaticDataForBooking.scala index 07a18c9..c151e84 100644 --- a/server/src/main/scala/com/lbs/server/conversation/StaticDataForBooking.scala +++ b/server/src/main/scala/com/lbs/server/conversation/StaticDataForBooking.scala @@ -1,18 +1,18 @@ package com.lbs.server.conversation -import com.lbs.api.json.model.IdName +import com.lbs.api.json.model.{IdName, Identified} import com.lbs.bot.model.Command +import com.lbs.server.ThrowableOr import com.lbs.server.conversation.Book.BookingData import com.lbs.server.conversation.StaticData.{FindOptions, FoundOptions, LatestOptions, StaticDataConfig} import com.lbs.server.conversation.base.Conversation -import com.lbs.server.ThrowableOr trait StaticDataForBooking extends Conversation[BookingData] { private[conversation] def staticData: StaticData - protected def withFunctions(latestOptions: => Seq[IdName], staticOptions: => ThrowableOr[List[IdName]], applyId: IdName => BookingData): Step => MessageProcessorFn = { + protected def withFunctions[T <: Identified](latestOptions: => Seq[IdName], staticOptions: => ThrowableOr[List[T]], applyId: IdName => BookingData): Step => MessageProcessorFn = { nextStep: Step => { case Msg(cmd: Command, _) => staticData ! cmd @@ -39,7 +39,7 @@ trait StaticDataForBooking extends Conversation[BookingData] { } } - private def filterOptions(options: ThrowableOr[List[IdName]], searchText: String) = { + private def filterOptions[T <: Identified](options: ThrowableOr[List[T]], searchText: String) = { options.map(opt => opt.filter(c => c.name.toLowerCase.contains(searchText))) } } diff --git a/server/src/main/scala/com/lbs/server/conversation/TimePicker.scala b/server/src/main/scala/com/lbs/server/conversation/TimePicker.scala index e09a421..a825e27 100644 --- a/server/src/main/scala/com/lbs/server/conversation/TimePicker.scala +++ b/server/src/main/scala/com/lbs/server/conversation/TimePicker.scala @@ -1,8 +1,6 @@ package com.lbs.server.conversation -import java.time.LocalTime - import akka.actor.ActorSystem import com.lbs.bot.model.Button import com.lbs.bot.{Bot, _} @@ -13,6 +11,7 @@ import com.lbs.server.lang.{Localizable, Localization} import com.lbs.server.util.DateTimeUtil.applyHourMinute import com.lbs.server.util.MessageExtractors.{CallbackCommand, TextCommand} +import java.time.LocalTime import scala.util.control.NonFatal /** 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 5014c24..c281b0c 100644 --- a/server/src/main/scala/com/lbs/server/lang/En.scala +++ b/server/src/main/scala/com/lbs/server/lang/En.scala @@ -1,13 +1,13 @@ package com.lbs.server.lang -import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit, ValuationsResponse} +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.util.DateTimeUtil._ -import java.time.{LocalTime, ZonedDateTime} +import java.time.{LocalDateTime, LocalTime} import java.util.Locale object En extends Lang { @@ -36,18 +36,18 @@ object En extends Lang { override def noUpcomingVisits: String = "ℹ No upcoming visits found" - override def areYouSureToCancelAppointment(visit: ReservedVisit): String = + override def areYouSureToCancelAppointment(event: Event): String = s""" Are you sure want to cancel appointment? | - |⏱ ${formatDateTime(visit.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${visit.doctorName} - |${capitalizeFirstLetter(service)}: ${visit.service.name} - |${capitalizeFirstLetter(clinic)}: ${visit.clinic.name} + |⏱ ${formatDateTime(event.date, locale)} + |${capitalizeFirstLetter(doctor)}: ${capitalizeFirstLetter(event.doctor.name)} ${capitalizeFirstLetter(event.doctor.lastname)} + |${capitalizeFirstLetter(service)}: ${event.title} + |${capitalizeFirstLetter(clinic)}: ${event.clinic.map(c => s"${capitalizeFirstLetter(c.city)} - ${capitalizeFirstLetter(c.address)}").getOrElse("Telemedicine")} |""".stripMargin - override def chooseDateFrom(exampleDate: ZonedDateTime): String = s" Please choose date from or write it manually using format dd-MM, e.g. ${formatDateShort(exampleDate)}" + override def chooseDateFrom(exampleDate: LocalDateTime): String = s" Please choose date from or write it manually using format dd-MM, e.g. ${formatDateShort(exampleDate)}" - override def chooseDateTo(exampleDate: ZonedDateTime): String = s" Please choose date to or write it manually using format dd-MM, e.g. ${formatDateShort(exampleDate)}" + override def chooseDateTo(exampleDate: LocalDateTime): String = s" Please choose date to or write it manually using format dd-MM, e.g. ${formatDateShort(exampleDate)}" override def findTerms: String = "🔍 Find terms" @@ -73,15 +73,13 @@ object En extends Lang { override def book: String = "Book" - override def confirmAppointment(term: AvailableVisitsTermPresentation, valuations: ValuationsResponse): String = + override def confirmAppointment(term: TermExt): String = - s""" ${valuations.optionsQuestion.getOrElse("Would you like to confirm your appointment booking?")} + s""" Would you like to confirm your appointment booking? | - |⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} - | - |ℹ${valuations.visitTermVariants.head.infoMessage}""".stripMargin + |⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic}""".stripMargin override def appointmentIsConfirmed: String = "👍 Your appointment has been confirmed!" @@ -165,7 +163,7 @@ object En extends Lang { override def providePassword: String = " Please provide password" - override def visitsHistoryIsEmpty: String = "ℹ No visits in your history" + override def eventsListIsEmpty: String = "ℹ No visits in your history" override def help: String = s"""ℹ Non official bot for Portal Pacjenta LUX MED (v.${Lang.version}). @@ -182,14 +180,14 @@ object En extends Lang { |/settings - settings, e.g. lang |/help - the help""".stripMargin - override def dateFromIs(dateFrom: ZonedDateTime): String = s"📅 Date from is ${formatDate(dateFrom, locale)}" + override def dateFromIs(dateFrom: LocalDateTime): String = s"📅 Date from is ${formatDate(dateFrom, locale)}" - override def dateToIs(dateTo: ZonedDateTime): String = s"📅 Date to is ${formatDate(dateTo, locale)}" + override def dateToIs(dateTo: LocalDateTime): String = s"📅 Date to is ${formatDate(dateTo, locale)}" - override def termEntry(term: AvailableVisitsTermPresentation, page: Int, index: Int): String = - s"""⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + override def termEntry(term: TermExt, page: Int, index: Int): String = + s"""⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic} | /book_$index | |""".stripMargin @@ -197,27 +195,27 @@ object En extends Lang { override def termsHeader(page: Int, pages: Int): String = withPages(" Available terms", page, pages) - override def historyEntry(visit: HistoricVisit, page: Int, index: Int): String = - s"""⏱ ${formatDateTime(visit.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${visit.doctorName} - |${capitalizeFirstLetter(service)}: ${visit.service.name} - |${capitalizeFirstLetter(clinic)}: ${visit.clinicName} + override def historyEntry(event: Event, page: Int, index: Int): String = + s"""⏱ ${formatDateTime(event.date, locale)} + |${capitalizeFirstLetter(doctor)}: ${capitalizeFirstLetter(event.doctor.name)} ${capitalizeFirstLetter(event.doctor.lastname)} + |${capitalizeFirstLetter(service)}: ${event.title} + |${capitalizeFirstLetter(clinic)}: ${event.clinic.map(c => s"${capitalizeFirstLetter(c.city)} - ${capitalizeFirstLetter(c.address)}").getOrElse("Telemedicine")} | |""".stripMargin override def historyHeader(page: Int, pages: Int): String = withPages(" Conducted visits", page, pages) - override def upcomingVisitEntry(visit: ReservedVisit, page: Int, index: Int): String = - s"""⏱ ${formatDateTime(visit.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${visit.doctorName} - |${capitalizeFirstLetter(service)}: ${visit.service.name} - |${capitalizeFirstLetter(clinic)}: ${visit.clinic.name} + override def reservedVisitEntry(event: Event, page: Int, index: Int): String = + s"""⏱ ${formatDateTime(event.date, locale)} + |${capitalizeFirstLetter(doctor)}: ${capitalizeFirstLetter(event.doctor.name)} ${capitalizeFirstLetter(event.doctor.lastname)} + |${capitalizeFirstLetter(service)}: ${event.title} + |${capitalizeFirstLetter(clinic)}: ${event.clinic.map(c => s"${capitalizeFirstLetter(c.city)} - ${capitalizeFirstLetter(c.address)}").getOrElse("Telemedicine")} | /cancel_$index | |""".stripMargin - override def upcomingVisitsHeader(page: Int, pages: Int): String = + override def reservedVisitsHeader(page: Int, pages: Int): String = withPages(" Reserved visits", page, pages) override def bugsHeader(page: Int, pages: Int): String = @@ -258,13 +256,13 @@ object En extends Lang { |Your monitorings were removed. Please /login again and create new monitorings. """.stripMargin - override def availableTermEntry(term: AvailableVisitsTermPresentation, monitoring: Monitoring, index: Int): String = - s"""⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} + override def availableTermEntry(term: TermExt, monitoring: Monitoring, index: Int): String = + s"""⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic} |${capitalizeFirstLetter(city)}: ${monitoring.cityName} - |/reserve_${monitoring.recordId}_${term.scheduleId}_${minutesSinceBeginOf2018(term.visitDate.startDateTime)} + |/reserve_${monitoring.recordId}_${term.term.scheduleId}_${minutesSinceBeginOf2018(term.term.dateTimeFrom)} | |""".stripMargin @@ -285,13 +283,13 @@ object En extends Lang { | | Create new monitoring /book""".stripMargin - override def appointmentIsBooked(term: AvailableVisitsTermPresentation, monitoring: Monitoring): String = + override def appointmentIsBooked(term: TermExt, monitoring: Monitoring): String = s"""👍 We just booked an appointment for you! | - |⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} + |⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic} |${capitalizeFirstLetter(city)}: ${monitoring.cityName}""".stripMargin override def maximumMonitoringsLimitExceeded: String = "Maximum monitorings per user is 10" 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 6517c75..078d1ea 100644 --- a/server/src/main/scala/com/lbs/server/lang/Lang.scala +++ b/server/src/main/scala/com/lbs/server/lang/Lang.scala @@ -1,12 +1,12 @@ package com.lbs.server.lang -import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit, ValuationsResponse} +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 java.time.{LocalTime, ZonedDateTime} +import java.time.{LocalDateTime, LocalTime} import java.util.Locale import scala.io.Source import scala.util.Try @@ -33,8 +33,10 @@ trait Lang { def label: String protected def capitalizeFirstLetter(str: String): String = { - val fistCapitalLetter = str.head.toTitleCase - fistCapitalLetter + str.tail + if (str != null && str != "") { + val fistCapitalLetter = str.head.toTitleCase + fistCapitalLetter + str.tail.toLowerCase + } else "" } protected def withPages(message: String, page: Int, pages: Int): String @@ -49,11 +51,11 @@ trait Lang { def noUpcomingVisits: String - def areYouSureToCancelAppointment(visit: ReservedVisit): String + def areYouSureToCancelAppointment(visit: Event): String - def chooseDateFrom(exampleDate: ZonedDateTime): String + def chooseDateFrom(exampleDate: LocalDateTime): String - def chooseDateTo(exampleDate: ZonedDateTime): String + def chooseDateTo(exampleDate: LocalDateTime): String def chooseTimeFrom(exampleTime: LocalTime): String @@ -75,7 +77,7 @@ trait Lang { def book: String - def confirmAppointment(term: AvailableVisitsTermPresentation, valuations: ValuationsResponse): String + def confirmAppointment(term: TermExt): String def appointmentIsConfirmed: String @@ -143,29 +145,29 @@ trait Lang { def providePassword: String - def visitsHistoryIsEmpty: String + def eventsListIsEmpty: String def help: String - def dateFromIs(dateFrom: ZonedDateTime): String + def dateFromIs(dateFrom: LocalDateTime): String - def dateToIs(dateTo: ZonedDateTime): String + def dateToIs(dateTo: LocalDateTime): String def timeFromIs(timeFrom: LocalTime): String def timeToIs(timeTo: LocalTime): String - def termEntry(term: AvailableVisitsTermPresentation, page: Int, index: Int): String + def termEntry(term: TermExt, page: Int, index: Int): String def termsHeader(page: Int, pages: Int): String - def historyEntry(visit: HistoricVisit, page: Int, index: Int): String + def historyEntry(event: Event, page: Int, index: Int): String def historyHeader(page: Int, pages: Int): String - def upcomingVisitEntry(visit: ReservedVisit, page: Int, index: Int): String + def reservedVisitEntry(visit: Event, page: Int, index: Int): String - def upcomingVisitsHeader(page: Int, pages: Int): String + def reservedVisitsHeader(page: Int, pages: Int): String def bugsHeader(page: Int, pages: Int): String @@ -179,13 +181,13 @@ trait Lang { def invalidLoginOrPassword: String - def availableTermEntry(term: AvailableVisitsTermPresentation, monitoring: Monitoring, index: Int): String + def availableTermEntry(term: TermExt, monitoring: Monitoring, index: Int): String def availableTermsHeader(size: Int): String def nothingWasFoundByMonitoring(monitoring: Monitoring): String - def appointmentIsBooked(term: AvailableVisitsTermPresentation, monitoring: Monitoring): String + def appointmentIsBooked(term: TermExt, monitoring: Monitoring): String def maximumMonitoringsLimitExceeded: String diff --git a/server/src/main/scala/com/lbs/server/lang/Localization.scala b/server/src/main/scala/com/lbs/server/lang/Localization.scala index 7458fbc..7685397 100644 --- a/server/src/main/scala/com/lbs/server/lang/Localization.scala +++ b/server/src/main/scala/com/lbs/server/lang/Localization.scala @@ -1,13 +1,13 @@ package com.lbs.server.lang -import java.util.concurrent.ConcurrentHashMap - import com.lbs.server.repository.model import com.lbs.server.service.DataService import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component +import java.util.concurrent.ConcurrentHashMap + @Component class Localization { diff --git a/server/src/main/scala/com/lbs/server/lang/Pl.scala b/server/src/main/scala/com/lbs/server/lang/Pl.scala index b4b8d43..e08f313 100644 --- a/server/src/main/scala/com/lbs/server/lang/Pl.scala +++ b/server/src/main/scala/com/lbs/server/lang/Pl.scala @@ -1,13 +1,13 @@ package com.lbs.server.lang -import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit, ValuationsResponse} +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.util.DateTimeUtil._ -import java.time.{LocalTime, ZonedDateTime} +import java.time.{LocalDateTime, LocalTime} import java.util.Locale object Pl extends Lang { @@ -36,18 +36,18 @@ object Pl extends Lang { override def noUpcomingVisits: String = "ℹ Nie znaleziono wizyt" - override def areYouSureToCancelAppointment(visit: ReservedVisit): String = + override def areYouSureToCancelAppointment(event: Event): String = s""" Czy na pewno chcesz anulować wizytę? | - |⏱ ${formatDateTime(visit.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${visit.doctorName} - |${capitalizeFirstLetter(service)}: ${visit.service.name} - |${capitalizeFirstLetter(clinic)}: ${visit.clinic.name} + |⏱ ${formatDateTime(event.date, locale)} + |${capitalizeFirstLetter(doctor)}: ${capitalizeFirstLetter(event.doctor.name)} ${capitalizeFirstLetter(event.doctor.lastname)} + |${capitalizeFirstLetter(service)}: ${event.title} + |${capitalizeFirstLetter(clinic)}: ${event.clinic.map(c => s"${capitalizeFirstLetter(c.city)} - ${capitalizeFirstLetter(c.address)}").getOrElse("Telemedicine")} |""".stripMargin - override def chooseDateFrom(exampleDate: ZonedDateTime): String = s" Wybierz datę albo zapisz ją w formacie dd-MM, np. ${formatDateShort(exampleDate)}" + override def chooseDateFrom(exampleDate: LocalDateTime): String = s" Wybierz datę albo zapisz ją w formacie dd-MM, np. ${formatDateShort(exampleDate)}" - override def chooseDateTo(exampleDate: ZonedDateTime): String = s" Wybierz datę albo zapisz ją w formacie dd-MM, np. ${formatDateShort(exampleDate)}" + override def chooseDateTo(exampleDate: LocalDateTime): String = s" Wybierz datę albo zapisz ją w formacie dd-MM, np. ${formatDateShort(exampleDate)}" override def findTerms: String = "🔍 Szukaj terminów" @@ -73,15 +73,13 @@ object Pl extends Lang { override def book: String = "Zarezerwuj" - override def confirmAppointment(term: AvailableVisitsTermPresentation, valuations: ValuationsResponse): String = + override def confirmAppointment(term: TermExt): String = - s""" ${valuations.optionsQuestion.getOrElse("Czy potwierdzasz wizytę?")} + s""" Czy potwierdzasz wizytę? | - |⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} - | - |ℹ${valuations.visitTermVariants.head.infoMessage}""".stripMargin + |⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic}""".stripMargin override def appointmentIsConfirmed: String = "👍 Twoja wizyta została potwierdzona!" @@ -165,7 +163,7 @@ object Pl extends Lang { override def providePassword: String = " Podaj hasło" - override def visitsHistoryIsEmpty: String = "ℹ Brak wizyt w historii" + override def eventsListIsEmpty: String = "ℹ Brak wizyt" override def help: String = s"""ℹ Nieoficjalny Bot do Portal Pacjenta LUX MED (v.${Lang.version}). @@ -182,14 +180,14 @@ object Pl extends Lang { |/settings - ustawienia, np. język |/help - pomoc""".stripMargin - override def dateFromIs(dateFrom: ZonedDateTime): String = s"📅 Data od ${formatDate(dateFrom, locale)}" + override def dateFromIs(dateFrom: LocalDateTime): String = s"📅 Data od ${formatDate(dateFrom, locale)}" - override def dateToIs(dateTo: ZonedDateTime): String = s"📅 Data do ${formatDate(dateTo, locale)}" + override def dateToIs(dateTo: LocalDateTime): String = s"📅 Data do ${formatDate(dateTo, locale)}" - override def termEntry(term: AvailableVisitsTermPresentation, page: Int, index: Int): String = - s"""⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + override def termEntry(term: TermExt, page: Int, index: Int): String = + s"""⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic} | /book_$index | |""".stripMargin @@ -197,27 +195,27 @@ object Pl extends Lang { override def termsHeader(page: Int, pages: Int): String = withPages(" Dostępne terminy", page, pages) - override def historyEntry(visit: HistoricVisit, page: Int, index: Int): String = - s"""⏱ ${formatDateTime(visit.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${visit.doctorName} - |${capitalizeFirstLetter(service)}: ${visit.service.name} - |${capitalizeFirstLetter(clinic)}: ${visit.clinicName} + override def historyEntry(event: Event, page: Int, index: Int): String = + s"""⏱ ${formatDateTime(event.date, locale)} + |${capitalizeFirstLetter(doctor)}: ${capitalizeFirstLetter(event.doctor.name)} ${capitalizeFirstLetter(event.doctor.lastname)} + |${capitalizeFirstLetter(service)}: ${event.title} + |${capitalizeFirstLetter(clinic)}: ${event.clinic.map(c => s"${capitalizeFirstLetter(c.city)} - ${capitalizeFirstLetter(c.address)}").getOrElse("Telemedicine")} | |""".stripMargin override def historyHeader(page: Int, pages: Int): String = withPages(" Odbyte wizyty", page, pages) - override def upcomingVisitEntry(visit: ReservedVisit, page: Int, index: Int): String = - s"""⏱ ${formatDateTime(visit.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${visit.doctorName} - |${capitalizeFirstLetter(service)}: ${visit.service.name} - |${capitalizeFirstLetter(clinic)}: ${visit.clinic.name} + override def reservedVisitEntry(event: Event, page: Int, index: Int): String = + s"""⏱ ${formatDateTime(event.date, locale)} + |${capitalizeFirstLetter(doctor)}: ${capitalizeFirstLetter(event.doctor.name)} ${capitalizeFirstLetter(event.doctor.lastname)} + |${capitalizeFirstLetter(service)}: ${event.title} + |${capitalizeFirstLetter(clinic)}: ${event.clinic.map(c => s"${capitalizeFirstLetter(c.city)} - ${capitalizeFirstLetter(c.address)}").getOrElse("Telemedicine")} | /cancel_$index | |""".stripMargin - override def upcomingVisitsHeader(page: Int, pages: Int): String = + override def reservedVisitsHeader(page: Int, pages: Int): String = withPages(" Zarezerwowane wizyty", page, pages) override def bugsHeader(page: Int, pages: Int): String = @@ -258,13 +256,13 @@ object Pl extends Lang { |Usunięto monitoringi. Zaloguj się przez /login i stwórz nowe monitoringi. """.stripMargin - override def availableTermEntry(term: AvailableVisitsTermPresentation, monitoring: Monitoring, index: Int): String = - s"""⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} + override def availableTermEntry(term: TermExt, monitoring: Monitoring, index: Int): String = + s"""⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic} |${capitalizeFirstLetter(city)}: ${monitoring.cityName} - |/reserve_${monitoring.recordId}_${term.scheduleId}_${minutesSinceBeginOf2018(term.visitDate.startDateTime)} + |/reserve_${monitoring.recordId}_${term.term.scheduleId}_${minutesSinceBeginOf2018(term.term.dateTimeFrom)} | |""".stripMargin @@ -285,13 +283,13 @@ object Pl extends Lang { | | Stwórz nowy monitoring przez /book""".stripMargin - override def appointmentIsBooked(term: AvailableVisitsTermPresentation, monitoring: Monitoring): String = + override def appointmentIsBooked(term: TermExt, monitoring: Monitoring): String = s"""👍 Zarezerwowaliśmy za Ciebie termin! | - |⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} + |⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic} |${capitalizeFirstLetter(city)}: ${monitoring.cityName}""".stripMargin override def maximumMonitoringsLimitExceeded: String = "Maksymalna liczba monitoringów uzytkownika to 10" 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 6bb4921..8bccc7d 100644 --- a/server/src/main/scala/com/lbs/server/lang/Ua.scala +++ b/server/src/main/scala/com/lbs/server/lang/Ua.scala @@ -1,13 +1,13 @@ package com.lbs.server.lang -import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit, ValuationsResponse} +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.util.DateTimeUtil._ -import java.time.{LocalTime, ZonedDateTime} +import java.time.{LocalDateTime, LocalTime} import java.util.Locale object Ua extends Lang { @@ -36,18 +36,18 @@ object Ua extends Lang { override def noUpcomingVisits: String = "ℹ Не знайдено жодного майбутнього візиту" - override def areYouSureToCancelAppointment(visit: ReservedVisit): String = + override def areYouSureToCancelAppointment(event: Event): String = s""" Ви впевнені, що хочете скасувати візит? | - |⏱ ${formatDateTime(visit.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${visit.doctorName} - |${capitalizeFirstLetter(service)}: ${visit.service.name} - |${capitalizeFirstLetter(clinic)}: ${visit.clinic.name} + |⏱ ${formatDateTime(event.date, locale)} + |${capitalizeFirstLetter(doctor)}: ${capitalizeFirstLetter(event.doctor.name)} ${capitalizeFirstLetter(event.doctor.lastname)} + |${capitalizeFirstLetter(service)}: ${event.title} + |${capitalizeFirstLetter(clinic)}: ${event.clinic.map(c => s"${capitalizeFirstLetter(c.city)} - ${capitalizeFirstLetter(c.address)}").getOrElse("Telemedicine")} |""".stripMargin - override def chooseDateFrom(exampleDate: ZonedDateTime): String = s" Будь ласка, виберіть початкову дату або введіть її, використовуючи формат dd-MM, наприклад ${formatDateShort(exampleDate)}" + override def chooseDateFrom(exampleDate: LocalDateTime): String = s" Будь ласка, виберіть початкову дату або введіть її, використовуючи формат dd-MM, наприклад ${formatDateShort(exampleDate)}" - override def chooseDateTo(exampleDate: ZonedDateTime): String = s" Будь ласка, виберіть кінцеву дату або введіть її, використовуючи формат dd-MM, наприклад ${formatDateShort(exampleDate)}" + override def chooseDateTo(exampleDate: LocalDateTime): String = s" Будь ласка, виберіть кінцеву дату або введіть її, використовуючи формат dd-MM, наприклад ${formatDateShort(exampleDate)}" override def findTerms: String = "🔍 Знайти терміни" @@ -73,15 +73,13 @@ object Ua extends Lang { override def book: String = "Зарезервувати" - override def confirmAppointment(term: AvailableVisitsTermPresentation, valuations: ValuationsResponse): String = + override def confirmAppointment(term: TermExt): String = - s""" ${valuations.optionsQuestion.getOrElse("Ви хотіли б підтвердити резервацію візиту?")} + s""" Ви хотіли б підтвердити резервацію візиту? | - |⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} - | - |ℹ${valuations.visitTermVariants.head.infoMessage}""".stripMargin + |⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic}""".stripMargin override def appointmentIsConfirmed: String = "👍 Ваш візит було підтверджено!" @@ -164,7 +162,7 @@ object Ua extends Lang { override def providePassword: String = " Будь ласка, введіть пароль" - override def visitsHistoryIsEmpty: String = "ℹ Немає візитів в вашій історії" + override def eventsListIsEmpty: String = "ℹ Немає візитів в вашій історії" override def help: String = s"""ℹ Це неофіційний бот для Порталу Пацієнта LUX MED (v.${Lang.version}). @@ -181,14 +179,14 @@ object Ua extends Lang { |/settings - налаштування |/help - допомога""".stripMargin - override def dateFromIs(dateFrom: ZonedDateTime): String = s"📅 Початкова дата ${formatDate(dateFrom, locale)}" + override def dateFromIs(dateFrom: LocalDateTime): String = s"📅 Початкова дата ${formatDate(dateFrom, locale)}" - override def dateToIs(dateTo: ZonedDateTime): String = s"📅 Кінцева дата ${formatDate(dateTo, locale)}" + override def dateToIs(dateTo: LocalDateTime): String = s"📅 Кінцева дата ${formatDate(dateTo, locale)}" - override def termEntry(term: AvailableVisitsTermPresentation, page: Int, index: Int): String = - s"""⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + override def termEntry(term: TermExt, page: Int, index: Int): String = + s"""⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic} | /book_$index | |""".stripMargin @@ -196,27 +194,27 @@ object Ua extends Lang { override def termsHeader(page: Int, pages: Int): String = withPages(" Доступні терміни", page, pages) - override def historyEntry(visit: HistoricVisit, page: Int, index: Int): String = - s"""⏱ ${formatDateTime(visit.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${visit.doctorName} - |${capitalizeFirstLetter(service)}: ${visit.service.name} - |${capitalizeFirstLetter(clinic)}: ${visit.clinicName} + override def historyEntry(event: Event, page: Int, index: Int): String = + s"""⏱ ${formatDateTime(event.date, locale)} + |${capitalizeFirstLetter(doctor)}: ${capitalizeFirstLetter(event.doctor.name)} ${capitalizeFirstLetter(event.doctor.lastname)} + |${capitalizeFirstLetter(service)}: ${event.title} + |${capitalizeFirstLetter(clinic)}: ${event.clinic.map(c => s"${capitalizeFirstLetter(c.city)} - ${capitalizeFirstLetter(c.address)}").getOrElse("Telemedicine")} | |""".stripMargin override def historyHeader(page: Int, pages: Int): String = withPages(" Завершені візити", page, pages) - override def upcomingVisitEntry(visit: ReservedVisit, page: Int, index: Int): String = - s"""⏱ ${formatDateTime(visit.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${visit.doctorName} - |${capitalizeFirstLetter(service)}: ${visit.service.name} - |${capitalizeFirstLetter(clinic)}: ${visit.clinic.name} + override def reservedVisitEntry(event: Event, page: Int, index: Int): String = + s"""⏱ ${formatDateTime(event.date, locale)} + |${capitalizeFirstLetter(doctor)}: ${capitalizeFirstLetter(event.doctor.name)} ${capitalizeFirstLetter(event.doctor.lastname)} + |${capitalizeFirstLetter(service)}: ${event.title} + |${capitalizeFirstLetter(clinic)}: ${event.clinic.map(c => s"${capitalizeFirstLetter(c.city)} - ${capitalizeFirstLetter(c.address)}").getOrElse("Telemedicine")} | /cancel_$index | |""".stripMargin - override def upcomingVisitsHeader(page: Int, pages: Int): String = + override def reservedVisitsHeader(page: Int, pages: Int): String = withPages(" Зарезервовані візити", page, pages) override def bugsHeader(page: Int, pages: Int): String = @@ -257,13 +255,13 @@ object Ua extends Lang { |Ваші моніторинги були видалені. Будь ласка, /login знову і створіть нові моніторинги. """.stripMargin - override def availableTermEntry(term: AvailableVisitsTermPresentation, monitoring: Monitoring, index: Int): String = - s"""⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} + override def availableTermEntry(term: TermExt, monitoring: Monitoring, index: Int): String = + s"""⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic} |${capitalizeFirstLetter(city)}: ${monitoring.cityName} - |/reserve_${monitoring.recordId}_${term.scheduleId}_${minutesSinceBeginOf2018(term.visitDate.startDateTime)} + |/reserve_${monitoring.recordId}_${term.term.scheduleId}_${minutesSinceBeginOf2018(term.term.dateTimeFrom)} | |""".stripMargin @@ -284,13 +282,13 @@ object Ua extends Lang { | | Створити новий моніторінг /book""".stripMargin - override def appointmentIsBooked(term: AvailableVisitsTermPresentation, monitoring: Monitoring): String = + override def appointmentIsBooked(term: TermExt, monitoring: Monitoring): String = s"""👍 Ми зерезевували візит для вас! | - |⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} - |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} + |⏱ ${formatDateTime(term.term.dateTimeFrom, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.term.doctor.firstName} ${term.term.doctor.lastName} |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} - |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + |${capitalizeFirstLetter(clinic)}: ${term.term.clinic} |${capitalizeFirstLetter(city)}: ${monitoring.cityName}""".stripMargin override def maximumMonitoringsLimitExceeded: String = "Максимальна кількість моніторінгів 10" diff --git a/server/src/main/scala/com/lbs/server/repository/DataRepository.scala b/server/src/main/scala/com/lbs/server/repository/DataRepository.scala index 7b722aa..df43b90 100644 --- a/server/src/main/scala/com/lbs/server/repository/DataRepository.scala +++ b/server/src/main/scala/com/lbs/server/repository/DataRepository.scala @@ -1,13 +1,12 @@ package com.lbs.server.repository -import java.time.ZonedDateTime - import com.lbs.server.repository.model.{CityHistory, ClinicHistory, Credentials, DoctorHistory, JLong, Monitoring, ServiceHistory, Settings, Source, SystemUser} -import javax.persistence.EntityManager import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Repository +import java.time.ZonedDateTime +import javax.persistence.EntityManager import scala.collection.JavaConverters._ @Repository diff --git a/server/src/main/scala/com/lbs/server/repository/model/CityHistory.scala b/server/src/main/scala/com/lbs/server/repository/model/CityHistory.scala index 8b644d2..c3c946a 100644 --- a/server/src/main/scala/com/lbs/server/repository/model/CityHistory.scala +++ b/server/src/main/scala/com/lbs/server/repository/model/CityHistory.scala @@ -2,9 +2,7 @@ package com.lbs.server.repository.model import java.time.ZonedDateTime - import javax.persistence.{Access, AccessType, Column, Entity} - import scala.beans.BeanProperty @Entity diff --git a/server/src/main/scala/com/lbs/server/repository/model/ClinicHistory.scala b/server/src/main/scala/com/lbs/server/repository/model/ClinicHistory.scala index 57b0feb..30e9ddd 100644 --- a/server/src/main/scala/com/lbs/server/repository/model/ClinicHistory.scala +++ b/server/src/main/scala/com/lbs/server/repository/model/ClinicHistory.scala @@ -2,9 +2,7 @@ package com.lbs.server.repository.model import java.time.ZonedDateTime - import javax.persistence.{Access, AccessType, Column, Entity} - import scala.beans.BeanProperty @Entity diff --git a/server/src/main/scala/com/lbs/server/repository/model/Credentials.scala b/server/src/main/scala/com/lbs/server/repository/model/Credentials.scala index 007eedc..4fa9da7 100644 --- a/server/src/main/scala/com/lbs/server/repository/model/Credentials.scala +++ b/server/src/main/scala/com/lbs/server/repository/model/Credentials.scala @@ -2,7 +2,6 @@ package com.lbs.server.repository.model import javax.persistence._ - import scala.beans.BeanProperty @Entity diff --git a/server/src/main/scala/com/lbs/server/repository/model/DoctorHistory.scala b/server/src/main/scala/com/lbs/server/repository/model/DoctorHistory.scala index aa32d11..3fdc903 100644 --- a/server/src/main/scala/com/lbs/server/repository/model/DoctorHistory.scala +++ b/server/src/main/scala/com/lbs/server/repository/model/DoctorHistory.scala @@ -2,9 +2,7 @@ package com.lbs.server.repository.model import java.time.ZonedDateTime - import javax.persistence.{Access, AccessType, Column, Entity} - import scala.beans.BeanProperty @Entity 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 1e6c892..d2618e2 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 @@ -2,9 +2,7 @@ package com.lbs.server.repository.model import java.time.{LocalTime, ZonedDateTime} - import javax.persistence.{Access, AccessType, Column, Entity} - import scala.beans.BeanProperty @Entity diff --git a/server/src/main/scala/com/lbs/server/repository/model/RecordId.scala b/server/src/main/scala/com/lbs/server/repository/model/RecordId.scala index 4175d11..9dcf75e 100644 --- a/server/src/main/scala/com/lbs/server/repository/model/RecordId.scala +++ b/server/src/main/scala/com/lbs/server/repository/model/RecordId.scala @@ -2,7 +2,6 @@ package com.lbs.server.repository.model import javax.persistence._ - import scala.beans.BeanProperty @Access(AccessType.FIELD) diff --git a/server/src/main/scala/com/lbs/server/repository/model/ServiceHistory.scala b/server/src/main/scala/com/lbs/server/repository/model/ServiceHistory.scala index 0c04c0e..e0a810e 100644 --- a/server/src/main/scala/com/lbs/server/repository/model/ServiceHistory.scala +++ b/server/src/main/scala/com/lbs/server/repository/model/ServiceHistory.scala @@ -2,9 +2,7 @@ package com.lbs.server.repository.model import java.time.ZonedDateTime - import javax.persistence.{Access, AccessType, Column, Entity} - import scala.beans.BeanProperty @Entity diff --git a/server/src/main/scala/com/lbs/server/repository/model/Settings.scala b/server/src/main/scala/com/lbs/server/repository/model/Settings.scala index f6e5139..8646a80 100644 --- a/server/src/main/scala/com/lbs/server/repository/model/Settings.scala +++ b/server/src/main/scala/com/lbs/server/repository/model/Settings.scala @@ -2,7 +2,6 @@ package com.lbs.server.repository.model import javax.persistence.{Access, AccessType, Column, Entity} - import scala.beans.BeanProperty @Entity diff --git a/server/src/main/scala/com/lbs/server/repository/model/Source.scala b/server/src/main/scala/com/lbs/server/repository/model/Source.scala index bf8d8c3..2337f9c 100644 --- a/server/src/main/scala/com/lbs/server/repository/model/Source.scala +++ b/server/src/main/scala/com/lbs/server/repository/model/Source.scala @@ -2,7 +2,6 @@ package com.lbs.server.repository.model import javax.persistence._ - import scala.beans.BeanProperty @Entity diff --git a/server/src/main/scala/com/lbs/server/repository/model/SystemUser.scala b/server/src/main/scala/com/lbs/server/repository/model/SystemUser.scala index 2a5836d..8b2be82 100644 --- a/server/src/main/scala/com/lbs/server/repository/model/SystemUser.scala +++ b/server/src/main/scala/com/lbs/server/repository/model/SystemUser.scala @@ -2,7 +2,6 @@ package com.lbs.server.repository.model import javax.persistence._ - import scala.beans.BeanProperty @Entity 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 4dd3e3b..2959e54 100644 --- a/server/src/main/scala/com/lbs/server/service/ApiService.scala +++ b/server/src/main/scala/com/lbs/server/service/ApiService.scala @@ -1,18 +1,18 @@ package com.lbs.server.service -import java.time.{LocalTime, ZonedDateTime} - import cats.instances.either._ import com.lbs.api.LuxmedApi +import com.lbs.api.http.Session import com.lbs.api.json.model._ import com.lbs.server.ThrowableOr -import com.lbs.server.util.ServerModelConverters._ import org.jasypt.util.text.TextEncryptor import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import scalaj.http.HttpResponse +import java.time.{LocalDateTime, LocalTime, ZonedDateTime} + @Service class ApiService extends SessionSupport { @@ -23,161 +23,100 @@ class ApiService extends SessionSupport { private val luxmedApi = new LuxmedApi[ThrowableOr] - def getAllCities(accountId: Long): ThrowableOr[List[IdName]] = + def getAllCities(accountId: Long): ThrowableOr[List[DictionaryCity]] = withSession(accountId) { session => - luxmedApi.reservationFilter(session.accessToken, session.tokenType).map(_.cities) + luxmedApi.dictionaryCities(session) + } - def getAllClinics(accountId: Long, cityId: Long): ThrowableOr[List[IdName]] = + def getAllFacilities(accountId: Long, cityId: Long, serviceVariantId: Long): ThrowableOr[List[IdName]] = withSession(accountId) { session => - luxmedApi.reservationFilter(session.accessToken, - session.tokenType, cityId = Some(cityId)).map(_.clinics) + luxmedApi.dictionaryFacilitiesAndDoctors(session, cityId = Some(cityId), serviceVariantId = Some(serviceVariantId)).map(_.facilities) } - def getAllServices(accountId: Long, cityId: Long, clinicId: Option[Long]): ThrowableOr[List[IdName]] = + def getAllServices(accountId: Long): ThrowableOr[List[DictionaryServiceVariants]] = withSession(accountId) { session => - luxmedApi.reservationFilter(session.accessToken, - session.tokenType, cityId = Some(cityId), - clinicId = clinicId).map(_.services) + luxmedApi.dictionaryServiceVariants(session).map(s => s.flatMap(_.flatten.filterNot(_.children.nonEmpty))) } - def getAllDoctors(accountId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long): ThrowableOr[List[IdName]] = + def getAllDoctors(accountId: Long, cityId: Long, serviceVariantId: Long): ThrowableOr[List[Doctor]] = withSession(accountId) { session => - luxmedApi.reservationFilter(session.accessToken, - session.tokenType, cityId = Some(cityId), - clinicId = clinicId, serviceId = Some(serviceId)).map(_.doctors) + luxmedApi.dictionaryFacilitiesAndDoctors(session, cityId = Some(cityId), serviceVariantId = Some(serviceVariantId)).map(_.doctors) } - def getPayers(accountId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long): ThrowableOr[(Option[IdName], Seq[IdName])] = + def getAvailableTerms(accountId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long, doctorId: Option[Long], + fromDate: LocalDateTime, toDate: LocalDateTime, timeFrom: LocalTime, timeTo: LocalTime, + languageId: Long = 10): ThrowableOr[List[TermExt]] = withSession(accountId) { session => - val reservationFilterResponse = luxmedApi.reservationFilter(session.accessToken, - session.tokenType, cityId = Some(cityId), - clinicId = clinicId, serviceId = Some(serviceId)) - reservationFilterResponse.map { response => - response.defaultPayer -> response.payers - } - } - - 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, - languageId: Long = 10, findFirstFreeTerm: Boolean = false): ThrowableOr[List[AvailableVisitsTermPresentation]] = - withSession(accountId) { session => - val termsEither = luxmedApi.availableTerms(session.accessToken, session.tokenType, payerId, cityId, clinicId, serviceId, doctorId, - fromDate, toDate, languageId = languageId, findFirstFreeTerm = findFirstFreeTerm).map(_.availableVisitsTermPresentation) + val termsEither = luxmedApi.termsIndex(session, cityId, clinicId, serviceId, doctorId, + fromDate, toDate, languageId = languageId) + .map(termsIndexResponse => termsIndexResponse.termsForService.termsForDays + .flatMap(_.terms.map(term => TermExt(termsIndexResponse.termsForService.additionalData, term))) + ) termsEither.map { terms => terms.filter { term => - val time = term.visitDate.startDateTime.toLocalTime - time == timeFrom || time == timeTo || (time.isAfter(timeFrom) && time.isBefore(timeTo)) + val time = term.term.dateTimeFrom.toLocalTime + val date = term.term.dateTimeFrom + (doctorId.isEmpty || doctorId.contains(term.term.doctor.id)) && + (clinicId.isEmpty || clinicId.contains(term.term.clinicId)) && + (time == timeFrom || time == timeTo || (time.isAfter(timeFrom) && time.isBefore(timeTo))) && + (date == fromDate || date == toDate || (date.isAfter(fromDate) && date.isBefore(toDate))) } } } - def temporaryReservation(accountId: Long, temporaryReservationRequest: TemporaryReservationRequest, valuationsRequest: ValuationsRequest): ThrowableOr[(TemporaryReservationResponse, ValuationsResponse)] = + def reservationLockterm(accountId: Long, xsrfToken: XsrfToken, reservationLocktermRequest: ReservationLocktermRequest): ThrowableOr[ReservationLocktermResponse] = withSession(accountId) { session => - for { - temporaryReservation <- luxmedApi.temporaryReservation(session.accessToken, session.tokenType, temporaryReservationRequest) - valuationsResponse <- luxmedApi.valuations(session.accessToken, session.tokenType, valuationsRequest) - } yield temporaryReservation -> valuationsResponse + luxmedApi.reservationLockterm(session, xsrfToken, reservationLocktermRequest) } - def deleteTemporaryReservation(accountId: Long, temporaryReservationId: Long): ThrowableOr[HttpResponse[String]] = + def deleteTemporaryReservation(accountId: Long, xsrfToken: XsrfToken, temporaryReservationId: Long): ThrowableOr[Unit] = withSession(accountId) { session => - luxmedApi.deleteTemporaryReservation(session.accessToken, session.tokenType, temporaryReservationId) + luxmedApi.deleteTemporaryReservation(session, xsrfToken, temporaryReservationId) } - def reservation(accountId: Long, reservationRequest: ReservationRequest): ThrowableOr[ReservationResponse] = + def reservationConfirm(accountId: Long, xsrfToken: XsrfToken, reservationConfirmRequest: ReservationConfirmRequest): ThrowableOr[ReservationConfirmResponse] = withSession(accountId) { session => - luxmedApi.reservation(session.accessToken, session.tokenType, reservationRequest) + luxmedApi.reservationConfirm(session, xsrfToken, reservationConfirmRequest) } - def reserveVisit(accountId: Long, term: AvailableVisitsTermPresentation): ThrowableOr[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): ThrowableOr[HttpResponse[String]] = + def reservationChangeTerm(accountId: Long, xsrfToken: XsrfToken, reservationChangetermRequest: ReservationChangetermRequest): ThrowableOr[ReservationConfirmResponse] = withSession(accountId) { session => - luxmedApi.canTermBeChanged(session.accessToken, session.tokenType, reservationId) + luxmedApi.reservationChangeTerm(session, xsrfToken, reservationChangetermRequest) } - - def detailToChangeTerm(accountId: Long, reservationId: Long): ThrowableOr[ChangeTermDetailsResponse] = + def history(accountId: Long, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1), + toDate: ZonedDateTime = ZonedDateTime.now()): ThrowableOr[List[Event]] = withSession(accountId) { session => - luxmedApi.detailToChangeTerm(session.accessToken, session.tokenType, reservationId) + luxmedApi.events(session, fromDate, toDate).map(_.events.filter(_.status == "Realized")) } - def temporaryReservationToChangeTerm(accountId: Long, reservationId: Long, temporaryReservationRequest: TemporaryReservationRequest, valuationsRequest: ValuationsRequest): ThrowableOr[(TemporaryReservationResponse, ValuationsResponse)] = + def reserved(accountId: Long, fromDate: ZonedDateTime = ZonedDateTime.now(), + toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3)): ThrowableOr[List[Event]] = withSession(accountId) { session => - for { - temporaryReservation <- luxmedApi.temporaryReservationToChangeTerm(session.accessToken, session.tokenType, reservationId, temporaryReservationRequest) - valuationsResponse <- luxmedApi.valuationToChangeTerm(session.accessToken, session.tokenType, reservationId, valuationsRequest) - } yield temporaryReservation -> valuationsResponse - } - - def valuationToChangeTerm(accountId: Long, reservationId: Long, valuationsRequest: ValuationsRequest): ThrowableOr[ValuationsResponse] = - withSession(accountId) { session => - luxmedApi.valuationToChangeTerm(session.accessToken, session.tokenType, reservationId, valuationsRequest) - } - - def changeTerm(accountId: Long, reservationId: Long, reservationRequest: ReservationRequest): ThrowableOr[ChangeTermResponse] = - withSession(accountId) { session => - luxmedApi.changeTerm(session.accessToken, session.tokenType, reservationId, reservationRequest) - } - - def updateReservedVisit(accountId: Long, term: AvailableVisitsTermPresentation): ThrowableOr[ChangeTermResponse] = { - val reservedVisitMaybe = reservedVisits(accountId, toDate = ZonedDateTime.now().plusMonths(6)).map(_.find(_.service.id == term.serviceId)) - reservedVisitMaybe match { - case Right(Some(reservedVisit: ReservedVisit)) => - val reservationId = reservedVisit.reservationId - 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(s"Term for reservation [$reservationId] can't be changed") - case Left(ex) => - Left(ex) - case _ => - left(s"Existing reservation for service [${term.serviceId}] not found. Nothing to update") - } - } - - def visitsHistory(accountId: Long, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1), - toDate: ZonedDateTime = ZonedDateTime.now(), page: Int = 1, pageSize: Int = 100): ThrowableOr[List[HistoricVisit]] = - withSession(accountId) { session => - luxmedApi.visitsHistory(session.accessToken, session.tokenType, fromDate, toDate, page, pageSize).map(_.historicVisits) - } - - def reservedVisits(accountId: Long, fromDate: ZonedDateTime = ZonedDateTime.now(), - toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3)): ThrowableOr[List[ReservedVisit]] = - withSession(accountId) { session => - luxmedApi.reservedVisits(session.accessToken, session.tokenType, fromDate, toDate).map(_.reservedVisits) + luxmedApi.events(session, fromDate, toDate).map(_.events.filter(_.status == "Reserved")) } def deleteReservation(accountId: Long, reservationId: Long): ThrowableOr[HttpResponse[String]] = withSession(accountId) { session => - luxmedApi.deleteReservation(session.accessToken, session.tokenType, reservationId) + luxmedApi.reservationDelete(session, reservationId) } - def login(username: String, password: String): ThrowableOr[LoginResponse] = { - luxmedApi.login(username, textEncryptor.decrypt(password)) + override def fullLogin(username: String, encryptedPassword: String): ThrowableOr[Session] = { + val password = textEncryptor.decrypt(encryptedPassword) + for { + r1 <- luxmedApi.login(username, password) + tmpSession = Session(r1.body.accessToken, r1.body.accessToken, r1.cookies) + r2 <- luxmedApi.loginToApp(tmpSession) + cookies = r1.cookies ++ r2.cookies + accessToken = r1.body.accessToken + tokenType = r1.body.tokenType + } yield Session(accessToken, tokenType, cookies) } - private def left(msg: String) = Left(new RuntimeException(msg)) - + def getXsrfToken(accountId: Long): ThrowableOr[XsrfToken] = { + withSession(accountId) { session => + luxmedApi.getForgeryToken(session).map(ft => XsrfToken(ft.body.token, ft.cookies)) + } + } } diff --git a/server/src/main/scala/com/lbs/server/service/DataService.scala b/server/src/main/scala/com/lbs/server/service/DataService.scala index 6494682..c404a01 100644 --- a/server/src/main/scala/com/lbs/server/service/DataService.scala +++ b/server/src/main/scala/com/lbs/server/service/DataService.scala @@ -1,18 +1,18 @@ package com.lbs.server.service -import java.time.ZonedDateTime - import com.lbs.api.json.model.IdName import com.lbs.bot.model.MessageSource import com.lbs.server.conversation.Book.BookingData import com.lbs.server.repository.DataRepository import com.lbs.server.repository.model._ import com.lbs.server.util.ServerModelConverters._ -import javax.transaction.Transactional import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service +import java.time.ZonedDateTime +import javax.transaction.Transactional + @Service class DataService { 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 98d8577..a448d20 100644 --- a/server/src/main/scala/com/lbs/server/service/MonitoringService.scala +++ b/server/src/main/scala/com/lbs/server/service/MonitoringService.scala @@ -1,23 +1,24 @@ package com.lbs.server.service -import com.lbs.api.exception.{InvalidLoginOrPasswordException, ServiceIsAlreadyBookedException} -import com.lbs.api.json.model.AvailableVisitsTermPresentation +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.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 org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service -import java.time.ZonedDateTime +import java.time.{LocalDateTime, ZonedDateTime} import java.util.concurrent.ScheduledFuture import javax.annotation.PostConstruct import scala.collection.mutable import scala.concurrent.duration._ -import scala.util.{Failure, Random} +import scala.util.Random @Service class MonitoringService extends Logger { @@ -49,7 +50,7 @@ class MonitoringService extends Logger { private var checkedOn: ZonedDateTime = _ - def notifyUserAboutTerms(terms: Seq[AvailableVisitsTermPresentation], monitoring: Monitoring): Unit = { + def notifyUserAboutTerms(terms: Seq[TermExt], monitoring: Monitoring): Unit = { deactivateMonitoring(monitoring.accountId, monitoring.recordId) val fiveTerms = terms.take(5).zipWithIndex //send only 5 closest terms @@ -65,9 +66,9 @@ class MonitoringService extends Logger { private def monitor(monitoring: Monitoring): Unit = { debug(s"Looking for available terms. Monitoring [#${monitoring.recordId}]") - val dateFrom = optimizeDateFrom(monitoring.dateFrom, monitoring.offset) - val termsEither = apiService.getAvailableTerms(monitoring.accountId, monitoring.payerId, monitoring.cityId, monitoring.clinicId, monitoring.serviceId, - monitoring.doctorId, dateFrom, Some(monitoring.dateTo), timeFrom = monitoring.timeFrom, timeTo = monitoring.timeTo) + val dateFrom = optimizeDateFrom(monitoring.dateFrom.toLocalDateTime, monitoring.offset) + val termsEither = apiService.getAvailableTerms(monitoring.accountId, monitoring.cityId, monitoring.clinicId, monitoring.serviceId, + monitoring.doctorId, dateFrom, monitoring.dateTo.toLocalDateTime, timeFrom = monitoring.timeFrom, timeTo = monitoring.timeTo) termsEither match { case Right(terms) => if (terms.nonEmpty) { @@ -92,8 +93,8 @@ class MonitoringService extends Logger { } } - private def optimizeDateFrom(date: ZonedDateTime, offset: Int) = { - val nowWithOffset = ZonedDateTime.now().plusHours(offset) + private def optimizeDateFrom(date: LocalDateTime, offset: Int) = { + val nowWithOffset = LocalDateTime.now().plusHours(offset) if (date.isBefore(nowWithOffset)) nowWithOffset else date } @@ -144,13 +145,19 @@ class MonitoringService extends Logger { dbChecker.schedule(updateMonitorings(), 1.minute) } - private def bookAppointment(term: AvailableVisitsTermPresentation, monitoring: Monitoring, rebookIfExists: Boolean): Unit = { - apiService.reserveVisit(monitoring.accountId, term).toTry.recoverWith { - case _: ServiceIsAlreadyBookedException if rebookIfExists => + private def bookAppointment(term: TermExt, monitoring: Monitoring, rebookIfExists: Boolean): Unit = { + val bookingResult = for { + xsrfToken <- apiService.getXsrfToken(monitoring.accountId) + reservationLocktermResponse <- apiService.reservationLockterm(monitoring.accountId, xsrfToken, term.mapTo[ReservationLocktermRequest]) + temporaryReservationId = reservationLocktermResponse.value.temporaryReservationId + response <- if (reservationLocktermResponse.value.changeTermAvailable && rebookIfExists) { info(s"Service [${monitoring.serviceName}] is already booked. Trying to update term") - apiService.updateReservedVisit(monitoring.accountId, term).toTry - case ex => Failure(ex) - }.toEither match { + bookOrUnlockTerm(monitoring.accountId, xsrfToken, temporaryReservationId, apiService.reservationChangeTerm(_, xsrfToken, (reservationLocktermResponse, term).mapTo[ReservationChangetermRequest])) + } else { + bookOrUnlockTerm(monitoring.accountId, xsrfToken, temporaryReservationId, apiService.reservationConfirm(_, xsrfToken, (reservationLocktermResponse, term).mapTo[ReservationConfirmRequest])) + } + } yield response + bookingResult match { case Right(_) => bot.sendMessage(monitoring.source, lang(monitoring.userId).appointmentIsBooked(term, monitoring)) deactivateMonitoring(monitoring.accountId, monitoring.recordId) @@ -159,6 +166,15 @@ class MonitoringService extends Logger { } } + private def bookOrUnlockTerm[T](accountId: Long, xsrfToken: XsrfToken, temporaryReservationId: Long, fn: (Long) => Either[Throwable, T]): Either[Throwable, T] = { + fn(accountId) match { + case r@Left(_) => + apiService.deleteTemporaryReservation(accountId, xsrfToken, temporaryReservationId) + r + case r => r + } + } + def deactivateMonitoring(accountId: JLong, monitoringId: JLong): Unit = { val activeMonitoringMaybe = activeMonitorings.remove(monitoringId) activeMonitoringMaybe match { @@ -200,11 +216,11 @@ class MonitoringService extends Logger { val monitoringMaybe = dataService.findMonitoring(accountId, monitoringId) monitoringMaybe match { case Some(monitoring) => - val termsEither = apiService.getAvailableTerms(monitoring.accountId, monitoring.payerId, monitoring.cityId, monitoring.clinicId, monitoring.serviceId, - monitoring.doctorId, monitoring.dateFrom, Some(monitoring.dateTo), timeFrom = monitoring.timeFrom, timeTo = monitoring.timeTo) + val termsEither = apiService.getAvailableTerms(monitoring.accountId, monitoring.cityId, monitoring.clinicId, monitoring.serviceId, + monitoring.doctorId, monitoring.dateFrom.toLocalDateTime, monitoring.dateTo.toLocalDateTime, timeFrom = monitoring.timeFrom, timeTo = monitoring.timeTo) termsEither match { case Right(terms) => - val termMaybe = terms.find(term => term.scheduleId == scheduleId && minutesSinceBeginOf2018(term.visitDate.startDateTime) == time) + val termMaybe = terms.find(term => term.term.scheduleId == scheduleId && minutesSinceBeginOf2018(term.term.dateTimeFrom) == time) termMaybe match { case Some(term) => bookAppointment(term, monitoring, rebookIfExists = true) diff --git a/server/src/main/scala/com/lbs/server/service/SessionSupport.scala b/server/src/main/scala/com/lbs/server/service/SessionSupport.scala index 0fd84bc..120ff77 100644 --- a/server/src/main/scala/com/lbs/server/service/SessionSupport.scala +++ b/server/src/main/scala/com/lbs/server/service/SessionSupport.scala @@ -2,7 +2,7 @@ package com.lbs.server.service import com.lbs.api.exception.SessionExpiredException -import com.lbs.api.json.model.LoginResponse +import com.lbs.api.http.Session import com.lbs.common.{Logger, ParametrizedLock} import com.lbs.server.ThrowableOr import com.lbs.server.exception.UserNotFoundException @@ -11,9 +11,7 @@ import scala.collection.mutable trait SessionSupport extends Logger { - case class Session(accessToken: String, tokenType: String) - - def login(username: String, password: String): ThrowableOr[LoginResponse] + def fullLogin(username: String, password: String): ThrowableOr[Session] protected def dataService: DataService @@ -27,9 +25,7 @@ trait SessionSupport extends Logger { def auth: ThrowableOr[Session] = { val credentialsMaybe = dataService.getCredentials(accountId) credentialsMaybe match { - case Some(credentials) => - val loginResponse = login(credentials.username, credentials.password) - loginResponse.map(r => Session(r.accessToken, r.tokenType)) + case Some(credentials) => fullLogin(credentials.username, credentials.password) case None => Left(UserNotFoundException(accountId)) } } @@ -67,8 +63,8 @@ trait SessionSupport extends Logger { } yield result } - def addSession(accountId: Long, accessToken: String, tokenType: String): Unit = + def addSession(accountId: Long, session: Session): Unit = lock.obtainLock(accountId).synchronized { - sessions.put(accountId, Session(accessToken, tokenType)) + sessions.put(accountId, session) } } 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 143ace6..e68829c 100644 --- a/server/src/main/scala/com/lbs/server/util/package.scala +++ b/server/src/main/scala/com/lbs/server/util/package.scala @@ -1,10 +1,6 @@ package com.lbs.server -import java.time._ -import java.time.format.DateTimeFormatter -import java.util.Locale - import com.lbs.api.json.model._ import com.lbs.bot.model.Command import com.lbs.common.ModelConverters @@ -12,6 +8,9 @@ import com.lbs.server.conversation.Book.BookingData import com.lbs.server.conversation.Login.UserId import com.lbs.server.repository.model.{History, Monitoring} +import java.time._ +import java.time.format.DateTimeFormatter +import java.util.Locale import scala.language.{higherKinds, implicitConversions} import scala.util.Try @@ -38,8 +37,8 @@ package object util { serviceName = bookingData.serviceId.name, doctorId = bookingData.doctorId.optionalId, doctorName = bookingData.doctorId.name, - dateFrom = bookingData.dateFrom, - dateTo = bookingData.dateTo, + dateFrom = bookingData.dateFrom.atZone(DateTimeUtil.Zone), + dateTo = bookingData.dateTo.atZone(DateTimeUtil.Zone), timeFrom = bookingData.timeFrom, timeTo = bookingData.timeTo, autobook = bookingData.autobook, @@ -48,49 +47,73 @@ package object util { ) } - implicit val AvailableVisitsTermPresentationToTemporaryReservationRequestConverter: - ObjectConverter[AvailableVisitsTermPresentation, TemporaryReservationRequest] = - (term: AvailableVisitsTermPresentation) => { - TemporaryReservationRequest( - clinicId = term.clinic.id, + implicit val ReservationLocktermResponseAndTermToReservationConfirmRequestConverter: + ObjectConverter[(ReservationLocktermResponse, TermExt), ReservationConfirmRequest] = + (data: (ReservationLocktermResponse, TermExt)) => { + val (reservationLocktermResponse, termExt) = data + val term = termExt.term + ReservationConfirmRequest( + date = term.dateTimeFrom.minusHours(2).toString + ":00.000Z", doctorId = term.doctor.id, - payerDetailsList = term.payerDetailsList, - referralRequiredByService = term.referralRequiredByService, + facilityId = term.clinicId, roomId = term.roomId, - serviceId = term.serviceId, - startDateTime = term.visitDate.startDateTime + scheduleId = term.scheduleId, + serviceVariantId = term.serviceId, + temporaryReservationId = reservationLocktermResponse.value.temporaryReservationId, + timeFrom = term.dateTimeFrom.toLocalTime, + valuation = reservationLocktermResponse.value.valuations.head ) } - implicit val TmpReservationIdWithValuationsToReservationRequestConverter: - ObjectConverter[(Long, VisitTermVariant, AvailableVisitsTermPresentation), ReservationRequest] = - (any: (Long, VisitTermVariant, AvailableVisitsTermPresentation)) => { - val (tmpReservationId, valuations, term) = any - ReservationRequest( - clinicId = term.clinic.id, - doctorId = term.doctor.id, - payerData = valuations.valuationDetail.payerData, - roomId = term.roomId, - serviceId = term.serviceId, - startDateTime = term.visitDate.startDateTime, - temporaryReservationId = tmpReservationId + implicit val ReservationLocktermResponseAndTermToReservationChangeTermRequestConverter: + ObjectConverter[(ReservationLocktermResponse, TermExt), ReservationChangetermRequest] = + (data: (ReservationLocktermResponse, TermExt)) => { + val (reservationLocktermResponse, termExt) = data + val term = termExt.term + val existingReservationId = reservationLocktermResponse.value.relatedVisits.head.reservationId + ReservationChangetermRequest( + existingReservationId = existingReservationId, + term = NewTerm( + date = term.dateTimeFrom.minusHours(2).toString + ":00.000Z", + doctorId = term.doctor.id, + facilityId = term.clinicId, + parentReservationId = existingReservationId, + referralRequired = reservationLocktermResponse.value.valuations.head.isReferralRequired, + roomId = term.roomId, + scheduleId = term.scheduleId, + serviceVariantId = term.serviceId, + temporaryReservationId = reservationLocktermResponse.value.temporaryReservationId, + timeFrom = term.dateTimeFrom.toLocalTime, + valuation = reservationLocktermResponse.value.valuations.head + ) ) } - implicit val AvailableVisitsTermPresentationToValuationRequestConverter: - ObjectConverter[AvailableVisitsTermPresentation, ValuationsRequest] = - (term: AvailableVisitsTermPresentation) => { - ValuationsRequest( - clinicId = term.clinic.id, + implicit val TermToReservationLocktermRequest: + ObjectConverter[TermExt, ReservationLocktermRequest] = + termExt => { + val term = termExt.term + val additionalData = termExt.additionalData + ReservationLocktermRequest( + date = term.dateTimeFrom.minusHours(2).toString + ":00.000Z", + doctor = term.doctor, doctorId = term.doctor.id, - payerDetailsList = term.payerDetailsList, - referralRequiredByService = term.referralRequiredByService, + facilityId = term.clinicId, + impedimentText = term.impedimentText, + isAdditional = term.isAdditional, + isImpediment = term.isImpediment, + isPreparationRequired = additionalData.isPreparationRequired, + isTelemedicine = term.isTelemedicine, + preparationItems = additionalData.preparationItems, roomId = term.roomId, - serviceId = term.serviceId, - startDateTime = term.visitDate.startDateTime + scheduleId = term.scheduleId, + serviceVariantId = term.serviceId, + timeFrom = term.dateTimeFrom.toLocalTime.toString, + timeTo = term.dateTimeTo.toLocalTime.toString ) } + implicit val HistoryToIdNameConverter: ObjectConverter[History, IdName] = (history: History) => IdName(history.id, history.name) } @@ -124,6 +147,8 @@ package object util { } object DateTimeUtil { + val Zone: ZoneId = ZoneId.of("Europe/Warsaw") + private val DateFormat: Locale => DateTimeFormatter = locale => DateTimeFormatter.ofPattern("dd MMM yyyy", locale) private val DateShortFormat = DateTimeFormatter.ofPattern("dd-MM") @@ -132,21 +157,25 @@ package object util { private val DateTimeFormat: Locale => DateTimeFormatter = locale => DateTimeFormatter.ofPattern("EEE',' dd MMM yyyy',' HH:mm", locale) + def formatDate(date: LocalDateTime, locale: Locale): String = date.format(DateFormat(locale)) + def formatDate(date: ZonedDateTime, locale: Locale): String = date.format(DateFormat(locale)) - def formatDateShort(date: ZonedDateTime): String = date.format(DateShortFormat) + def formatDateShort(date: LocalDateTime): String = date.format(DateShortFormat) def formatTime(time: LocalTime): String = time.format(TimeFormat) def formatDateTime(date: ZonedDateTime, locale: Locale): String = date.format(DateTimeFormat(locale)) - private val EpochMinutesTillBeginOf2018: Long = epochMinutes(ZonedDateTime.of(2018, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault())) + def formatDateTime(date: LocalDateTime, locale: Locale): String = date.format(DateTimeFormat(locale)) - def epochMinutes(time: ZonedDateTime): Long = time.toInstant.getEpochSecond / 60 + private val EpochMinutesTillBeginOf2022: Long = epochMinutes(LocalDateTime.of(2022, 1, 1, 0, 0, 0, 0)) - def minutesSinceBeginOf2018(time: ZonedDateTime): Long = epochMinutes(time) - EpochMinutesTillBeginOf2018 + def epochMinutes(time: LocalDateTime): Long = time.toInstant(ZonedDateTime.now().getOffset).getEpochSecond / 60 - def applyDayMonth(dayMonthStr: String, date: ZonedDateTime): ZonedDateTime = { + def minutesSinceBeginOf2018(time: LocalDateTime): Long = epochMinutes(time) - EpochMinutesTillBeginOf2022 + + def applyDayMonth(dayMonthStr: String, date: LocalDateTime): LocalDateTime = { val dayMonth = MonthDay.parse(dayMonthStr, DateShortFormat) val newDate = date.withDayOfMonth(dayMonth.getDayOfMonth).withMonth(dayMonth.getMonthValue) diff --git a/server/src/test/scala/com/lbs/server/repository/DataRepositorySpec.scala b/server/src/test/scala/com/lbs/server/repository/DataRepositorySpec.scala index 799a0ab..8d6fa03 100644 --- a/server/src/test/scala/com/lbs/server/repository/DataRepositorySpec.scala +++ b/server/src/test/scala/com/lbs/server/repository/DataRepositorySpec.scala @@ -1,9 +1,6 @@ package com.lbs.server.repository -import java.time.ZonedDateTime - import com.lbs.server.repository.model.{CityHistory, ClinicHistory, Credentials, DoctorHistory, ServiceHistory} -import javax.persistence.EntityManager import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -13,6 +10,9 @@ import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import org.springframework.test.context.junit4.SpringRunner +import java.time.ZonedDateTime +import javax.persistence.EntityManager + object DataRepositorySpec { @TestConfiguration