From 68557d960a5ca7549fc48e4304142ab22f12afc6 Mon Sep 17 00:00:00 2001 From: Eugene Zadyra Date: Thu, 31 May 2018 00:28:58 +0200 Subject: [PATCH] Initial commit --- .gitignore | 122 ++++++ LICENSE | 21 ++ README.md | 22 ++ api/build.gradle | 6 + api/src/main/scala/com/lbs/api/ApiBase.scala | 44 +++ .../main/scala/com/lbs/api/LuxmedApi.scala | 167 +++++++++ .../scala/com/lbs/api/LuxmedApiAsync.scala | 95 +++++ .../lbs/api/exception/LuxmedException.scala | 28 ++ .../main/scala/com/lbs/api/http/package.scala | 101 +++++ .../com/lbs/api/json/JsonSerializer.scala | 69 ++++ .../json/model/AvailbaleTermsResponse.scala | 86 +++++ .../scala/com/lbs/api/json/model/IdName.scala | 28 ++ .../lbs/api/json/model/LoginResponse.scala | 34 ++ .../lbs/api/json/model/LuxmedBaseError.scala | 26 ++ .../api/json/model/LuxmedCompositeError.scala | 28 ++ .../com/lbs/api/json/model/LuxmedError.scala | 26 ++ .../com/lbs/api/json/model/PayerDetails.scala | 27 ++ .../model/ReservationFilterResponse.scala | 89 +++++ .../api/json/model/ReservationRequest.scala | 50 +++ .../api/json/model/ReservationResponse.scala | 46 +++ .../json/model/ReservedVisitsResponse.scala | 66 ++++ .../json/model/SerializableJsonObject.scala | 26 ++ .../model/TemporaryReservationRequest.scala | 65 ++++ .../model/TemporaryReservationResponse.scala | 27 ++ .../api/json/model/ValuationsRequest.scala | 65 ++++ .../api/json/model/ValuationsResponse.scala | 63 ++++ .../com/lbs/api/json/model/VisitDate.scala | 28 ++ .../json/model/VisitsHistoryResponse.scala | 99 +++++ api/src/main/scala/com/lbs/api/package.scala | 66 ++++ .../com/lbs/api/json/model/CommonSpec.scala | 43 +++ .../api/json/model/LoginResponseSpec.scala | 48 +++ .../model/ReservationFilterResponseSpec.scala | 106 ++++++ .../model/ReservedVisitsResponseSpec.scala | 84 +++++ .../TemporaryReservationRequestSpec.scala | 71 ++++ .../json/model/ValuationsRequestSpec.scala | 71 ++++ .../model/VisitsHistoryResponseSpec.scala | 119 ++++++ bot/build.gradle | 5 + bot/src/main/scala/com/lbs/bot/Bot.scala | 49 +++ bot/src/main/scala/com/lbs/bot/PollBot.scala | 38 ++ .../main/scala/com/lbs/bot/WebhookBot.scala | 30 ++ .../main/scala/com/lbs/bot/model/Button.scala | 38 ++ .../scala/com/lbs/bot/model/Command.scala | 28 ++ .../main/scala/com/lbs/bot/model/Event.scala | 26 ++ .../com/lbs/bot/model/InlineKeyboard.scala | 26 ++ .../com/lbs/bot/model/MessageSource.scala | 26 ++ .../lbs/bot/model/MessageSourceSystem.scala | 57 +++ bot/src/main/scala/com/lbs/bot/package.scala | 36 ++ .../com/lbs/bot/telegram/TelegramBot.scala | 51 +++ .../com/lbs/bot/telegram/TelegramClient.scala | 66 ++++ .../com/lbs/bot/telegram/TelegramEvent.scala | 29 ++ .../scala/com/lbs/bot/telegram/package.scala | 77 ++++ build.gradle | 47 +++ common/build.gradle | 5 + .../main/scala/com/lbs/common/Implicits.scala | 38 ++ .../main/scala/com/lbs/common/Logger.scala | 66 ++++ .../com/lbs/common/ModelConverters.scala | 47 +++ .../com/lbs/common/ParametrizedLock.scala | 32 ++ .../main/scala/com/lbs/common/Scheduler.scala | 41 ++ gradle.properties | 5 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54711 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++++++++ gradlew.bat | 84 +++++ server/build.gradle | 31 ++ server/src/main/resources/application.conf | 3 + server/src/main/resources/application.yml | 31 ++ server/src/main/resources/banner.txt | 6 + .../src/main/scala/com/lbs/server/Boot.scala | 35 ++ .../scala/com/lbs/server/BootConfig.scala | 148 ++++++++ .../scala/com/lbs/server/actor/Auth.scala | 104 ++++++ .../scala/com/lbs/server/actor/Book.scala | 342 +++++++++++++++++ .../main/scala/com/lbs/server/actor/Bug.scala | 135 +++++++ .../scala/com/lbs/server/actor/Chat.scala | 196 ++++++++++ .../com/lbs/server/actor/DatePicker.scala | 155 ++++++++ .../scala/com/lbs/server/actor/FSMData.scala | 26 ++ .../scala/com/lbs/server/actor/FSMState.scala | 26 ++ .../scala/com/lbs/server/actor/Help.scala | 50 +++ .../scala/com/lbs/server/actor/History.scala | 97 +++++ .../scala/com/lbs/server/actor/Login.scala | 126 +++++++ .../com/lbs/server/actor/Monitorings.scala | 113 ++++++ .../scala/com/lbs/server/actor/Pager.scala | 132 +++++++ .../scala/com/lbs/server/actor/Pagers.scala | 70 ++++ .../scala/com/lbs/server/actor/Router.scala | 85 +++++ .../scala/com/lbs/server/actor/SafeFSM.scala | 57 +++ .../scala/com/lbs/server/actor/Settings.scala | 103 +++++ .../com/lbs/server/actor/StaticData.scala | 133 +++++++ .../server/actor/StaticDataForBooking.scala | 69 ++++ .../lbs/server/actor/UnauthorizedHelp.scala | 50 +++ .../scala/com/lbs/server/actor/Visits.scala | 119 ++++++ .../scala/com/lbs/server/actor/package.scala | 34 ++ .../exception/UserNotFoundException.scala | 26 ++ .../main/scala/com/lbs/server/lang/En.scala | 351 ++++++++++++++++++ .../main/scala/com/lbs/server/lang/Lang.scala | 236 ++++++++++++ .../com/lbs/server/lang/Localizable.scala | 34 ++ .../com/lbs/server/lang/Localization.scala | 63 ++++ .../main/scala/com/lbs/server/lang/Ua.scala | 350 +++++++++++++++++ .../server/repository/DataRepository.scala | 192 ++++++++++ .../com/lbs/server/repository/model/Bug.scala | 68 ++++ .../server/repository/model/CityHistory.scala | 61 +++ .../repository/model/ClinicHistory.scala | 66 ++++ .../server/repository/model/Credentials.scala | 55 +++ .../repository/model/DoctorHistory.scala | 76 ++++ .../lbs/server/repository/model/History.scala | 31 ++ .../server/repository/model/Monitoring.scala | 132 +++++++ .../server/repository/model/RecordId.scala | 36 ++ .../repository/model/ServiceHistory.scala | 71 ++++ .../server/repository/model/Settings.scala | 55 +++ .../lbs/server/repository/model/Source.scala | 56 +++ .../server/repository/model/SystemUser.scala | 34 ++ .../lbs/server/repository/model/package.scala | 35 ++ .../com/lbs/server/service/ApiService.scala | 128 +++++++ .../com/lbs/server/service/DataService.scala | 163 ++++++++ .../server/service/MonitoringService.scala | 258 +++++++++++++ .../lbs/server/service/SessionSupport.scala | 92 +++++ .../scala/com/lbs/server/util/package.scala | 154 ++++++++ settings.gradle | 4 + 116 files changed, 8590 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 api/build.gradle create mode 100644 api/src/main/scala/com/lbs/api/ApiBase.scala create mode 100644 api/src/main/scala/com/lbs/api/LuxmedApi.scala create mode 100644 api/src/main/scala/com/lbs/api/LuxmedApiAsync.scala create mode 100644 api/src/main/scala/com/lbs/api/exception/LuxmedException.scala create mode 100644 api/src/main/scala/com/lbs/api/http/package.scala create mode 100644 api/src/main/scala/com/lbs/api/json/JsonSerializer.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/AvailbaleTermsResponse.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/IdName.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/LoginResponse.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/LuxmedBaseError.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/LuxmedCompositeError.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/LuxmedError.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/PayerDetails.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/ReservationFilterResponse.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/ReservationRequest.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/ReservationResponse.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/ReservedVisitsResponse.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/SerializableJsonObject.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/TemporaryReservationRequest.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/TemporaryReservationResponse.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/ValuationsRequest.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/ValuationsResponse.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/VisitDate.scala create mode 100644 api/src/main/scala/com/lbs/api/json/model/VisitsHistoryResponse.scala create mode 100644 api/src/main/scala/com/lbs/api/package.scala create mode 100644 api/src/test/scala/com/lbs/api/json/model/CommonSpec.scala create mode 100644 api/src/test/scala/com/lbs/api/json/model/LoginResponseSpec.scala create mode 100644 api/src/test/scala/com/lbs/api/json/model/ReservationFilterResponseSpec.scala create mode 100644 api/src/test/scala/com/lbs/api/json/model/ReservedVisitsResponseSpec.scala create mode 100644 api/src/test/scala/com/lbs/api/json/model/TemporaryReservationRequestSpec.scala create mode 100644 api/src/test/scala/com/lbs/api/json/model/ValuationsRequestSpec.scala create mode 100644 api/src/test/scala/com/lbs/api/json/model/VisitsHistoryResponseSpec.scala create mode 100644 bot/build.gradle create mode 100644 bot/src/main/scala/com/lbs/bot/Bot.scala create mode 100644 bot/src/main/scala/com/lbs/bot/PollBot.scala create mode 100644 bot/src/main/scala/com/lbs/bot/WebhookBot.scala create mode 100644 bot/src/main/scala/com/lbs/bot/model/Button.scala create mode 100644 bot/src/main/scala/com/lbs/bot/model/Command.scala create mode 100644 bot/src/main/scala/com/lbs/bot/model/Event.scala create mode 100644 bot/src/main/scala/com/lbs/bot/model/InlineKeyboard.scala create mode 100644 bot/src/main/scala/com/lbs/bot/model/MessageSource.scala create mode 100644 bot/src/main/scala/com/lbs/bot/model/MessageSourceSystem.scala create mode 100644 bot/src/main/scala/com/lbs/bot/package.scala create mode 100644 bot/src/main/scala/com/lbs/bot/telegram/TelegramBot.scala create mode 100644 bot/src/main/scala/com/lbs/bot/telegram/TelegramClient.scala create mode 100644 bot/src/main/scala/com/lbs/bot/telegram/TelegramEvent.scala create mode 100644 bot/src/main/scala/com/lbs/bot/telegram/package.scala create mode 100644 build.gradle create mode 100644 common/build.gradle create mode 100644 common/src/main/scala/com/lbs/common/Implicits.scala create mode 100644 common/src/main/scala/com/lbs/common/Logger.scala create mode 100644 common/src/main/scala/com/lbs/common/ModelConverters.scala create mode 100644 common/src/main/scala/com/lbs/common/ParametrizedLock.scala create mode 100644 common/src/main/scala/com/lbs/common/Scheduler.scala create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 server/build.gradle create mode 100644 server/src/main/resources/application.conf create mode 100644 server/src/main/resources/application.yml create mode 100644 server/src/main/resources/banner.txt create mode 100644 server/src/main/scala/com/lbs/server/Boot.scala create mode 100644 server/src/main/scala/com/lbs/server/BootConfig.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Auth.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Book.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Bug.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Chat.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/DatePicker.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/FSMData.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/FSMState.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Help.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/History.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Login.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Monitorings.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Pager.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Pagers.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Router.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/SafeFSM.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Settings.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/StaticData.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/StaticDataForBooking.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/UnauthorizedHelp.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/Visits.scala create mode 100644 server/src/main/scala/com/lbs/server/actor/package.scala create mode 100644 server/src/main/scala/com/lbs/server/exception/UserNotFoundException.scala create mode 100644 server/src/main/scala/com/lbs/server/lang/En.scala create mode 100644 server/src/main/scala/com/lbs/server/lang/Lang.scala create mode 100644 server/src/main/scala/com/lbs/server/lang/Localizable.scala create mode 100644 server/src/main/scala/com/lbs/server/lang/Localization.scala create mode 100644 server/src/main/scala/com/lbs/server/lang/Ua.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/DataRepository.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/Bug.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/CityHistory.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/ClinicHistory.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/Credentials.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/DoctorHistory.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/History.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/Monitoring.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/RecordId.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/ServiceHistory.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/Settings.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/Source.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/SystemUser.scala create mode 100644 server/src/main/scala/com/lbs/server/repository/model/package.scala create mode 100644 server/src/main/scala/com/lbs/server/service/ApiService.scala create mode 100644 server/src/main/scala/com/lbs/server/service/DataService.scala create mode 100644 server/src/main/scala/com/lbs/server/service/MonitoringService.scala create mode 100644 server/src/main/scala/com/lbs/server/service/SessionSupport.scala create mode 100644 server/src/main/scala/com/lbs/server/util/package.scala create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..19f5c64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,122 @@ +# Created by .ignore support plugin (hsz.mobi) +### Java template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +### Scala template +*.class +*.log +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ +cmake-build-release/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +### Gradle template +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +/.idea/ +/**/*.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..31f8735 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) ${year} ${name} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d266c5e --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Luxmed Booking Service + +Non official bot for **Portal Pacienta LUX MED**. + +With its help user can book a visit to a doctor, create term monitoring, view upcoming visits and visit history. + +It is available by [@luxmedbot](https://telegram.me/luxmedbot) +####To setup your own + +1. create your own telegram bot using [@BotFather](https://telegram.me/botfather) +2. add to .bash_profile + + ``` + export TELEGRAM_TOKEN="SOME TOKEN" + export SECURITY_SECRET="SOME SECRET FOR ENCODING USER PASSWORDS" + ``` +3. install postgres and create db **lbs** with login **lbs** and password **lsb123** +4. run using `./gradlew bootRun` +5. send `/start` to your bot + + + diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 0000000..0a4604d --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,6 @@ +dependencies { + compile project(':common') + + compile group: "org.scalaj", name: "scalaj-http_2.12", version: "2.3.0" + compile group: "org.json4s", name: "json4s-jackson_2.12", version: "3.6.0-M3" +} diff --git a/api/src/main/scala/com/lbs/api/ApiBase.scala b/api/src/main/scala/com/lbs/api/ApiBase.scala new file mode 100644 index 0000000..d83687b --- /dev/null +++ b/api/src/main/scala/com/lbs/api/ApiBase.scala @@ -0,0 +1,44 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api + +import com.lbs.api.http.headers._ +import scalaj.http.{Http, HttpRequest} + +trait ApiBase { + private val CommonHeaders = + Map( + Host -> "portalpacjenta.luxmed.pl", + Accept -> "*/*", + Connection -> "keep-alive", + `Accept-Encoding` -> "gzip;q=1.0, compress;q=0.5", + `User-Agent` -> "PatientPortal/3.3.0 (pl.luxmed.pp.LUX-MED; build:166; iOS 11.3.0) Alamofire/4.5.1", + `Accept-Language` -> "en-PL;q=1.0, ru-PL;q=0.9, pl-PL;q=0.8, uk-PL;q=0.7" + ) + + + protected def http(url: String): HttpRequest = { + Http(s"https://portalpacjenta.luxmed.pl/PatientPortalMobileAPI/api/$url").headers(CommonHeaders) + } +} diff --git a/api/src/main/scala/com/lbs/api/LuxmedApi.scala b/api/src/main/scala/com/lbs/api/LuxmedApi.scala new file mode 100644 index 0000000..088ddb7 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/LuxmedApi.scala @@ -0,0 +1,167 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api + +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +import com.lbs.api.http._ +import com.lbs.api.http.headers._ +import com.lbs.api.json.JsonSerializer.extensions._ +import com.lbs.api.json.model._ +import scalaj.http.{HttpRequest, HttpResponse} +import com.lbs.api.ApiResponseMutators._ + +object LuxmedApi extends ApiBase { + + private val dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + + def login(username: String, password: String, clientId: String = "iPhone"): Either[Throwable, LoginResponse] = { + 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", "password"). + param("password", password). + param("username", username) + post[LoginResponse](request) + } + + def refreshToken(refreshToken: String, clientId: String = "iPhone"): Either[Throwable, 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 reservedVisits(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(), + toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3)): Either[Throwable, 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 visitsHistory(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1), + toDate: ZonedDateTime = ZonedDateTime.now(), page: Int = 1, pageSize: Int = 100): Either[Throwable, VisitsHistoryResponse] = { + val request = http("visits/history"). + 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 + } + + 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): Either[Throwable, ReservationFilterResponse] = { + val request = http("visits/available-terms/reservation-filter"). + 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 + } + + 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): Either[Throwable, AvailableTermsResponse] = { + val request = http("visits/available-terms"). + 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("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 + } + + def temporaryReservation(accessToken: String, tokenType: String, temporaryReservationRequest: TemporaryReservationRequest): Either[Throwable, TemporaryReservationResponse] = { + val request = http("visits/temporary-reservation"). + header(`Content-Type`, "application/json"). + header(Authorization, s"$tokenType $accessToken") + post[TemporaryReservationResponse](request, bodyOpt = Some(temporaryReservationRequest)) + } + + def deleteTemporaryReservation(accessToken: String, tokenType: String, temporaryReservationId: Long): Either[Throwable, HttpResponse[String]] = { + val request = http(s"visits/temporary-reservation/$temporaryReservationId"). + header(`Content-Type`, "application/json"). + header(Authorization, s"$tokenType $accessToken") + delete(request) + } + + def valuations(accessToken: String, tokenType: String, valuationsRequest: ValuationsRequest): Either[Throwable, ValuationsResponse] = { + val request = http("visits/available-terms/valuations"). + header(`Content-Type`, "application/json"). + header(Authorization, s"$tokenType $accessToken") + post[ValuationsResponse](request, bodyOpt = Some(valuationsRequest)) + } + + def reservation(accessToken: String, tokenType: String, reservationRequest: ReservationRequest): Either[Throwable, ReservationResponse] = { + val request = http("visits/reserved"). + header(`Content-Type`, "application/json"). + header(Authorization, s"$tokenType $accessToken") + post[ReservationResponse](request, bodyOpt = Some(reservationRequest)) + } + + def deleteReservation(accessToken: String, tokenType: String, reservationId: Long): Either[Throwable, HttpResponse[String]] = { + val request = http(s"visits/reserved/$reservationId"). + header(`Content-Type`, "application/json"). + header(Authorization, s"$tokenType $accessToken") + delete(request) + } + + private def get[T <: SerializableJsonObject](request: HttpRequest)(implicit mf: scala.reflect.Manifest[T]): Either[Throwable, T] = { + request.toEither.map(_.body.as[T]) + } + + private def post[T <: SerializableJsonObject](request: HttpRequest, bodyOpt: Option[SerializableJsonObject] = None)(implicit mf: scala.reflect.Manifest[T]): Either[Throwable, T] = { + val postRequest = bodyOpt match { + case Some(body) => request.postData(body.asJson) + case None => request.postForm + } + postRequest.toEither.map(_.body.as[T]) + } + + private def delete(request: HttpRequest): Either[Throwable, HttpResponse[String]] = { + request.postForm.method("DELETE").toEither + } + +} diff --git a/api/src/main/scala/com/lbs/api/LuxmedApiAsync.scala b/api/src/main/scala/com/lbs/api/LuxmedApiAsync.scala new file mode 100644 index 0000000..827dc55 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/LuxmedApiAsync.scala @@ -0,0 +1,95 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api + +import java.time.ZonedDateTime + +import com.lbs.api.json.model._ +import scalaj.http.HttpResponse + +import scala.concurrent.{ExecutionContext, Future} +import scala.language.implicitConversions + + +object LuxmedApiAsync { + + private val syncApi = LuxmedApi + + def login(username: String, password: String, clientId: String = "iPhone")(implicit ec: ExecutionContext): Future[LoginResponse] = { + async(syncApi.login(username, password, clientId)) + } + + def refreshToken(refreshToken: String, clientId: String = "iPhone")(implicit ec: ExecutionContext): Future[LoginResponse] = { + async(syncApi.refreshToken(refreshToken, clientId)) + } + + def reservedVisits(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(), + toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3))(implicit ec: ExecutionContext): Future[ReservedVisitsResponse] = { + async(syncApi.reservedVisits(accessToken, tokenType, fromDate, toDate)) + } + + def visitsHistory(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1), + toDate: ZonedDateTime, page: Int = 1, pageSize: Int = 100)(implicit ec: ExecutionContext): Future[VisitsHistoryResponse] = { + async(syncApi.visitsHistory(accessToken, tokenType, fromDate, toDate, page, pageSize)) + } + + def reservationFilter(accessToken: String, tokenType: String, fromDate: ZonedDateTime = ZonedDateTime.now(), + toDate: Option[ZonedDateTime] = None, cityId: Option[Long] = None, + serviceId: Option[Long] = None)(implicit ec: ExecutionContext): Future[ReservationFilterResponse] = { + async(syncApi.reservationFilter(accessToken, tokenType, fromDate, toDate, cityId, serviceId)) + } + + def availableTerms(accessToken: String, tokenType: String, payerId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long, doctorId: Option[Long], + fromDate: ZonedDateTime = ZonedDateTime.now(), toDate: Option[ZonedDateTime] = None, timeOfDay: Int = 0, + languageId: Long = 10, findFirstFreeTerm: Boolean = true)(implicit ec: ExecutionContext): Future[AvailableTermsResponse] = { + async(syncApi.availableTerms(accessToken, tokenType, cityId, payerId, clinicId, serviceId, doctorId, fromDate, toDate, timeOfDay, languageId, findFirstFreeTerm)) + } + + def temporaryReservation(accessToken: String, tokenType: String, temporaryReservationRequest: TemporaryReservationRequest)(implicit ec: ExecutionContext): Future[TemporaryReservationResponse] = { + async(syncApi.temporaryReservation(accessToken, tokenType, temporaryReservationRequest)) + } + + def deleteTemporaryReservation(accessToken: String, tokenType: String, temporaryReservationId: Long)(implicit ec: ExecutionContext): Future[HttpResponse[String]] = { + async(syncApi.deleteTemporaryReservation(accessToken, tokenType, temporaryReservationId)) + } + + def valuations(accessToken: String, tokenType: String, valuationsRequest: ValuationsRequest)(implicit ec: ExecutionContext): Future[ValuationsResponse] = { + async(syncApi.valuations(accessToken, tokenType, valuationsRequest)) + } + + def reservation(accessToken: String, tokenType: String, reservationRequest: ReservationRequest)(implicit ec: ExecutionContext): Future[ReservationResponse] = { + async(syncApi.reservation(accessToken, tokenType, reservationRequest)) + } + + def deleteReservation(accessToken: String, tokenType: String, reservationId: Long)(implicit ec: ExecutionContext): Future[HttpResponse[String]] = { + async(syncApi.deleteReservation(accessToken, tokenType, reservationId)) + } + + private def async[T](f: => Either[Throwable, T])(implicit ec: ExecutionContext) = { + Future(f).flatMap { + case Right(r) => Future.successful(r) + case Left(ex) => Future.failed(ex) + } + } +} diff --git a/api/src/main/scala/com/lbs/api/exception/LuxmedException.scala b/api/src/main/scala/com/lbs/api/exception/LuxmedException.scala new file mode 100644 index 0000000..b1dc93a --- /dev/null +++ b/api/src/main/scala/com/lbs/api/exception/LuxmedException.scala @@ -0,0 +1,28 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.exception + +case class LuxmedException(code: Int, status: String, message: String) extends Exception(message) { + override def toString: String = s"Code: $code, status: $status, message: $message" +} diff --git a/api/src/main/scala/com/lbs/api/http/package.scala b/api/src/main/scala/com/lbs/api/http/package.scala new file mode 100644 index 0000000..e055139 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/http/package.scala @@ -0,0 +1,101 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api + +import com.lbs.api.exception.LuxmedException +import com.lbs.api.json.JsonSerializer.extensions._ +import com.lbs.api.json.model.{LuxmedBaseError, LuxmedCompositeError, LuxmedError, SerializableJsonObject} +import com.lbs.common.Logger +import scalaj.http.{HttpRequest, HttpResponse} + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.Try + +package object http extends Logger { + + object headers { + val `Content-Type` = "Content-Type" + val Host = "Host" + val Accept = "Accept" + val Connection = "Connection" + val `Accept-Encoding` = "Accept-Encoding" + val `User-Agent` = "User-Agent" + val `x-api-client-identifier` = "x-api-client-identifier" + val `Accept-Language` = "Accept-Language" + val Authorization = "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]) + } + + def asEntityAsync[T <: SerializableJsonObject](implicit mf: scala.reflect.Manifest[T], ec: ExecutionContext): Future[HttpResponse[T]] = { + Future(asEntity[T]) + } + } + + implicit class ExtendedHttpRequest(httpRequest: HttpRequest) { + + def toEither: Either[Throwable, HttpResponse[String]] = { + toTry.toEither + } + + def toTry: Try[HttpResponse[String]] = { + LOG.debug(s"Sending request:\n${hidePasswords(httpRequest)}") + val httpResponse = Try(httpRequest.asString) + LOG.debug(s"Received response:\n$httpResponse") + extractLuxmedError(httpResponse) match { + case Some(error) => Try(throw error) + case None => httpResponse.map(_.throwError) + } + } + + def param(key: String, value: Option[String]): HttpRequest = { + value.map(v => httpRequest.param(key, v)).getOrElse(httpRequest) + } + + private def luxmedErrorToException[T <: LuxmedBaseError](ler: HttpResponse[T]) = { + ler.body match { + case e: LuxmedCompositeError => + LuxmedException(ler.code, ler.statusLine, e.errors.map(_.message).mkString("; ")) + case e: LuxmedError => + LuxmedException(ler.code, ler.statusLine, e.message) + } + } + + private def extractLuxmedError(httpResponse: Try[HttpResponse[String]]) = { + httpResponse.flatMap(response => Try(response.asEntity[LuxmedCompositeError]).map(luxmedErrorToException). + orElse(Try(response.asEntity[LuxmedError]).map(luxmedErrorToException))).toOption + } + + private def hidePasswords(httpRequest: HttpRequest) = { + httpRequest.copy(params = httpRequest.params.map { case (k, v) => + if (k.toLowerCase.contains("passw")) k -> "******" else k -> v + }) + } + } + +} diff --git a/api/src/main/scala/com/lbs/api/json/JsonSerializer.scala b/api/src/main/scala/com/lbs/api/json/JsonSerializer.scala new file mode 100644 index 0000000..4560b65 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/JsonSerializer.scala @@ -0,0 +1,69 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.json + +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +import com.lbs.api.json.model.SerializableJsonObject +import org.json4s._ +import org.json4s.jackson.JsonMethods._ + + +object JsonSerializer { + + private val localDateTimeSerializer = 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 + + def extract[T <: SerializableJsonObject](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)) + } + + object extensions { + + implicit class JsonStringToObject(jsonString: String) { + def as[T <: SerializableJsonObject](implicit mf: scala.reflect.Manifest[T]): T = { + extract[T](jsonString) + } + } + + implicit class JsonObjectToString[T <: SerializableJsonObject](jsonObject: T) { + def asJson: String = { + write(jsonObject) + } + } + + } + +} 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 new file mode 100644 index 0000000..87e4311 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/AvailbaleTermsResponse.scala @@ -0,0 +1,86 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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/IdName.scala b/api/src/main/scala/com/lbs/api/json/model/IdName.scala new file mode 100644 index 0000000..ea55728 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/IdName.scala @@ -0,0 +1,28 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.json.model + +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/LoginResponse.scala b/api/src/main/scala/com/lbs/api/json/model/LoginResponse.scala new file mode 100644 index 0000000..ca12213 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/LoginResponse.scala @@ -0,0 +1,34 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.json.model + +/** + * { + * "access_token": "IDtjG_ECOd_ETYE2fwrCoTcC6bW935cn_nUh6d3BaEa-jvPlHfPLOY5AkF", + * "expires_in": 599, + * "refresh_token": "d251c66c-49e0-4777-b766-08326d83fa31", + * "token_type": "bearer" + * } + */ +case class LoginResponse(accessToken: String, expiresIn: Int, refreshToken: String, tokenType: String) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/LuxmedBaseError.scala b/api/src/main/scala/com/lbs/api/json/model/LuxmedBaseError.scala new file mode 100644 index 0000000..6d7ac20 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/LuxmedBaseError.scala @@ -0,0 +1,26 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.json.model + +trait LuxmedBaseError diff --git a/api/src/main/scala/com/lbs/api/json/model/LuxmedCompositeError.scala b/api/src/main/scala/com/lbs/api/json/model/LuxmedCompositeError.scala new file mode 100644 index 0000000..e2899a3 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/LuxmedCompositeError.scala @@ -0,0 +1,28 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.json.model + +case class LuxmedCompositeError(errors: List[LuxmedCompositeMessage]) extends SerializableJsonObject with LuxmedBaseError + +case class LuxmedCompositeMessage(errorCode: Int, message: String) extends SerializableJsonObject \ No newline at end of file diff --git a/api/src/main/scala/com/lbs/api/json/model/LuxmedError.scala b/api/src/main/scala/com/lbs/api/json/model/LuxmedError.scala new file mode 100644 index 0000000..f49071a --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/LuxmedError.scala @@ -0,0 +1,26 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.json.model + +case class LuxmedError(message: String) extends SerializableJsonObject with LuxmedBaseError \ No newline at end of file diff --git a/api/src/main/scala/com/lbs/api/json/model/PayerDetails.scala b/api/src/main/scala/com/lbs/api/json/model/PayerDetails.scala new file mode 100644 index 0000000..f5dbc78 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/PayerDetails.scala @@ -0,0 +1,27 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.json.model + +case class PayerDetails(brandId: Option[Long], contractId: Long, payerId: Long, payerName: String, productElementId: Long, + productId: Long, productInContractId: Long, servaAppId: Long, servaId: Long) extends SerializableJsonObject diff --git a/api/src/main/scala/com/lbs/api/json/model/ReservationFilterResponse.scala b/api/src/main/scala/com/lbs/api/json/model/ReservationFilterResponse.scala new file mode 100644 index 0000000..463e63e --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ReservationFilterResponse.scala @@ -0,0 +1,89 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.json.model + +/** +{ + "Cities": [ + { + "Id": 5, + "Name": "Wrocław" + } + ], + "Clinics": [ + { + "Id": 1405, + "Name": "Konsylium Wrocław - Legnicka 40" + }, + { + "Id": 7, + "Name": "LX Wrocław - Kwidzyńska 6" + } + ], + "DefaultPayer": { + "Id": 22222, + "Name": "FIRMA POLAND SP. Z O.O." + }, + "Doctors": [ + { + "Id": 38275, + "Name": "ANNA ABRAMCZYK lek. med." + }, + { + "Id": 15565, + "Name": "ANDRZEJ ANDEWSKI dr n. med." + } + ], + "Languages": [ + { + "Id": 11, + "Name": "english" + }, + { + "Id": 10, + "Name": "polish" + } + ], + "Payers": [ + { + "Id": 22222, + "Name": "FIRMA POLAND SP. Z O.O." + } + ], + "Services": [ + { + "Id": 5857, + "Name": "Audiometr standardowy" + }, + { + "Id": 7976, + "Name": "Audiometr standardowy - audiometria nadprogowa" + } + ] +} + */ +case class ReservationFilterResponse(cities: List[IdName], clinics: List[IdName], defaultPayer: Option[IdName], + doctors: List[IdName], languages: List[IdName], payers: List[IdName], + services: List[IdName]) extends SerializableJsonObject + 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 new file mode 100644 index 0000000..c56ec69 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ReservationRequest.scala @@ -0,0 +1,50 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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 new file mode 100644 index 0000000..8eff762 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ReservationResponse.scala @@ -0,0 +1,46 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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 new file mode 100644 index 0000000..b2dc62a --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ReservedVisitsResponse.scala @@ -0,0 +1,66 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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/SerializableJsonObject.scala b/api/src/main/scala/com/lbs/api/json/model/SerializableJsonObject.scala new file mode 100644 index 0000000..47eb511 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/SerializableJsonObject.scala @@ -0,0 +1,26 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.json.model + +trait 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 new file mode 100644 index 0000000..d40d57e --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/TemporaryReservationRequest.scala @@ -0,0 +1,65 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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 new file mode 100644 index 0000000..b633856 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/TemporaryReservationResponse.scala @@ -0,0 +1,27 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.json.model + +case class TemporaryReservationResponse(hasReferralRequired: Boolean, id: Long, + informationMessages: List[String]) extends SerializableJsonObject 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 new file mode 100644 index 0000000..e20d6fb --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ValuationsRequest.scala @@ -0,0 +1,65 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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 new file mode 100644 index 0000000..4db9860 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/ValuationsResponse.scala @@ -0,0 +1,63 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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/VisitDate.scala b/api/src/main/scala/com/lbs/api/json/model/VisitDate.scala new file mode 100644 index 0000000..6599600 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/VisitDate.scala @@ -0,0 +1,28 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.api.json.model + +import java.time.ZonedDateTime + +case class VisitDate(formattedDate: String, startDateTime: ZonedDateTime) 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 new file mode 100644 index 0000000..badc49c --- /dev/null +++ b/api/src/main/scala/com/lbs/api/json/model/VisitsHistoryResponse.scala @@ -0,0 +1,99 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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/package.scala b/api/src/main/scala/com/lbs/api/package.scala new file mode 100644 index 0000000..116f0c3 --- /dev/null +++ b/api/src/main/scala/com/lbs/api/package.scala @@ -0,0 +1,66 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs + +import com.lbs.api.json.model.{AvailableTermsResponse, ReservationFilterResponse, ReservedVisitsResponse, VisitsHistoryResponse} + +import scala.util.matching.Regex + +package object api { + + 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](response: Either[Throwable, T]) { + def mutate: Either[Throwable, T] = { + val mutator = implicitly[ResponseMutator[T]] + response.map(mutator.mutate) + } + } + + implicit val ReservedVisitsResponseMutator: ResponseMutator[ReservedVisitsResponse] = (response: ReservedVisitsResponse) => { + response.copy(reservedVisits = response.reservedVisits.map(rv => rv.copy(doctorName = cleanupDoctorName(rv.doctorName)))) + } + + implicit val VisitsHistoryResponseMutator: ResponseMutator[VisitsHistoryResponse] = (response: VisitsHistoryResponse) => { + response.copy(historicVisits = response.historicVisits.map(hv => hv.copy(doctorName = cleanupDoctorName(hv.doctorName)))) + } + + implicit val ReservationFilterResponseMutator: ResponseMutator[ReservationFilterResponse] = (response: ReservationFilterResponse) => { + response.copy(doctors = response.doctors.map(d => d.copy(name = cleanupDoctorName(d.name)))) + } + + implicit val AvailableTermsResponseMutator: ResponseMutator[AvailableTermsResponse] = (response: AvailableTermsResponse) => { + response.copy(availableVisitsTermPresentation = + response.availableVisitsTermPresentation.map(atp => atp.copy(doctor = atp.doctor.copy(name = cleanupDoctorName(atp.doctor.name))))) + } + } + +} 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 new file mode 100644 index 0000000..9b7e735 --- /dev/null +++ b/api/src/test/scala/com/lbs/api/json/model/CommonSpec.scala @@ -0,0 +1,43 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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 new file mode 100644 index 0000000..218ec95 --- /dev/null +++ b/api/src/test/scala/com/lbs/api/json/model/LoginResponseSpec.scala @@ -0,0 +1,48 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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/ReservationFilterResponseSpec.scala b/api/src/test/scala/com/lbs/api/json/model/ReservationFilterResponseSpec.scala new file mode 100644 index 0000000..d73fedb --- /dev/null +++ b/api/src/test/scala/com/lbs/api/json/model/ReservationFilterResponseSpec.scala @@ -0,0 +1,106 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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 new file mode 100644 index 0000000..8110c35 --- /dev/null +++ b/api/src/test/scala/com/lbs/api/json/model/ReservedVisitsResponseSpec.scala @@ -0,0 +1,84 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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 new file mode 100644 index 0000000..7af54d9 --- /dev/null +++ b/api/src/test/scala/com/lbs/api/json/model/TemporaryReservationRequestSpec.scala @@ -0,0 +1,71 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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 = 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 new file mode 100644 index 0000000..bd136d2 --- /dev/null +++ b/api/src/test/scala/com/lbs/api/json/model/ValuationsRequestSpec.scala @@ -0,0 +1,71 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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 = 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/VisitsHistoryResponseSpec.scala b/api/src/test/scala/com/lbs/api/json/model/VisitsHistoryResponseSpec.scala new file mode 100644 index 0000000..dfed9d5 --- /dev/null +++ b/api/src/test/scala/com/lbs/api/json/model/VisitsHistoryResponseSpec.scala @@ -0,0 +1,119 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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 new file mode 100644 index 0000000..cd08c79 --- /dev/null +++ b/bot/build.gradle @@ -0,0 +1,5 @@ +dependencies { + compile project(':common') + + compile group: "info.mukel", name: "telegrambot4s_2.12", version: "3.0.14" +} \ No newline at end of file diff --git a/bot/src/main/scala/com/lbs/bot/Bot.scala b/bot/src/main/scala/com/lbs/bot/Bot.scala new file mode 100644 index 0000000..c362a32 --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/Bot.scala @@ -0,0 +1,49 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot + +import com.lbs.bot.model._ +import com.lbs.bot.telegram.TelegramBot +import com.lbs.common.Logger + +class Bot(telegram: TelegramBot /* other bots */) extends Logger { + def sendMessage(source: MessageSource, text: String): Unit = + resolveAdapter(source).sendMessage(source.chatId, text) + + def sendMessage(source: MessageSource, text: String, inlineKeyboard: Option[InlineKeyboard] = None): Unit = + resolveAdapter(source).sendMessage(source.chatId, text, inlineKeyboard) + + def sendEditMessage(source: MessageSource, messageId: String, inlineKeyboard: Option[InlineKeyboard]): Unit = + resolveAdapter(source).sendEditMessage(source.chatId, messageId, inlineKeyboard) + + def sendEditMessage(source: MessageSource, messageId: String, text: String, inlineKeyboard: Option[InlineKeyboard] = None): Unit = + resolveAdapter(source).sendEditMessage(source.chatId, messageId, text, inlineKeyboard) + + private def resolveAdapter(source: MessageSource): PollBot[_] = + source.sourceSystem match { + case TelegramMessageSourceSystem => telegram + case sourceSystem => + sys.error(s"Unsupported source system $sourceSystem") + } +} diff --git a/bot/src/main/scala/com/lbs/bot/PollBot.scala b/bot/src/main/scala/com/lbs/bot/PollBot.scala new file mode 100644 index 0000000..749e1ff --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/PollBot.scala @@ -0,0 +1,38 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot + +import com.lbs.bot.model.{Event, InlineKeyboard} + +trait PollBot[In <: Event] { + def sendMessage(chatId: String, text: String): Unit + + def sendMessage(chatId: String, text: String, buttons: Option[InlineKeyboard] = None): Unit + + def sendEditMessage(chatId: String, messageId: String, buttons: Option[InlineKeyboard]): Unit + + def sendEditMessage(chatId: String, messageId: String, text: String, buttons: Option[InlineKeyboard] = None): Unit + + protected def onReceive(command: In): Unit +} diff --git a/bot/src/main/scala/com/lbs/bot/WebhookBot.scala b/bot/src/main/scala/com/lbs/bot/WebhookBot.scala new file mode 100644 index 0000000..c156d37 --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/WebhookBot.scala @@ -0,0 +1,30 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot + +import com.lbs.bot.model.Event + +trait WebhookBot[In <: Event] extends PollBot[In] { + def processPayload(payload: String, signature: Option[String]): Unit +} diff --git a/bot/src/main/scala/com/lbs/bot/model/Button.scala b/bot/src/main/scala/com/lbs/bot/model/Button.scala new file mode 100644 index 0000000..9951cfd --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/model/Button.scala @@ -0,0 +1,38 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot.model + +object Button { + def apply(label: String, id: Long) = new TaggedButton(label, id.toString) + + def apply(label: String, id: String) = new TaggedButton(label, id) + + def apply(label: String) = new LabeledButton(label) +} + +trait Button + +class TaggedButton(val label: String, val tag: String) extends Button + +class LabeledButton(val label: String) extends Button \ No newline at end of file diff --git a/bot/src/main/scala/com/lbs/bot/model/Command.scala b/bot/src/main/scala/com/lbs/bot/model/Command.scala new file mode 100644 index 0000000..20b0bd5 --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/model/Command.scala @@ -0,0 +1,28 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot.model + +case class Message(messageId: String, text: Option[String] = None) + +case class Command(source: MessageSource, message: Message, callbackData: Option[String] = None) \ No newline at end of file diff --git a/bot/src/main/scala/com/lbs/bot/model/Event.scala b/bot/src/main/scala/com/lbs/bot/model/Event.scala new file mode 100644 index 0000000..a0859ba --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/model/Event.scala @@ -0,0 +1,26 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot.model + +trait Event diff --git a/bot/src/main/scala/com/lbs/bot/model/InlineKeyboard.scala b/bot/src/main/scala/com/lbs/bot/model/InlineKeyboard.scala new file mode 100644 index 0000000..2c63b68 --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/model/InlineKeyboard.scala @@ -0,0 +1,26 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot.model + +case class InlineKeyboard(buttons: Seq[Seq[Button]]) diff --git a/bot/src/main/scala/com/lbs/bot/model/MessageSource.scala b/bot/src/main/scala/com/lbs/bot/model/MessageSource.scala new file mode 100644 index 0000000..bdf647b --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/model/MessageSource.scala @@ -0,0 +1,26 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot.model + +case class MessageSource(sourceSystem: MessageSourceSystem, chatId: String) diff --git a/bot/src/main/scala/com/lbs/bot/model/MessageSourceSystem.scala b/bot/src/main/scala/com/lbs/bot/model/MessageSourceSystem.scala new file mode 100644 index 0000000..917a088 --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/model/MessageSourceSystem.scala @@ -0,0 +1,57 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot.model + +trait MessageSourceSystem { + def id: Long + + def name: String + + override def toString: String = name +} + +object MessageSourceSystem { + val MessageSourceSystems: Seq[MessageSourceSystem] = Seq( + TelegramMessageSourceSystem, + FacebookMessageSourceSystem + ) + + private val MessageSourceSystemsMap = MessageSourceSystems.map(e => e.id -> e).toMap + + def apply(id: Long): MessageSourceSystem = { + MessageSourceSystemsMap.getOrElse(id, sys.error(s"Unsupported source system $id")) + } +} + +object TelegramMessageSourceSystem extends MessageSourceSystem { + override def id: Long = 1 + + override def name: String = "Telegram" +} + +object FacebookMessageSourceSystem extends MessageSourceSystem { + override def id: Long = 2 + + override def name: String = "Facebook" +} diff --git a/bot/src/main/scala/com/lbs/bot/package.scala b/bot/src/main/scala/com/lbs/bot/package.scala new file mode 100644 index 0000000..a72af8a --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/package.scala @@ -0,0 +1,36 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs + +import com.lbs.bot.model.{Button, InlineKeyboard} + +package object bot { + def createInlineKeyboard(buttons: Seq[Button], columns: Int = 2): Option[InlineKeyboard] = { + Option(buttons).filterNot(_.isEmpty).map(b => InlineKeyboard(b.grouped(columns).toSeq)) + } + + def createInlineKeyboard(buttons: Seq[Seq[Button]]): Option[InlineKeyboard] = { + Option(buttons).filterNot(_.isEmpty).map(InlineKeyboard) + } +} diff --git a/bot/src/main/scala/com/lbs/bot/telegram/TelegramBot.scala b/bot/src/main/scala/com/lbs/bot/telegram/TelegramBot.scala new file mode 100644 index 0000000..9fdb73a --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/telegram/TelegramBot.scala @@ -0,0 +1,51 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot.telegram + +import com.lbs.bot.PollBot +import com.lbs.bot.model._ +import com.lbs.bot.telegram.TelegramModelConverters._ +import info.mukel.telegrambot4s.models.InlineKeyboardMarkup + +class TelegramBot(onCommand: Command => Unit, botToken: String) extends PollBot[TelegramEvent] { + + private val telegramBot = new TelegramClient(onReceive, botToken) + telegramBot.run() + + def sendMessage(chatId: String, text: String): Unit = + telegramBot.sendMessage(chatId.toLong, text) + + def sendMessage(chatId: String, text: String, buttons: Option[InlineKeyboard] = None): Unit = + telegramBot.sendMessage(chatId.toLong, text, replyMarkup = buttons.map(_.mapTo[InlineKeyboardMarkup])) + + def sendEditMessage(chatId: String, messageId: String, buttons: Option[InlineKeyboard]): Unit = + telegramBot.sendEditMessage(chatId.toLong, messageId.toInt, replyMarkup = buttons.map(_.mapTo[InlineKeyboardMarkup])) + + def sendEditMessage(chatId: String, messageId: String, text: String, buttons: Option[InlineKeyboard] = None): Unit = + telegramBot.sendEditMessage(chatId.toLong, messageId.toInt, text, replyMarkup = buttons.map(_.mapTo[InlineKeyboardMarkup])) + + override protected def onReceive(command: TelegramEvent): Unit = { + onCommand(command.mapTo[Command]) + } +} \ 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 new file mode 100644 index 0000000..05ba3ef --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/telegram/TelegramClient.scala @@ -0,0 +1,66 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot.telegram + +import com.lbs.common.Logger +import info.mukel.telegrambot4s.api.declarative.{Callbacks, Commands} +import info.mukel.telegrambot4s.api.{Polling, TelegramBot => TelegramBotBase} +import info.mukel.telegrambot4s.methods.{EditMessageReplyMarkup, EditMessageText, ParseMode, SendMessage} +import info.mukel.telegrambot4s.models._ + +import scala.concurrent.Future + +class TelegramClient(onReceive: TelegramEvent => Unit, botToken: String) extends TelegramBotBase with Polling with Commands with Callbacks with Logger { + + override def token: String = botToken + + def sendMessage(chatId: Long, text: String): Future[Message] = + request(SendMessage(chatId, text, parseMode = Some(ParseMode.HTML))) + + def sendMessage(chatId: Long, text: String, replyMarkup: Option[InlineKeyboardMarkup] = None): Future[Message] = + request(SendMessage(chatId, text, parseMode = Some(ParseMode.HTML), replyMarkup = replyMarkup)) + + def sendEditMessage(chatId: Long, messageId: Int, replyMarkup: Option[InlineKeyboardMarkup]): Future[Either[Boolean, Message]] = + request(EditMessageReplyMarkup(Some(chatId), Some(messageId), replyMarkup = replyMarkup)) + + def sendEditMessage(chatId: Long, messageId: Int, text: String, replyMarkup: Option[InlineKeyboardMarkup] = None): Future[Either[Boolean, Message]] = + request(EditMessageText(Some(chatId), Some(messageId), text = text, parseMode = Some(ParseMode.HTML), replyMarkup = replyMarkup)) + + + override def receiveMessage(msg: Message): Unit = { + LOG.debug(s"Received telegram message: $msg") + onReceive(TelegramEvent(msg, None)) + } + + onCallbackWithTag(TagPrefix) { implicit cbq => + LOG.debug(s"Received telegram callback: $cbq") + ackCallback() + for { + data <- cbq.data.map(_.stripPrefix(TagPrefix)) + msg <- cbq.message + } { + onReceive(TelegramEvent(msg, Some(data))) + } + } +} \ No newline at end of file diff --git a/bot/src/main/scala/com/lbs/bot/telegram/TelegramEvent.scala b/bot/src/main/scala/com/lbs/bot/telegram/TelegramEvent.scala new file mode 100644 index 0000000..2d4a114 --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/telegram/TelegramEvent.scala @@ -0,0 +1,29 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot.telegram + +import com.lbs.bot.model.Event +import info.mukel.telegrambot4s.models.Message + +case class TelegramEvent(msg: Message, callbackData: Option[String]) extends Event diff --git a/bot/src/main/scala/com/lbs/bot/telegram/package.scala b/bot/src/main/scala/com/lbs/bot/telegram/package.scala new file mode 100644 index 0000000..149b5b6 --- /dev/null +++ b/bot/src/main/scala/com/lbs/bot/telegram/package.scala @@ -0,0 +1,77 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.bot + +import com.lbs.bot.model._ +import com.lbs.common.ModelConverters +import info.mukel.telegrambot4s.models +import info.mukel.telegrambot4s.models.{InlineKeyboardButton, InlineKeyboardMarkup} + +package object telegram { + + protected[bot] val TagPrefix = "callback" + + object TelegramModelConverters extends ModelConverters { + implicit val TelegramCommandToCommandConverter: + ObjectConverter[TelegramEvent, Command] = + new ObjectConverter[TelegramEvent, Command] { + override def convert[Z <: TelegramEvent](data: Z): Command = { + Command( + source = MessageSource(TelegramMessageSourceSystem, data.msg.chat.id.toString), + message = Message(data.msg.messageId.toString, data.msg.text), + callbackData = data.callbackData + ) + } + } + + implicit val TelegramMessageToMessageConverter: + ObjectConverter[models.Message, Message] = + new ObjectConverter[models.Message, Message] { + override def convert[Z <: models.Message](data: Z): Message = { + Message(data.messageId.toString, data.text) + } + } + + implicit val InlineKeyboardToInlineKeyboardMarkup: + ObjectConverter[InlineKeyboard, InlineKeyboardMarkup] = + new ObjectConverter[InlineKeyboard, InlineKeyboardMarkup] { + override def convert[Z <: InlineKeyboard](inlineKeyboard: Z): InlineKeyboardMarkup = { + val buttons = inlineKeyboard.buttons.map { row => + row.map(createInlineKeyboardButton) + } + InlineKeyboardMarkup(buttons) + } + } + + private def createInlineKeyboardButton(button: Button) = { + button match { + case b: TaggedButton => InlineKeyboardButton.callbackData(b.label, tag(b.tag)) + case b: LabeledButton => InlineKeyboardButton.callbackData(b.label, b.label) + } + } + + private def tag(name: String): String = TagPrefix + name + } + +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..dee1e5f --- /dev/null +++ b/build.gradle @@ -0,0 +1,47 @@ +group = 'com.lbs' +version = '0.0.1-SNAPSHOT' + +ext { + scalaVersion = "2.12.6" +} + +apply plugin: 'idea' + +subprojects { + + idea { + module { + downloadSources = true + } + } + + repositories { + mavenCentral() + jcenter() + maven { url 'https://repo.spring.io/libs-milestone' } + } + + apply plugin: 'java' + apply plugin: 'scala' + + dependencies { + compile group: 'org.scala-lang', name: "scala-library", version: scalaVersion + compile('org.scalaz:scalaz-core_2.12:7.2.23') + + testCompile('org.scalatest:scalatest_2.12:3.0.4') + testCompile('org.mockito:mockito-core:2.13.0') + testCompile('org.pegdown:pegdown:1.6.0') + } + + task scalaTest(dependsOn: ['testClasses'], type: JavaExec) { + main = 'org.scalatest.tools.Runner' + args = ['-R', "build/classes/test", + '-u', "build/test-results/$name", + '-h', "build/resports/scalaTests/$name", + '-o' + ] + classpath = sourceSets.test.runtimeClasspath + } + + test.dependsOn scalaTest +} \ No newline at end of file diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000..bdf3ccd --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,5 @@ +dependencies { + compile group: "org.slf4j", name: "slf4j-api", version: "1.7.25" + compile group: "ch.qos.logback", name: "logback-classic", version: "1.2.3" + compile group: "ch.qos.logback", name: "logback-core", version: "1.2.3" +} diff --git a/common/src/main/scala/com/lbs/common/Implicits.scala b/common/src/main/scala/com/lbs/common/Implicits.scala new file mode 100644 index 0000000..b82b8c4 --- /dev/null +++ b/common/src/main/scala/com/lbs/common/Implicits.scala @@ -0,0 +1,38 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.common + +import java.util.Optional + +import scala.language.implicitConversions + +object Implicits { + implicit def optionalToOption[T](optional: Optional[T]): Option[T] = { + Option(optional.orElse(null.asInstanceOf[T])) + } + + implicit def optionToOptional[T](option: Option[T]): Optional[T] = { + Optional.of(option.getOrElse(null.asInstanceOf[T])) + } +} diff --git a/common/src/main/scala/com/lbs/common/Logger.scala b/common/src/main/scala/com/lbs/common/Logger.scala new file mode 100644 index 0000000..f825947 --- /dev/null +++ b/common/src/main/scala/com/lbs/common/Logger.scala @@ -0,0 +1,66 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.common + +import org.slf4j +import org.slf4j.LoggerFactory + +trait Logger { + private val log: slf4j.Logger = LoggerFactory.getLogger(this.getClass) + + protected val LOG = new LoggerWrapper + + class LoggerWrapper { + def debug(msg: => String): Unit = { + if (log.isDebugEnabled) + log.debug(msg) + } + + def warn(msg: => String): Unit = { + if (log.isWarnEnabled) + log.warn(msg) + } + + def warn(msg: => String, throwable: Throwable): Unit = { + if (log.isWarnEnabled) + log.warn(msg, throwable) + } + + def error(msg: => String): Unit = { + if (log.isErrorEnabled) + log.error(msg) + } + + def error(msg: => String, throwable: Throwable): Unit = { + if (log.isErrorEnabled) + log.error(msg, throwable) + } + + def info(msg: => String): Unit = { + if (log.isInfoEnabled) + log.info(msg) + } + } + +} diff --git a/common/src/main/scala/com/lbs/common/ModelConverters.scala b/common/src/main/scala/com/lbs/common/ModelConverters.scala new file mode 100644 index 0000000..92420f0 --- /dev/null +++ b/common/src/main/scala/com/lbs/common/ModelConverters.scala @@ -0,0 +1,47 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.common + +import scala.collection.generic.CanBuildFrom +import scala.language.{higherKinds, implicitConversions} + +trait ModelConverters { + + trait CollectionConverter[-In, Out] { + def convert[Z <: In, Col[X] <: Iterable[X]](col: Col[Z])(implicit bf: CanBuildFrom[Col[Z], Out, Col[Out]]): Col[Out] + } + + trait ObjectConverter[-In, Out] { + def convert[Z <: In](any: Z): Out + } + + implicit class CollectionOps[From, Col[X] <: Iterable[X]](col: Col[From]) { + def mapTo[To](implicit converter: CollectionConverter[From, To], bf: CanBuildFrom[Col[From], To, Col[To]]): Col[To] = converter.convert(col) + } + + implicit class ObjectOps[From](anyRef: From) { + def mapTo[To](implicit converter: ObjectConverter[From, To]): To = converter.convert(anyRef) + } + +} diff --git a/common/src/main/scala/com/lbs/common/ParametrizedLock.scala b/common/src/main/scala/com/lbs/common/ParametrizedLock.scala new file mode 100644 index 0000000..9a3e19f --- /dev/null +++ b/common/src/main/scala/com/lbs/common/ParametrizedLock.scala @@ -0,0 +1,32 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.common + +import java.util.concurrent.ConcurrentHashMap + +class ParametrizedLock[K] { + private val locks = new ConcurrentHashMap[K, AnyRef] + + def obtainLock(key: K): AnyRef = locks.computeIfAbsent(key, k => new AnyRef) +} diff --git a/common/src/main/scala/com/lbs/common/Scheduler.scala b/common/src/main/scala/com/lbs/common/Scheduler.scala new file mode 100644 index 0000000..ea55c6a --- /dev/null +++ b/common/src/main/scala/com/lbs/common/Scheduler.scala @@ -0,0 +1,41 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.common + +import java.util.concurrent.{Executors, ScheduledFuture} + +import scala.concurrent.duration.FiniteDuration + +class Scheduler(poolSize: Int) { + private val scheduledThreadPool = Executors.newScheduledThreadPool(poolSize) + + def schedule(fn: => Unit, period: FiniteDuration): ScheduledFuture[_] = { + scheduledThreadPool.scheduleAtFixedRate(() => fn, period.length, period.length, period.unit) + } + + def schedule(fn: => Unit, delay: FiniteDuration, period: FiniteDuration): ScheduledFuture[_] = { + require(delay.unit == period.unit, s"Delay units must be the same as for period ${period.unit}") + scheduledThreadPool.scheduleAtFixedRate(() => fn, delay.length, period.length, period.unit) + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7935b5a --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.configureondemand=true +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1a958be6420a4dd04b588fe4b31eb5d7ff8748c2 GIT binary patch literal 54711 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/server/build.gradle b/server/build.gradle new file mode 100644 index 0000000..fe10085 --- /dev/null +++ b/server/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext { + springBootVersion = '2.0.2.RELEASE' + } + repositories { + mavenCentral() + jcenter() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +apply plugin: 'java' +apply plugin: 'scala' +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' + +dependencies { + compile project(':api') + compile project(':bot') + compile project(':common') + + compile('org.springframework.boot:spring-boot-starter') + compile('org.springframework.boot:spring-boot-starter-data-jpa') + + compile('org.jasypt:jasypt:1.9.2') + compile('org.postgresql:postgresql:42.2.1.jre7') +} + + diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf new file mode 100644 index 0000000..21fe89b --- /dev/null +++ b/server/src/main/resources/application.conf @@ -0,0 +1,3 @@ +akka { + loglevel = "WARNING" +} \ No newline at end of file diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml new file mode 100644 index 0000000..79d42ce --- /dev/null +++ b/server/src/main/resources/application.yml @@ -0,0 +1,31 @@ +spring: + datasource: + url: "jdbc:postgresql://127.0.0.1:5432/lbs" + username: "lbs" + password: "lsb123" + jpa: + properties: + hibernate: + temp: + use_jdbc_metadata_defaults: "false" + database-platform: "org.hibernate.dialect.PostgreSQL9Dialect" + generate-ddl: "true" +# hibernate: +# ddl-auto: "create-update" +# ddl-auto: "create-drop" + +banner: + location: "classpath:/banner.txt" + +logging: + file: logs/app.log + level: + com.lbs: DEBUG +# org.hibernate.SQL: DEBUG +# org.hibernate.type.descriptor.sql.BasicBinder: TRACE + pattern: + file: "%d{yyyy-MM-dd HH:mm:ss} %logger{25} - %msg%n" + console: "%d{yyyy-MM-dd HH:mm:ss} %logger{25} - %msg%n" + +security.secret: ${SECURITY_SECRET:random_secret_hfjdsk72euhdsbcgg6} +telegram.token: ${TELEGRAM_TOKEN} \ No newline at end of file diff --git a/server/src/main/resources/banner.txt b/server/src/main/resources/banner.txt new file mode 100644 index 0000000..d8c5ffc --- /dev/null +++ b/server/src/main/resources/banner.txt @@ -0,0 +1,6 @@ +,--. ,-----. ,---. +| | | |) /_ ' .-' +| | | .-. \ `. `-. +| '--. | '--' / .-' | +`-----' `------' `-----' + \ No newline at end of file diff --git a/server/src/main/scala/com/lbs/server/Boot.scala b/server/src/main/scala/com/lbs/server/Boot.scala new file mode 100644 index 0000000..a69762e --- /dev/null +++ b/server/src/main/scala/com/lbs/server/Boot.scala @@ -0,0 +1,35 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication + + +@SpringBootApplication +class Boot + +object Boot extends App { + SpringApplication.run(classOf[Boot], args: _*) +} diff --git a/server/src/main/scala/com/lbs/server/BootConfig.scala b/server/src/main/scala/com/lbs/server/BootConfig.scala new file mode 100644 index 0000000..8e0df87 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/BootConfig.scala @@ -0,0 +1,148 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server + +import akka.actor.{ActorRef, ActorSystem} +import com.lbs.bot.Bot +import com.lbs.bot.model.MessageSource +import com.lbs.bot.telegram.TelegramBot +import com.lbs.server.actor.Login.UserId +import com.lbs.server.actor._ +import com.lbs.server.lang.Localization +import com.lbs.server.service.{ApiService, DataService, MonitoringService} +import org.jasypt.util.text.{StrongTextEncryptor, TextEncryptor} +import org.springframework.beans.factory.annotation.{Autowired, Value} +import org.springframework.context.annotation.{Bean, Configuration} + +@Configuration +class BootConfig { + @Value("${security.secret}") + private var secret: String = _ + + @Value("${telegram.token}") + private var telegramBotToken: String = _ + + @Autowired + private var apiService: ApiService = _ + + @Autowired + private var dataService: DataService = _ + + @Autowired + private var monitoringService: MonitoringService = _ + + @Autowired + private var localization: Localization = _ + + @Bean + def actorSystem = ActorSystem() + + @Bean + def textEncryptor: TextEncryptor = { + val encryptor = new StrongTextEncryptor + encryptor.setPassword(secret) + encryptor + } + + @Bean + def authActorFactory: MessageSource => ActorRef = source => actorSystem.actorOf(Auth.props(source, + dataService, unauthorizedHelpActorFactory, loginActorFactory, chatActorFactory)) + + @Bean + def loginActorFactory: (MessageSource, ActorRef) => ActorRef = (source, originator) => actorSystem.actorOf(Login.props(source, bot, + dataService, apiService, textEncryptor, localization, originator)) + + @Bean + def bookingActorFactory: UserId => ActorRef = userId => actorSystem.actorOf(Book.props(userId, bot, apiService, dataService, + monitoringService, localization, datePickerFactory, staticDataActorFactory, termsPagerActorFactory)) + + @Bean + def unauthorizedHelpActorFactory: MessageSource => ActorRef = source => actorSystem.actorOf(UnauthorizedHelp.props(source, bot)) + + @Bean + def helpActorFactory: UserId => ActorRef = userId => actorSystem.actorOf(Help.props(userId, bot, localization)) + + @Bean + def monitoringsActorFactory: UserId => ActorRef = + userId => actorSystem.actorOf(Monitorings.props(userId, bot, monitoringService, localization, monitoringsPagerActorFactory)) + + @Bean + def historyActorFactory: UserId => ActorRef = + userId => actorSystem.actorOf(History.props(userId, bot, apiService, localization, historyPagerActorFactory)) + + @Bean + def visitsActorFactory: UserId => ActorRef = + userId => actorSystem.actorOf(Visits.props(userId, bot, apiService, localization, visitsPagerActorFactory)) + + @Bean + def bugActorFactory: UserId => ActorRef = + userId => actorSystem.actorOf(Bug.props(userId, bot, dataService, bugPagerActorFactory, localization)) + + @Bean + def settingsActorFactory: UserId => ActorRef = + userId => actorSystem.actorOf(Settings.props(userId, bot, dataService, localization)) + + @Bean + def chatActorFactory: UserId => ActorRef = + userId => actorSystem.actorOf(Chat.props(userId, dataService, monitoringService, bookingActorFactory, helpActorFactory, + monitoringsActorFactory, historyActorFactory, visitsActorFactory, settingsActorFactory, bugActorFactory)) + + @Bean + def datePickerFactory: (UserId, ActorRef) => ActorRef = (userId, originator) => + actorSystem.actorOf(DatePicker.props(userId, bot, localization, originator)) + + @Bean + def staticDataActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) => + actorSystem.actorOf(StaticData.props(userId, bot, localization, originator)) + + @Bean + def termsPagerActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) => + actorSystem.actorOf(Pagers(userId, bot, localization).termsPagerProps(originator)) + + @Bean + def visitsPagerActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) => + actorSystem.actorOf(Pagers(userId, bot, localization).visitsPagerProps(originator)) + + @Bean + def bugPagerActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) => + actorSystem.actorOf(Pagers(userId, bot, localization).bugPagerProps(originator)) + + @Bean + def historyPagerActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) => + actorSystem.actorOf(Pagers(userId, bot, localization).historyPagerProps(originator)) + + @Bean + def monitoringsPagerActorFactory: (UserId, ActorRef) => ActorRef = (userId, originator) => + actorSystem.actorOf(Pagers(userId, bot, localization).monitoringsPagerProps(originator)) + + + @Bean + def router: ActorRef = actorSystem.actorOf(Router.props(authActorFactory)) + + @Bean + def telegram: TelegramBot = new TelegramBot(router ! _, telegramBotToken) + + @Bean + def bot: Bot = new Bot(telegram) +} diff --git a/server/src/main/scala/com/lbs/server/actor/Auth.scala b/server/src/main/scala/com/lbs/server/actor/Auth.scala new file mode 100644 index 0000000..92fd346 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Auth.scala @@ -0,0 +1,104 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{Actor, ActorRef, PoisonPill, Props} +import com.lbs.bot.model.{Command, MessageSource} +import com.lbs.common.Logger +import com.lbs.server.actor.Chat.Init +import com.lbs.server.actor.Login.{LoggedIn, UserId} +import com.lbs.server.service.DataService +import com.lbs.server.util.MessageExtractors._ + +class Auth(val source: MessageSource, dataService: DataService, unauthorizedHelpActorFactory: MessageSource => ActorRef, + loginActorFactory: (MessageSource, ActorRef) => ActorRef, chatActorFactory: UserId => ActorRef) extends Actor with Logger { + + private val loginActor = loginActorFactory(source, self) + private val unauthorizedHelpActor: ActorRef = unauthorizedHelpActorFactory(source) + + private var userId: Option[UserId] = getUserId + private var chatActor: ActorRef = _ + + override def receive: Receive = { + case cmd@Command(_, Text("/help"), _) if userId.isEmpty => + unauthorizedHelpActor ! cmd + case cmd@Command(_, Text("/start"), _) if userId.isEmpty => + unauthorizedHelpActor ! cmd + case cmd@Command(_, Text("/login"), _) => + userId = None + loginActor ! Init + loginActor ! cmd + case cmd: Command if userId.isEmpty => + loginActor ! cmd + case cmd: Command if userId.nonEmpty => + chatActor = getChatActor(userId.get) + chatActor ! cmd + case LoggedIn(forwardCommand, id) => + val uId = UserId(id, source) + val cmd = forwardCommand.cmd + userId = Some(uId) + chatActor = getChatActor(uId, reinit = true) + if (!cmd.message.text.contains("/login")) + chatActor ! cmd + case cmd: Command => + chatActor ! cmd + } + + private def getChatActor(userId: UserId, reinit: Boolean = false): ActorRef = { + if (chatActor == null) { + chatActorFactory(userId) + } else { + if (reinit) { + chatActor ! PoisonPill + chatActorFactory(userId) + } else chatActor + } + } + + def getUserId: Option[UserId] = { + val userIdMaybe = dataService.findUserIdBySource(source) + userIdMaybe.map(id => UserId(id, source)) + } + + override def postStop(): Unit = { + loginActor ! PoisonPill + unauthorizedHelpActor ! PoisonPill + if (chatActor != null) chatActor ! PoisonPill + } +} + +object Auth { + def props(source: MessageSource, dataService: DataService, unauthorizedHelpActorFactory: MessageSource => ActorRef, + loginActorFactory: (MessageSource, ActorRef) => ActorRef, chatActorFactory: UserId => ActorRef): Props = + Props(classOf[Auth], source, dataService, unauthorizedHelpActorFactory, loginActorFactory, chatActorFactory) +} + + + + + + + + + diff --git a/server/src/main/scala/com/lbs/server/actor/Book.scala b/server/src/main/scala/com/lbs/server/actor/Book.scala new file mode 100644 index 0000000..c5ed027 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Book.scala @@ -0,0 +1,342 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import java.time.ZonedDateTime + +import akka.actor.{ActorRef, PoisonPill, Props} +import com.lbs.api.json.model._ +import com.lbs.bot._ +import com.lbs.bot.model.{Button, Command} +import com.lbs.server.actor.Book._ +import com.lbs.server.actor.Chat.Init +import com.lbs.server.actor.DatePicker.{DateFromMode, DateToMode} +import com.lbs.server.actor.Login.UserId +import com.lbs.server.actor.StaticData.StaticDataConfig +import com.lbs.server.lang.{Localizable, Localization} +import com.lbs.server.repository.model.Monitoring +import com.lbs.server.service.{ApiService, DataService, MonitoringService} +import com.lbs.server.util.ServerModelConverters._ + +import scala.util.{Failure, Success, Try} + +class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: DataService, monitoringService: MonitoringService, + val localization: Localization, datePickerActorFactory: (UserId, ActorRef) => ActorRef, staticDataActorFactory: (UserId, ActorRef) => ActorRef, + termsPagerActorFactory: (UserId, ActorRef) => ActorRef) extends SafeFSM[FSMState, FSMData] with StaticDataForBooking with Localizable { + + private val datePicker = datePickerActorFactory(userId, self) + protected val staticData = staticDataActorFactory(userId, self) + private val termsPager = termsPagerActorFactory(userId, self) + + startWith(RequestCity, BookingData()) + + requestStaticData(RequestCity, AwaitCity, cityConfig) { bd: BookingData => + withFunctions( + latestOptions = dataService.getLatestCities(userId.userId), + staticOptions = apiService.getAllCities(userId.userId), + applyId = id => bd.copy(cityId = id)) + }(requestNext = RequestClinic) + + requestStaticData(RequestClinic, AwaitClinic, clinicConfig) { bd: BookingData => + withFunctions( + latestOptions = dataService.getLatestClinicsByCityId(userId.userId, bd.cityId.id), + staticOptions = apiService.getAllClinics(userId.userId, bd.cityId.id), + applyId = id => bd.copy(clinicId = id)) + }(requestNext = RequestService) + + requestStaticData(RequestService, AwaitService, serviceConfig) { bd: BookingData => + withFunctions( + latestOptions = dataService.getLatestServicesByCityIdAndClinicId(userId.userId, bd.cityId.id, bd.clinicId.optionalId), + staticOptions = apiService.getAllServices(userId.userId, bd.cityId.id, bd.clinicId.optionalId), + applyId = id => bd.copy(serviceId = id)) + }(requestNext = RequestDoctor) + + requestStaticData(RequestDoctor, AwaitDoctor, doctorConfig) { bd: BookingData => + withFunctions( + latestOptions = dataService.getLatestDoctorsByCityIdAndClinicIdAndServiceId(userId.userId, bd.cityId.id, bd.clinicId.optionalId, bd.serviceId.id), + staticOptions = apiService.getAllDoctors(userId.userId, bd.cityId.id, bd.clinicId.optionalId, bd.serviceId.id), + applyId = id => bd.copy(doctorId = id)) + }(requestNext = RequestDateFrom) + + whenSafe(RequestDateFrom) { + case Event(_, bookingData: BookingData) => + datePicker ! DateFromMode + datePicker ! bookingData.dateFrom + goto(AwaitDateFrom) + } + + whenSafe(AwaitDateFrom) { + case Event(cmd: Command, _) => + datePicker ! cmd + stay() + case Event(date: ZonedDateTime, bookingData: BookingData) => + invokeNext() + goto(RequestDateTo) using bookingData.copy(dateFrom = date) + } + + whenSafe(RequestDateTo) { + case Event(_, bookingData: BookingData) => + datePicker ! DateToMode + datePicker ! bookingData.dateFrom.plusDays(1) + goto(AwaitDateTo) + } + + whenSafe(AwaitDateTo) { + case Event(cmd: Command, _) => + datePicker ! cmd + stay() + case Event(date: ZonedDateTime, bookingData: BookingData) => + invokeNext() + goto(RequestDayTime) using bookingData.copy(dateTo = date) + } + + whenSafe(RequestDayTime) { + case Event(Next, _: BookingData) => + bot.sendMessage(userId.source, lang.chooseTimeOfDay, + inlineKeyboard = createInlineKeyboard(lang.timeOfDay.map { case (id, label) => Button(label, id.toString) }.toSeq, columns = 1)) + goto(AwaitDayTime) + } + + whenSafe(AwaitDayTime) { + case Event(Command(_, msg, Some(timeIdStr)), bookingData: BookingData) => + invokeNext() + val timeId = timeIdStr.toInt + bot.sendEditMessage(userId.source, msg.messageId, lang.preferredTimeIs(timeId)) + goto(RequestAction) using bookingData.copy(timeOfDay = timeId) + } + + whenSafe(RequestAction) { + case Event(Next, bookingData: BookingData) => + dataService.storeAppointment(userId.userId, bookingData) + bot.sendMessage(userId.source, + lang.bookingSummary(bookingData), + inlineKeyboard = createInlineKeyboard(Seq(Button(lang.findTerms, Tags.FindTerms), Button(lang.modifyDate, Tags.ModifyDate)))) + goto(AwaitAction) + } + + whenSafe(AwaitAction) { + case Event(Command(_, _, Some(Tags.FindTerms)), _) => + invokeNext() + goto(RequestTerm) + case Event(Command(_, _, Some(Tags.ModifyDate)), _) => + invokeNext() + goto(RequestDateFrom) + } + + whenSafe(RequestTerm) { + case Event(Next, bookingData: BookingData) => + val availableTerms = apiService.getAvailableTerms(userId.userId, bookingData.cityId.id, + bookingData.clinicId.optionalId, bookingData.serviceId.id, bookingData.doctorId.optionalId, + bookingData.dateFrom, Some(bookingData.dateTo), timeOfDay = bookingData.timeOfDay) + termsPager ! availableTerms + goto(AwaitTerm) + } + + whenSafe(AwaitTerm) { + case Event(Command(_, _, Some(Tags.ModifyDate)), _) => + invokeNext() + goto(RequestDateFrom) + case Event(Command(_, _, Some(Tags.CreateMonitoring)), _) => + invokeNext() + goto(AskMonitoringOptions) + case Event(cmd: Command, _) => + termsPager ! cmd + stay() + case Event(term: AvailableVisitsTermPresentation, _) => + self ! term + goto(RequestReservation) + case Event(Pager.NoItemsFound, _) => + bot.sendMessage(userId.source, lang.noTermsFound, inlineKeyboard = + createInlineKeyboard(Seq(Button(lang.modifyDate, Tags.ModifyDate), Button(lang.createMonitoring, Tags.CreateMonitoring)))) + stay() + } + + whenSafe(RequestReservation) { + case Event(term: AvailableVisitsTermPresentation, bookingData: BookingData) => + val response = apiService.temporaryReservation(userId.userId, term.mapTo[TemporaryReservationRequest], term.mapTo[ValuationsRequest]) + response match { + case Left(ex) => + bot.sendMessage(userId.source, ex.getMessage) + invokeNext() + stay() + 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)) + } + } + + whenSafe(AwaitReservation) { + case Event(Command(_, _, Some(Tags.Cancel)), bookingData: BookingData) => + apiService.deleteTemporaryReservation(userId.userId, bookingData.temporaryReservationId.get) + stay() + case Event(Command(_, _, Some(Tags.Book)), bookingData: BookingData) => + val reservationRequestMaybe = for { + tmpReservationId <- bookingData.temporaryReservationId + valuations <- bookingData.valuations + visitTermVariant <- valuations.visitTermVariants.headOption + term <- bookingData.term + } yield (tmpReservationId, visitTermVariant, term).mapTo[ReservationRequest] + + reservationRequestMaybe match { + case Some(reservationRequest) => + apiService.reservation(userId.userId, reservationRequest) match { + case Left(ex) => + bot.sendMessage(userId.source, ex.getMessage) + invokeNext() + stay() + case Right(success) => + log.debug(s"Successfully confirmed: $success") + bot.sendMessage(userId.source, lang.appointmentIsConfirmed) + stay() + } + } + + } + + whenSafe(AskMonitoringOptions) { + case Event(Next, _) => + bot.sendMessage(userId.source, lang.chooseTypeOfMonitoring, + inlineKeyboard = createInlineKeyboard(Seq(Button(lang.bookByApplication, Tags.BookByApplication), Button(lang.bookManually, Tags.BookManually)), columns = 1)) + stay() + case Event(Command(_, _, Some(autobookStr)), bookingData: BookingData) => + val autobook = autobookStr.toBoolean + invokeNext() + goto(CreateMonitoring) using bookingData.copy(autobook = autobook) + } + + whenSafe(CreateMonitoring) { + case Event(Next, bookingData: BookingData) => + LOG.debug(s"Creating monitoring for $bookingData") + Try(monitoringService.createMonitoring((userId -> bookingData).mapTo[Monitoring])) match { + case Success(_) => bot.sendMessage(userId.source, lang.monitoringHasBeenCreated) + case Failure(ex) => + LOG.error("Unable to create monitoring", ex) + bot.sendMessage(userId.source, lang.unableToCreateMonitoring) + } + goto(RequestCity) using BookingData() + } + + whenUnhandledSafe { + case Event(Init, _) => + reinit() + case e: Event => + LOG.error(s"Unhandled event in state:$stateName. Event: $e") + stay() + } + + private def cityConfig = StaticDataConfig(lang.city, "Wrocław", isAnyAllowed = false) + + private def clinicConfig = StaticDataConfig(lang.clinic, "Swobodna 1", isAnyAllowed = true) + + private def serviceConfig = StaticDataConfig(lang.service, "Stomatolog", isAnyAllowed = false) + + private def doctorConfig = StaticDataConfig(lang.doctor, "Bartniak", isAnyAllowed = true) + + private def reinit() = { + invokeNext() + datePicker ! Init + staticData ! Init + termsPager ! Init + goto(RequestCity) using BookingData() + } + + initialize() + + override def postStop(): Unit = { + datePicker ! PoisonPill + staticData ! PoisonPill + termsPager ! PoisonPill + super.postStop() + } +} + +object Book { + + def props(userId: UserId, bot: Bot, apiService: ApiService, dataService: DataService, monitoringService: MonitoringService, + localization: Localization, datePickerActorFactory: (UserId, ActorRef) => ActorRef, + staticDataActorFactory: (UserId, ActorRef) => ActorRef, termsPagerActorFactory: (UserId, ActorRef) => ActorRef): Props = + Props(classOf[Book], userId, bot, apiService, dataService, monitoringService, localization, datePickerActorFactory, + staticDataActorFactory, termsPagerActorFactory) + + object RequestCity extends FSMState + + object AwaitCity extends FSMState + + object RequestClinic extends FSMState + + object AwaitClinic extends FSMState + + object RequestService extends FSMState + + object AwaitService extends FSMState + + object RequestDoctor extends FSMState + + object AwaitDoctor extends FSMState + + object CreateMonitoring extends FSMState + + object AskMonitoringOptions extends FSMState + + object RequestDateFrom extends FSMState + + object AwaitDateFrom extends FSMState + + object RequestDateTo extends FSMState + + object AwaitDateTo extends FSMState + + object RequestDayTime extends FSMState + + object AwaitDayTime extends FSMState + + object RequestAction extends FSMState + + object AwaitAction extends FSMState + + object RequestTerm extends FSMState + + object AwaitTerm extends FSMState + + object RequestReservation extends FSMState + + object AwaitReservation extends FSMState + + 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), timeOfDay: Int = 0, autobook: Boolean = false, term: Option[AvailableVisitsTermPresentation] = None, + temporaryReservationId: Option[Long] = None, valuations: Option[ValuationsResponse] = None) extends FSMData + + object Tags { + val Cancel = "cancel" + val Book = "book" + val FindTerms = "find_terms" + val ModifyDate = "modify_date" + val CreateMonitoring = "create_monitoring" + val BookManually = "false" + val BookByApplication = "true" + } + +} diff --git a/server/src/main/scala/com/lbs/server/actor/Bug.scala b/server/src/main/scala/com/lbs/server/actor/Bug.scala new file mode 100644 index 0000000..24b4351 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Bug.scala @@ -0,0 +1,135 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{ActorRef, PoisonPill, Props} +import com.lbs.bot.model.{Button, Command} +import com.lbs.bot.{Bot, _} +import com.lbs.server.actor.Bug._ +import com.lbs.server.actor.Chat.Init +import com.lbs.server.actor.Login.UserId +import com.lbs.server.lang.{Localizable, Localization} +import com.lbs.server.repository.model +import com.lbs.server.service.DataService +import com.lbs.server.util.MessageExtractors + +class Bug(val userId: UserId, bot: Bot, dataService: DataService, bugPagerActorFactory: (UserId, ActorRef) => ActorRef, + val localization: Localization) extends SafeFSM[FSMState, FSMData] with Localizable { + + private val bugPager = bugPagerActorFactory(userId, self) + + startWith(RequestAction, null) + + whenSafe(RequestAction) { + case Event(Next, _) => + bot.sendMessage(userId.source, lang.bugAction, inlineKeyboard = + createInlineKeyboard(Seq(Button(lang.createNewBug, Tags.SubmitNew), Button(lang.showSubmittedBugs, Tags.ListSubmitted)))) + goto(AwaitAction) + } + + whenSafe(AwaitAction) { + case Event(Command(_, _, Some(Tags.SubmitNew)), _) => + bot.sendMessage(userId.source, lang.enterIssueDetails) + goto(AwaitBugDescription) + case Event(Command(_, _, Some(Tags.ListSubmitted)), _) => + invokeNext() + goto(RequestData) + } + + whenSafe(RequestData) { + case Event(Next, _) => + val bugs = dataService.getBugs(userId.userId) + bugPager ! Init + bugPager ! Right[Throwable, Seq[model.Bug]](bugs) + goto(AwaitPage) + } + + whenSafe(AwaitPage) { + case Event(cmd: Command, _) => + bugPager ! cmd + stay() + case Event(Pager.NoItemsFound, _) => + bot.sendMessage(userId.source, lang.noSubmittedIssuesFound) + goto(RequestData) + } + + whenSafe(AwaitBugDescription) { + case Event(Command(_, MessageExtractors.Text(details), _), _) => + val bugId = dataService.submitBug(userId.userId, userId.source.sourceSystem.id, details) + bot.sendMessage(userId.source, lang.bugHasBeenCreated(bugId.getOrElse(-1L))) + goto(RequestAction) using null + } + + whenUnhandledSafe { + case Event(Init, _) => + invokeNext() + bugPager ! Init + goto(RequestAction) + } + + initialize() + + override def postStop(): Unit = { + bugPager ! PoisonPill + super.postStop() + } +} + +object Bug { + def props(userId: UserId, bot: Bot, dataService: DataService, bugPagerActorFactory: (UserId, ActorRef) => ActorRef, localization: Localization): Props = + Props(classOf[Bug], userId, bot, dataService, bugPagerActorFactory, localization) + + object RequestBugDetails extends FSMState + + object AwaitBugDescription extends FSMState + + object RequestAction extends FSMState + + object AwaitAction extends FSMState + + object RequestData extends FSMState + + object AwaitPage extends FSMState + + object Tags { + val SubmitNew = "submit" + val ListSubmitted = "list" + } + +} + + + + + + + + + + + + + + + diff --git a/server/src/main/scala/com/lbs/server/actor/Chat.scala b/server/src/main/scala/com/lbs/server/actor/Chat.scala new file mode 100644 index 0000000..6aa5ff1 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Chat.scala @@ -0,0 +1,196 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{ActorRef, PoisonPill, Props} +import com.lbs.bot.model.Command +import com.lbs.common.Logger +import com.lbs.server.actor.Chat._ +import com.lbs.server.actor.Login.UserId +import com.lbs.server.service.{DataService, MonitoringService} +import com.lbs.server.util.MessageExtractors._ + +import scala.util.matching.Regex + +class Chat(val userId: UserId, dataService: DataService, monitoringService: MonitoringService, bookingActorFactory: UserId => ActorRef, helpActorFactory: UserId => ActorRef, + monitoringsActorFactory: UserId => ActorRef, historyActorFactory: UserId => ActorRef, + visitsActorFactory: UserId => ActorRef, settingsActorFactory: UserId => ActorRef, + bugActorFactory: UserId => ActorRef) extends SafeFSM[FSMState, FSMData] with Logger { + + private val bookingActor = bookingActorFactory(userId) + private val helpActor = helpActorFactory(userId) + private val monitoringsActor = monitoringsActorFactory(userId) + private val historyActor = historyActorFactory(userId) + private val visitsActor = visitsActorFactory(userId) + private val settingsActor = settingsActorFactory(userId) + private val bugActor = bugActorFactory(userId) + + startWith(HelpChat, null) + + when(HelpChat, helpActor) { + case Event(cmd@Command(_, Text("/help"), _), _) => + helpActor ! cmd + stay() + case Event(cmd@Command(_, Text("/start"), _), _) => + helpActor ! cmd + stay() + } + + when(BookChat, bookingActor) { + case Event(Command(_, Text("/book"), _), _) => + bookingActor ! Init + stay() + } + + when(HistoryChat, historyActor) { + case Event(Command(_, Text("/history"), _), _) => + historyActor ! Init + stay() + } + + when(VisitsChat, visitsActor) { + case Event(Command(_, Text("/visits"), _), _) => + visitsActor ! Init + stay() + } + + when(BugChat, bugActor) { + case Event(Command(_, Text("/bug"), _), _) => + bugActor ! Init + goto(BugChat) + } + + when(MonitoringsChat, monitoringsActor) { + case Event(Command(_, Text("/monitorings"), _), _) => + monitoringsActor ! Init + stay() + } + + when(SettingsChat, settingsActor) { + case Event(Command(_, Text("/settings"), _), _) => + settingsActor ! Init + stay() + } + + private def when(state: FSMState, actor: ActorRef)(mainStateFunction: StateFunction): Unit = { + whenSafe(state) { + case event: Event => + if (mainStateFunction.isDefinedAt(event)) mainStateFunction(event) + else { + val secondaryStateFunction = secondaryState(actor) + if (secondaryStateFunction.isDefinedAt(event)) secondaryStateFunction(event) + else eventHandler(event) + } + } + } + + private def secondaryState(actor: ActorRef): StateFunction = { + case Event(cmd@Command(_, Text("/bug"), _), _) => + self ! cmd + goto(BugChat) + case Event(cmd@Command(_, Text("/help"), _), _) => + self ! cmd + goto(HelpChat) + case Event(cmd@Command(_, Text("/start"), _), _) => + self ! cmd + goto(HelpChat) + case Event(cmd@Command(_, Text("/book"), _), _) => + self ! cmd + goto(BookChat) + case Event(cmd@Command(_, Text("/monitorings"), _), _) => + self ! cmd + goto(MonitoringsChat) + case Event(cmd@Command(_, Text("/history"), _), _) => + self ! cmd + goto(HistoryChat) + case Event(cmd@Command(_, Text("/visits"), _), _) => + self ! cmd + goto(VisitsChat) + case Event(cmd@Command(_, Text("/settings"), _), _) => + self ! cmd + goto(SettingsChat) + case Event(cmd@Command(_, Text(MonitoringId(monitoringIdStr, scheduleIdStr, timeStr)), _), _) => + val monitoringId = monitoringIdStr.toLong + val scheduleId = scheduleIdStr.toLong + val time = timeStr.toLong + monitoringService.bookAppointmentByScheduleId(userId.userId, monitoringId, scheduleId, time) + stay() + case Event(cmd: Command, _) => + actor ! cmd + stay() + } + + whenUnhandledSafe { + case e: Event => + LOG.debug(s"Unhandled event in state:$stateName. Event: $e") + stay() + } + + initialize() + + override def postStop(): Unit = { + bookingActor ! PoisonPill + helpActor ! PoisonPill + monitoringsActor ! PoisonPill + historyActor ! PoisonPill + visitsActor ! PoisonPill + settingsActor ! PoisonPill + bugActor ! PoisonPill + super.postStop() + } +} + +object Chat { + def props(userId: UserId, dataService: DataService, monitoringService: MonitoringService, bookingActorFactory: UserId => ActorRef, helpActorFactory: UserId => ActorRef, + monitoringsActorFactory: UserId => ActorRef, historyActorFactory: UserId => ActorRef, + visitsActorFactory: UserId => ActorRef, settingsActorFactory: UserId => ActorRef, bugActorFactory: UserId => ActorRef): Props = + Props(classOf[Chat], userId, dataService, monitoringService, bookingActorFactory, helpActorFactory, monitoringsActorFactory, + historyActorFactory, visitsActorFactory, settingsActorFactory, bugActorFactory) + + object HelpChat extends FSMState + + object BookChat extends FSMState + + object MonitoringsChat extends FSMState + + object HistoryChat extends FSMState + + object VisitsChat extends FSMState + + object SettingsChat extends FSMState + + object BugChat extends FSMState + + object Init + + val MonitoringId: Regex = s"/reserve_(\\d+)_(\\d+)_(\\d+)".r + +} + + + + + + + diff --git a/server/src/main/scala/com/lbs/server/actor/DatePicker.scala b/server/src/main/scala/com/lbs/server/actor/DatePicker.scala new file mode 100644 index 0000000..4381332 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/DatePicker.scala @@ -0,0 +1,155 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import java.time.format.TextStyle +import java.time.{LocalTime, ZonedDateTime} + +import akka.actor.{ActorRef, Props} +import com.lbs.bot.model.{Button, Command} +import com.lbs.bot.{Bot, _} +import com.lbs.server.actor.Chat.Init +import com.lbs.server.actor.DatePicker._ +import com.lbs.server.actor.Login.UserId +import com.lbs.server.lang.{Localizable, Localization} + +/** + * Date picker Inline Keyboard + * + * ⬆ ⬆ ⬆ + * dd MM yyyy + * ⬇ ⬇ ⬇ + * + */ +class DatePicker(val userId: UserId, val bot: Bot, val localization: Localization, originator: ActorRef) extends SafeFSM[FSMState, ZonedDateTime] with Localizable { + + startWith(AwaitMode, null) + + private var mode: Mode = DateFromMode + + whenSafe(AwaitMode) { + case Event(newMode: Mode, _) => + mode = newMode + goto(RequestDate) + } + + whenSafe(RequestDate) { + case Event(initialDate: ZonedDateTime, _) => + val message = mode match { + case DateFromMode => lang.chooseDateFrom + case DateToMode => lang.chooseDateTo + } + bot.sendMessage(userId.source, message, inlineKeyboard = dateButtons(initialDate)) + goto(AwaitDate) using initialDate + } + + whenSafe(AwaitDate) { + case Event(Command(_, msg, Some(Tags.Done)), finalDate: ZonedDateTime) => + + val (message, updatedDate) = mode match { + case DateFromMode => + val startOfTheDay = finalDate.`with`(LocalTime.MIN) + val dateFrom = if (startOfTheDay.isBefore(ZonedDateTime.now())) finalDate else startOfTheDay + lang.dateFromIs(dateFrom) -> dateFrom + case DateToMode => + val dateTo = finalDate.`with`(LocalTime.MAX).minusHours(2) + lang.dateToIs(dateTo) -> dateTo + } + bot.sendEditMessage(userId.source, msg.messageId, message) + originator ! updatedDate + goto(AwaitMode) using null + + case Event(Command(_, msg, Some(tag)), date: ZonedDateTime) => + val modifiedDate = modifyDate(date, tag) + bot.sendEditMessage(userId.source, msg.messageId, inlineKeyboard = dateButtons(modifiedDate)) + stay() using modifiedDate + } + + whenUnhandledSafe { + case Event(Init, _) => + goto(AwaitMode) using null + case e: Event => + LOG.error(s"Unhandled event in state:$stateName. Event: $e") + stay() + } + + initialize() + + private def modifyDate(date: ZonedDateTime, tag: String) = { + val dateModifier = tag match { + case Tags.DayUp => date.plusDays _ + case Tags.MonthUp => date.plusMonths _ + case Tags.YearUp => date.plusYears _ + case Tags.DayDown => date.minusDays _ + case Tags.MonthDown => date.minusMonths _ + case Tags.YearDown => date.minusYears _ + } + dateModifier(1) + } + + private def dateButtons(date: ZonedDateTime) = { + val day = date.getDayOfMonth.toString + val dayOfWeek = date.getDayOfWeek.getDisplayName(TextStyle.SHORT, lang.locale) + val month = date.getMonth.getDisplayName(TextStyle.SHORT, lang.locale) + val year = date.getYear.toString + + createInlineKeyboard(Seq( + Seq(Button("⬆", Tags.DayUp), Button("⬆", Tags.MonthUp), Button("⬆", Tags.YearUp)), + Seq(Button(s"$day ($dayOfWeek)"), Button(month), Button(year)), + Seq(Button("⬇", Tags.DayDown), Button("⬇", Tags.MonthDown), Button("⬇", Tags.YearDown)), + Seq(Button("Done", Tags.Done)) + )) + } +} + +object DatePicker { + def props(userId: UserId, bot: Bot, localization: Localization, originator: ActorRef): Props = + Props(classOf[DatePicker], userId, bot, localization, originator) + + object RequestDate extends FSMState + + object AwaitDate extends FSMState + + object AwaitMode extends FSMState + + trait Mode + + object DateFromMode extends Mode + + object DateToMode extends Mode + + object Tags { + val DayUp = "day_up" + val MonthUp = "month_up" + val YearUp = "year_up" + val DayDown = "day_down" + val MonthDown = "month_down" + val YearDown = "year_down" + val Done = "done" + } + +} + + + diff --git a/server/src/main/scala/com/lbs/server/actor/FSMData.scala b/server/src/main/scala/com/lbs/server/actor/FSMData.scala new file mode 100644 index 0000000..7727ba6 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/FSMData.scala @@ -0,0 +1,26 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +trait FSMData diff --git a/server/src/main/scala/com/lbs/server/actor/FSMState.scala b/server/src/main/scala/com/lbs/server/actor/FSMState.scala new file mode 100644 index 0000000..7bb3dc5 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/FSMState.scala @@ -0,0 +1,26 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +trait FSMState diff --git a/server/src/main/scala/com/lbs/server/actor/Help.scala b/server/src/main/scala/com/lbs/server/actor/Help.scala new file mode 100644 index 0000000..d2c7a94 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Help.scala @@ -0,0 +1,50 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{Actor, Props} +import com.lbs.bot.Bot +import com.lbs.bot.model.Command +import com.lbs.server.actor.Login.UserId +import com.lbs.server.lang.{Localizable, Localization} + +class Help(val userId: UserId, bot: Bot, val localization: Localization) extends Actor with Localizable { + override def receive: Receive = { + case _: Command => + bot.sendMessage(userId.source, lang.help) + } +} + +object Help { + def props(userId: UserId, bot: Bot, localization: Localization): Props = Props(classOf[Help], userId, bot, localization) +} + + + + + + + + + diff --git a/server/src/main/scala/com/lbs/server/actor/History.scala b/server/src/main/scala/com/lbs/server/actor/History.scala new file mode 100644 index 0000000..49da8eb --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/History.scala @@ -0,0 +1,97 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{ActorRef, PoisonPill, Props} +import com.lbs.api.json.model.HistoricVisit +import com.lbs.bot.Bot +import com.lbs.bot.model.Command +import com.lbs.server.actor.Chat.Init +import com.lbs.server.actor.History.{AwaitPage, RequestData} +import com.lbs.server.actor.Login.UserId +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, historyPagerActorFactory: (UserId, ActorRef) => ActorRef) extends SafeFSM[FSMState, FSMData] with Localizable { + + private val historyPager = historyPagerActorFactory(userId, self) + + startWith(RequestData, null) + + whenSafe(RequestData) { + case Event(Next, _) => + val visits = apiService.visitsHistory(userId.userId) + historyPager ! visits + goto(AwaitPage) + } + + whenSafe(AwaitPage) { + case Event(cmd: Command, _) => + historyPager ! cmd + stay() + case Event(Pager.NoItemsFound, _) => + bot.sendMessage(userId.source, lang.visitsHistoryIsEmpty) + goto(RequestData) + case Event(_: HistoricVisit, _) => + goto(RequestData) using null + } + + whenUnhandledSafe { + case Event(Init, _) => + invokeNext() + historyPager ! Init + goto(RequestData) + } + + initialize() + + override def postStop(): Unit = { + historyPager ! PoisonPill + super.postStop() + } +} + +object History { + def props(userId: UserId, bot: Bot, apiService: ApiService, localization: Localization, historyPagerActorFactory: (UserId, ActorRef) => ActorRef): Props = + Props(classOf[History], userId, bot, apiService, localization, historyPagerActorFactory) + + object RequestData extends FSMState + + object AwaitPage extends FSMState + + +} + + + + + + + + + + + + + diff --git a/server/src/main/scala/com/lbs/server/actor/Login.scala b/server/src/main/scala/com/lbs/server/actor/Login.scala new file mode 100644 index 0000000..fede315 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Login.scala @@ -0,0 +1,126 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{ActorRef, Props} +import com.lbs.bot.Bot +import com.lbs.bot.model.{Command, MessageSource} +import com.lbs.bot.telegram.TelegramBot +import com.lbs.server.actor.Chat.Init +import com.lbs.server.actor.Login._ +import com.lbs.server.lang.{Localizable, Localization} +import com.lbs.server.service.{ApiService, DataService} +import com.lbs.server.util.MessageExtractors +import org.jasypt.util.text.TextEncryptor + +class Login(source: MessageSource, bot: Bot, dataService: DataService, apiService: ApiService, textEncryptor: TextEncryptor, val localization: Localization, originator: ActorRef) extends SafeFSM[FSMState, FSMData] with Localizable { + + protected var userId: UserId = _ + + startWith(LogIn, LoginData()) + + private var forwardCommand: ForwardCommand = _ + + whenSafe(LogIn) { + case Event(cmd: Command, LoginData(None, None)) => + forwardCommand = ForwardCommand(cmd) + invokeNext() + goto(RequestUsername) + case Event(_, LoginData(Some(username), Some(password))) => + val loginResult = apiService.login(username, password) + loginResult match { + case Left(error) => + bot.sendMessage(source, error.getMessage) + invokeNext() + goto(RequestUsername) using LoginData() + case Right(loggedIn) => + val credentials = dataService.saveCredentials(source, username, password) + userId = UserId(credentials.userId, source) + apiService.addSession(credentials.userId, loggedIn.accessToken, loggedIn.tokenType) + bot.sendMessage(source, lang.loginAndPasswordAreOk) + originator ! LoggedIn(forwardCommand, credentials.userId) + stay() using null + } + } + + whenSafe(RequestUsername) { + case Event(Next, _) => + bot.sendMessage(source, lang.provideUsername) + goto(AwaitUsername) + } + + whenSafe(AwaitUsername) { + case Event(Command(_, MessageExtractors.TextOpt(username), _), loginData: LoginData) => + invokeNext() + goto(RequestPassword) using loginData.copy(username = username) + } + + whenSafe(RequestPassword) { + case Event(Next, _) => + bot.sendMessage(source, lang.providePassword) + goto(AwaitPassword) + } + + whenSafe(AwaitPassword) { + case Event(Command(_, MessageExtractors.TextOpt(password), _), loginData: LoginData) => + invokeNext() + goto(LogIn) using loginData.copy(password = password.map(textEncryptor.encrypt)) + } + + whenUnhandledSafe { + case Event(Init, _) => + goto(LogIn) using LoginData() + case e: Event => + LOG.error(s"Unhandled event in state:$stateName. Event: $e") + stay() + } + + initialize() + +} + +object Login { + + def props(source: MessageSource, bot: Bot, dataService: DataService, apiService: ApiService, textEncryptor: TextEncryptor, localization: Localization, originator: ActorRef): Props = + Props(classOf[Login], source, bot, dataService, apiService, textEncryptor, localization, originator) + + object LogIn extends FSMState + + object RequestUsername extends FSMState + + object AwaitUsername extends FSMState + + object RequestPassword extends FSMState + + object AwaitPassword extends FSMState + + case class LoginData(username: Option[String] = None, password: Option[String] = None) extends FSMData + + case class ForwardCommand(cmd: Command) + + case class UserId(userId: Long, source: MessageSource) + + case class LoggedIn(forwardCommand: ForwardCommand, userId: Long) + +} diff --git a/server/src/main/scala/com/lbs/server/actor/Monitorings.scala b/server/src/main/scala/com/lbs/server/actor/Monitorings.scala new file mode 100644 index 0000000..3a32b3f --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Monitorings.scala @@ -0,0 +1,113 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{ActorRef, PoisonPill, Props} +import com.lbs.bot._ +import com.lbs.bot.model.{Button, Command} +import com.lbs.server.actor.Chat.Init +import com.lbs.server.actor.Login.UserId +import com.lbs.server.actor.Monitorings.{AwaitDecision, AwaitPage, RequestData, Tags} +import com.lbs.server.lang.{Localizable, Localization} +import com.lbs.server.repository.model.Monitoring +import com.lbs.server.service.MonitoringService + +class Monitorings(val userId: UserId, bot: Bot, monitoringService: MonitoringService, val localization: Localization, monitoringsPagerActorFactory: (UserId, ActorRef) => ActorRef) extends SafeFSM[FSMState, Monitoring] with Localizable { + + private val monitoringsPager = monitoringsPagerActorFactory(userId, self) + + startWith(RequestData, null) + + whenSafe(RequestData) { + case Event(Next, _) => + val monitorings = monitoringService.getActiveMonitorings(userId.userId) + monitoringsPager ! Right[Throwable, Seq[Monitoring]](monitorings) + goto(AwaitPage) + } + + whenSafe(AwaitPage) { + case Event(cmd: Command, _) => + monitoringsPager ! cmd + stay() + case Event(Pager.NoItemsFound, _) => + bot.sendMessage(userId.source, lang.noActiveMonitorings) + goto(RequestData) + case Event(monitoring: Monitoring, _) => + bot.sendMessage(userId.source, lang.deactivateMonitoring(monitoring), inlineKeyboard = + createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes)))) + goto(AwaitDecision) using monitoring + } + + whenSafe(AwaitDecision) { + case Event(Command(_, _, Some(Tags.No)), _) => + bot.sendMessage(userId.source, lang.monitoringWasNotDeactivated) + goto(RequestData) + case Event(Command(_, _, Some(Tags.Yes)), monitoring: Monitoring) => + monitoringService.deactivateMonitoring(monitoring.recordId) + bot.sendMessage(userId.source, lang.deactivated) + goto(RequestData) + } + + whenUnhandledSafe { + case Event(Init, _) => + invokeNext() + monitoringsPager ! Init + goto(RequestData) + } + + initialize() + + override def postStop(): Unit = { + monitoringsPager ! PoisonPill + super.postStop() + } +} + +object Monitorings { + def props(userId: UserId, bot: Bot, monitoringService: MonitoringService, localization: Localization, monitoringsPagerActorFactory: (UserId, ActorRef) => ActorRef): Props = + Props(classOf[Monitorings], userId, bot, monitoringService, localization, monitoringsPagerActorFactory) + + object RequestData extends FSMState + + object AwaitPage extends FSMState + + object AwaitDecision extends FSMState + + object Tags { + val Yes = "yes" + val No = "no" + } + +} + + + + + + + + + + + diff --git a/server/src/main/scala/com/lbs/server/actor/Pager.scala b/server/src/main/scala/com/lbs/server/actor/Pager.scala new file mode 100644 index 0000000..87ce224 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Pager.scala @@ -0,0 +1,132 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{ActorRef, Props} +import com.lbs.bot.model.{Button, Command} +import com.lbs.bot.{Bot, _} +import com.lbs.common.Logger +import com.lbs.server.actor.Chat.Init +import com.lbs.server.actor.Login.UserId +import com.lbs.server.actor.Pager.{Tags, _} +import com.lbs.server.lang.{Localizable, Localization} +import com.lbs.server.util.MessageExtractors + +class Pager[Data](val userId: UserId, bot: Bot, makeMessage: (Data, Int, Int) => String, + makeHeader: (Int, Int) => String, selectionPrefix: Option[String], + val localization: Localization, originator: ActorRef) + extends SafeFSM[FSMState, FSMData] with Localizable with Logger { + + private case class Page(page: Int, pages: Seq[Seq[Data]]) extends FSMData + + private val Selection = s"/${selectionPrefix.getOrElse("")}_(\\d+)_(\\d+)".r + + startWith(PrepareData, null) + + whenSafe(PrepareData) { + case Event(Left(error: Throwable), _) => + bot.sendMessage(userId.source, error.getMessage) + invokeNext() + goto(PrepareData) + case Event(Right(items: Seq[Data]), _) if items.isEmpty => + originator ! NoItemsFound + goto(PrepareData) using null + case Event(Right(items: Seq[Data]), _) => + invokeNext() + goto(RequestData) using Page(0, items.grouped(Pager.PageSize).toList) + } + + whenSafe(RequestData) { + case Event(Next, page: Page) => + sendPage(page.page, page.pages) + goto(AwaitData) + } + + whenSafe(AwaitData) { + case Event(Command(_, msg, Some(Tags.Next)), termsData: Page) => + val page = termsData.page + 1 + sendPage(page, termsData.pages, Some(msg.messageId)) + stay() using termsData.copy(page = page) + case Event(Command(_, msg, Some(Tags.Previous)), termsData: Page) => + val page = termsData.page - 1 + sendPage(page, termsData.pages, Some(msg.messageId)) + stay() using termsData.copy(page = page) + case Event(Command(_, MessageExtractors.Text(Selection(pageStr, indexStr)), _), termsData: Page) if selectionPrefix.nonEmpty => + val page = pageStr.toInt + val index = indexStr.toInt + originator ! termsData.pages(page)(index) + goto(PrepareData) using null + } + + private def sendPage(page: Int, data: Seq[Seq[Data]], messageId: Option[String] = None): Unit = { + val pages = data.length + val message = makeHeader(page, data.length) + "\n\n" + data(page).zipWithIndex.map { case (d, index) => makeMessage(d, page, index) }.mkString + + val previousButton = if (page > 0) Some(Button(lang.previous, Tags.Previous)) else None + val nextButton = if (page >= 0 && page < pages - 1) Some(Button(lang.next, Tags.Next)) else None + val buttons = previousButton.toSeq ++ nextButton.toSeq + + messageId match { + case Some(id) => + bot.sendEditMessage(userId.source, id, message, inlineKeyboard = createInlineKeyboard(buttons)) + case None => + bot.sendMessage(userId.source, message, inlineKeyboard = createInlineKeyboard(buttons)) + } + } + + whenUnhandledSafe { + case Event(Init, _) => + goto(PrepareData) using null + case e: Event => + LOG.error(s"Unhandled event in state:$stateName. Event: $e") + stay() + } + + initialize() +} + +object Pager { + + def props[Data](userId: UserId, bot: Bot, + makeMessage: (Data, Int, Int) => String, makeHeader: (Int, Int) => String, dataPrefix: Option[String], localization: Localization, originator: ActorRef): Props = + Props(classOf[Pager[Data]], userId, bot, makeMessage, makeHeader, dataPrefix, localization, originator) + + val PageSize = 5 + + object PrepareData extends FSMState + + object RequestData extends FSMState + + object AwaitData extends FSMState + + object NoItemsFound + + object Tags { + val Previous = "previous" + val Next = "next" + } + +} + + diff --git a/server/src/main/scala/com/lbs/server/actor/Pagers.scala b/server/src/main/scala/com/lbs/server/actor/Pagers.scala new file mode 100644 index 0000000..1eef6d1 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Pagers.scala @@ -0,0 +1,70 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{ActorRef, Props} +import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit} +import com.lbs.bot.Bot +import com.lbs.server.actor.Login.UserId +import com.lbs.server.lang.{Localizable, Localization} +import com.lbs.server.repository.model +import com.lbs.server.repository.model.Monitoring + +class Pagers(val userId: UserId, bot: Bot, val localization: Localization) extends Localizable { + + def termsPagerProps(originator: ActorRef): Props = + Pager.props[AvailableVisitsTermPresentation](userId, bot, + (term: AvailableVisitsTermPresentation, page: Int, index: Int) => lang.termEntry(term, page, index), + (page: Int, pages: Int) => lang.termsHeader(page, pages), + Some("book"), localization, originator) + + def historyPagerProps(originator: ActorRef): Props = + Pager.props[HistoricVisit](userId, bot, + (visit: HistoricVisit, page: Int, index: Int) => lang.historyEntry(visit, page, index), + (page: Int, pages: Int) => lang.historyHeader(page, pages), + Some("repeat"), localization, originator) + + def visitsPagerProps(originator: ActorRef): Props = + Pager.props[ReservedVisit](userId, bot, + (visit: ReservedVisit, page: Int, index: Int) => lang.upcomingVisitEntry(visit, page, index), + (page: Int, pages: Int) => lang.upcomingVisitsHeader(page, pages), + Some("cancel"), localization, originator) + + def bugPagerProps(originator: ActorRef): Props = + Pager.props[model.Bug](userId, bot, + (bug: model.Bug, page: Int, index: Int) => lang.bugEntry(bug, page, index), + (page: Int, pages: Int) => lang.bugsHeader(page, pages), + None, localization, originator) + + def monitoringsPagerProps(originator: ActorRef): Props = + Pager.props[Monitoring](userId, bot, + (monitoring: Monitoring, page: Int, index: Int) => lang.monitoringEntry(monitoring, page, index), + (page: Int, pages: Int) => lang.monitoringsHeader(page, pages), + Some("cancel"), localization, originator) + +} + +object Pagers { + def apply(userId: UserId, bot: Bot, localization: Localization) = new Pagers(userId, bot, localization) +} diff --git a/server/src/main/scala/com/lbs/server/actor/Router.scala b/server/src/main/scala/com/lbs/server/actor/Router.scala new file mode 100644 index 0000000..4ad4b4a --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Router.scala @@ -0,0 +1,85 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{Actor, ActorRef, Cancellable, PoisonPill, Props} +import com.lbs.bot.model.{Command, MessageSource} +import com.lbs.common.Logger +import com.lbs.server.actor.Router.DestroyChat + +import scala.collection.mutable +import scala.concurrent.ExecutionContextExecutor +import scala.concurrent.duration.DurationLong + +class Router(authActorFactory: MessageSource => ActorRef) extends Actor with Logger { + + private val chats = mutable.Map.empty[MessageSource, ActorRef] + + private val timers = mutable.Map.empty[MessageSource, Cancellable] + + private val idleTimeout = 1.hour + + private implicit val dispatcher: ExecutionContextExecutor = context.system.dispatcher + + override def receive: Receive = { + case cmd@Command(source, _, _) => + scheduleIdleChatDestroyer(source) + val chat = chats.get(source) match { + case Some(actor) => actor + case None => + val actor = authActorFactory(source) + chats += source -> actor + actor + } + chat ! cmd + case DestroyChat(source) => + destroyChat(source) + case what => LOG.info(s"Unknown message: $what") + } + + private def destroyChat(source: MessageSource): Unit = { + LOG.info(s"Destroying chat for $source due to $idleTimeout inactivity") + timers.remove(source) + chats.remove(source).foreach(_ ! PoisonPill) + } + + private def scheduleIdleChatDestroyer(source: MessageSource): Unit = { + timers.remove(source).foreach(_.cancel()) + val cancellable = context.system.scheduler.scheduleOnce(idleTimeout) { + self ! DestroyChat(source) + } + timers += source -> cancellable + } + + override def postStop(): Unit = { + chats.foreach(_._2 ! PoisonPill) + } +} + +object Router { + def props(authActorFactory: MessageSource => ActorRef) = Props(classOf[Router], authActorFactory) + + case class DestroyChat(source: MessageSource) + +} diff --git a/server/src/main/scala/com/lbs/server/actor/SafeFSM.scala b/server/src/main/scala/com/lbs/server/actor/SafeFSM.scala new file mode 100644 index 0000000..a69263f --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/SafeFSM.scala @@ -0,0 +1,57 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.FSM +import com.lbs.common.Logger + +trait SafeFSM[S, D] extends FSM[S, D] with Logger { + + protected val defaultEventHandler: StateFunction = { + case e: Event => + LOG.warn(s"Unhandled event in state:$stateName. Event: $e") + stay() + } + + protected var eventHandler: StateFunction = defaultEventHandler + + protected def whenSafe(state: S)(stateFunction: StateFunction): Unit = { + when(state) { + case event: Event => + try { + if (stateFunction.isDefinedAt(event)) stateFunction(event) + else eventHandler(event) + } catch { + case e: Exception => + LOG.error(s"Exception occurred while processing event $event", e) + stay() + } + } + } + + protected def whenUnhandledSafe(stateFunction: StateFunction): Unit = { + whenUnhandled(stateFunction) + eventHandler = stateFunction orElse defaultEventHandler + } +} diff --git a/server/src/main/scala/com/lbs/server/actor/Settings.scala b/server/src/main/scala/com/lbs/server/actor/Settings.scala new file mode 100644 index 0000000..2c8e836 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Settings.scala @@ -0,0 +1,103 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.Props +import com.lbs.bot.model.{Button, Command} +import com.lbs.bot.{Bot, _} +import com.lbs.server.actor.Chat.Init +import com.lbs.server.actor.Login.UserId +import com.lbs.server.actor.Settings._ +import com.lbs.server.lang.{Lang, Localizable, Localization} +import com.lbs.server.service.DataService + +class Settings(val userId: UserId, bot: Bot, dataService: DataService, val localization: Localization) extends SafeFSM[FSMState, FSMData] with Localizable { + + startWith(RequestAction, null) + + whenSafe(RequestAction) { + case Event(Next, _) => + bot.sendMessage(userId.source, lang.settingsHeader, inlineKeyboard = + createInlineKeyboard(Seq(Button(lang.language, Tags.Language)))) + goto(AwaitAction) + } + + whenSafe(AwaitAction) { + case Event(Command(_, _, Some(Tags.Language)), _) => + bot.sendMessage(userId.source, lang.chooseLanguage, + inlineKeyboard = createInlineKeyboard(Lang.Langs.map(l => Button(l.label, l.id)), columns = 1)) + goto(AwaitLanguage) + } + + whenSafe(AwaitLanguage) { + case Event(Command(_, _, Some(langIdStr)), _) => + val langId = langIdStr.toInt + localization.updateLanguage(userId.userId, Lang(langId)) + bot.sendMessage(userId.source, lang.languageUpdated) + goto(RequestAction) using null + } + + whenUnhandledSafe { + case Event(Init, _) => + invokeNext() + goto(RequestAction) + } + + initialize() +} + +object Settings { + def props(userId: UserId, bot: Bot, dataService: DataService, localization: Localization): Props = + Props(classOf[Settings], userId, bot, dataService, localization) + + object AwaitLanguage extends FSMState + + object RequestAction extends FSMState + + object AwaitAction extends FSMState + + object Tags { + val Language = "language" + } + +} + + + + + + + + + + + + + + + + + + + diff --git a/server/src/main/scala/com/lbs/server/actor/StaticData.scala b/server/src/main/scala/com/lbs/server/actor/StaticData.scala new file mode 100644 index 0000000..b2f342d --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/StaticData.scala @@ -0,0 +1,133 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{ActorRef, Props} +import com.lbs.api.json.model.IdName +import com.lbs.bot.model.{Button, Command, TaggedButton} +import com.lbs.bot.{Bot, _} +import com.lbs.server.actor.Chat.Init +import com.lbs.server.actor.Login.UserId +import com.lbs.server.actor.StaticData._ +import com.lbs.server.lang.{Localizable, Localization} + +class StaticData(val userId: UserId, bot: Bot, val localization: Localization, originator: ActorRef) extends SafeFSM[FSMState, IdName] with Localizable { + + private def anySelectOption: List[TaggedButton] = if (config.isAnyAllowed) List(Button(lang.any, -1L)) else List() + + startWith(AwaitConfig, null) + + private var config: StaticDataConfig = _ + + private var callbackTags: List[TaggedButton] = List() + + whenSafe(AwaitConfig) { + case Event(newConfig: StaticDataConfig, _) => + config = newConfig + invokeNext() + goto(RequestStaticData) + } + + whenSafe(RequestStaticData) { + case Event(Next, _) => + originator ! LatestOptions + stay() + case Event(LatestOptions(options), _) if options.isEmpty => + callbackTags = anySelectOption + bot.sendMessage(userId.source, lang.pleaseEnterStaticDataNameOrAny(config), + inlineKeyboard = createInlineKeyboard(callbackTags)) + goto(AwaitStaticData) + case Event(LatestOptions(options), _) if options.nonEmpty => + callbackTags = anySelectOption ++ options.map(data => Button(data.name, data.id)) + bot.sendMessage(userId.source, lang.pleaseEnterStaticDataNameOrPrevious(config), + inlineKeyboard = createInlineKeyboard(callbackTags, columns = 1)) + goto(AwaitStaticData) + } + + whenSafe(AwaitStaticData) { + case Event(Command(_, msg, Some(tag)), _) => + val id = tag.toLong + val label = callbackTags.find(_.tag == tag).map(_.label).getOrElse(sys.error("Unable to get callback tag label")) + bot.sendEditMessage(userId.source, msg.messageId, lang.staticDataIs(config, label)) + originator ! IdName(id, label) + goto(AwaitConfig) + + case Event(Command(_, msg, _), _) => + val searchText = msg.text.get.toLowerCase + originator ! FindOptions(searchText) + stay() + + case Event(FoundOptions(Right(options)), _) if options.nonEmpty => + callbackTags = anySelectOption ::: options.map(c => Button(c.name, c.id)) + bot.sendMessage(userId.source, lang.pleaseChooseStaticDataNameOrAny(config), + inlineKeyboard = createInlineKeyboard(callbackTags, columns = 1)) + stay() + + case Event(FoundOptions(Right(options)), _) if options.isEmpty => + callbackTags = anySelectOption + bot.sendMessage(userId.source, lang.staticNotFound(config), inlineKeyboard = createInlineKeyboard(callbackTags)) + stay() + + case Event(FoundOptions(Left(ex)), _) => + bot.sendMessage(userId.source, ex.getMessage) + stay() + } + + whenUnhandledSafe { + case Event(Init, _) => + goto(AwaitConfig) using null + case e: Event => + LOG.error(s"Unhandled event in state:$stateName. Event: $e") + stay() + } + + initialize() +} + +object StaticData { + def props(userId: UserId, bot: Bot, localization: Localization, originator: ActorRef): Props = + Props(classOf[StaticData], userId, bot, localization, originator) + + object AwaitConfig extends FSMState + + object RequestStaticData extends FSMState + + object AwaitStaticData extends FSMState + + case class StaticDataConfig(name: String, example: String, isAnyAllowed: Boolean) + + object LatestOptions + + case class LatestOptions(options: Seq[IdName]) + + case class FindOptions(searchText: String) + + case class FoundOptions(option: Either[Throwable, List[IdName]]) + +} + + + + + diff --git a/server/src/main/scala/com/lbs/server/actor/StaticDataForBooking.scala b/server/src/main/scala/com/lbs/server/actor/StaticDataForBooking.scala new file mode 100644 index 0000000..37752ad --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/StaticDataForBooking.scala @@ -0,0 +1,69 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.ActorRef +import com.lbs.api.json.model.IdName +import com.lbs.bot.model.Command +import com.lbs.server.actor.Book.BookingData +import com.lbs.server.actor.StaticData.{FindOptions, FoundOptions, LatestOptions, StaticDataConfig} + +trait StaticDataForBooking extends SafeFSM[FSMState, FSMData] { + + protected def staticData: ActorRef + + protected def withFunctions(latestOptions: => Seq[IdName], staticOptions: => Either[Throwable, List[IdName]], applyId: IdName => BookingData): FSMState => StateFunction = { + nextState: FSMState => { + case Event(cmd: Command, _) => + staticData ! cmd + stay() + case Event(LatestOptions, _) => + staticData ! LatestOptions(latestOptions) + stay() + case Event(FindOptions(searchText), _) => + staticData ! FoundOptions(filterOptions(staticOptions, searchText)) + stay() + case Event(id: IdName, _) => + invokeNext() + goto(nextState) using applyId(id) + } + } + + protected def requestStaticData(requestState: FSMState, awaitState: FSMState, staticDataConfig: => StaticDataConfig)(functions: BookingData => FSMState => StateFunction)(requestNext: FSMState): Unit = { + whenSafe(requestState) { + case Event(_, _) => + staticData ! staticDataConfig + goto(awaitState) + } + whenSafe(awaitState) { + case event@Event(_, bookingData: BookingData) => + val fn = functions(bookingData)(requestNext) + if (fn.isDefinedAt(event)) fn(event) else eventHandler(event) + } + } + + private def filterOptions(options: Either[Throwable, List[IdName]], searchText: String) = { + options.map(opt => opt.filter(c => c.name.toLowerCase.contains(searchText))) + } +} diff --git a/server/src/main/scala/com/lbs/server/actor/UnauthorizedHelp.scala b/server/src/main/scala/com/lbs/server/actor/UnauthorizedHelp.scala new file mode 100644 index 0000000..caddd3c --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/UnauthorizedHelp.scala @@ -0,0 +1,50 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{Actor, Props} +import com.lbs.bot.Bot +import com.lbs.bot.model.{Command, MessageSource} +import com.lbs.server.lang.En + +class UnauthorizedHelp(source: MessageSource, bot: Bot) extends Actor { + override def receive: Receive = { + case _: Command => + bot.sendMessage(source, En.help) + } +} + +object UnauthorizedHelp { + def props(source: MessageSource, bot: Bot): Props = Props(classOf[UnauthorizedHelp], source, bot) +} + + + + + + + + + + diff --git a/server/src/main/scala/com/lbs/server/actor/Visits.scala b/server/src/main/scala/com/lbs/server/actor/Visits.scala new file mode 100644 index 0000000..2ff0996 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/Visits.scala @@ -0,0 +1,119 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.actor + +import akka.actor.{ActorRef, PoisonPill, Props} +import com.lbs.api.json.model.ReservedVisit +import com.lbs.bot.model.{Button, Command} +import com.lbs.bot.{Bot, _} +import com.lbs.server.actor.Chat.Init +import com.lbs.server.actor.Login.UserId +import com.lbs.server.actor.Visits.{AwaitDecision, AwaitPage, RequestData, Tags} +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, + visitsPagerActorFactory: (UserId, ActorRef) => ActorRef) extends SafeFSM[FSMState, ReservedVisit] with Localizable { + + private val reservedVisitsPager = visitsPagerActorFactory(userId, self) + + startWith(RequestData, null) + + whenSafe(RequestData) { + case Event(Next, _) => + val visits = apiService.reservedVisits(userId.userId) + reservedVisitsPager ! visits + goto(AwaitPage) + } + + whenSafe(AwaitPage) { + case Event(cmd: Command, _) => + reservedVisitsPager ! cmd + stay() + case Event(Pager.NoItemsFound, _) => + bot.sendMessage(userId.source, lang.noUpcomingVisits) + goto(RequestData) + case Event(visit: ReservedVisit, _) => + bot.sendMessage(userId.source, lang.areYouSureToCancelAppointment(visit), + inlineKeyboard = createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes)))) + goto(AwaitDecision) using visit + } + + whenSafe(AwaitDecision) { + case Event(Command(_, _, Some(Tags.No)), _) => + bot.sendMessage(userId.source, lang.appointmentWasNotCancelled) + goto(RequestData) + case Event(Command(_, _, Some(Tags.Yes)), visit: ReservedVisit) => + apiService.deleteReservation(userId.userId, visit.reservationId) match { + case Left(ex) => bot.sendMessage(userId.source, lang.unableToCancelUpcomingVisit(ex.getMessage)) + case Right(r) => bot.sendMessage(userId.source, lang.appointmentHasBeenCancelled) + } + goto(RequestData) + } + + whenUnhandledSafe { + case Event(Init, _) => + invokeNext() + reservedVisitsPager ! Init + goto(RequestData) + } + + initialize() + + override def postStop(): Unit = { + reservedVisitsPager ! PoisonPill + super.postStop() + } +} + +object Visits { + def props(userId: UserId, bot: Bot, apiService: ApiService, localization: Localization, + visitsPagerActorFactory: (UserId, ActorRef) => ActorRef): Props = + Props(classOf[Visits], userId, bot, apiService, localization, visitsPagerActorFactory) + + object RequestData extends FSMState + + object AwaitPage extends FSMState + + object AwaitDecision extends FSMState + + object Tags { + val Yes = "yes" + val No = "no" + } + +} + + + + + + + + + + + + + diff --git a/server/src/main/scala/com/lbs/server/actor/package.scala b/server/src/main/scala/com/lbs/server/actor/package.scala new file mode 100644 index 0000000..3dd4d0d --- /dev/null +++ b/server/src/main/scala/com/lbs/server/actor/package.scala @@ -0,0 +1,34 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server + +import akka.actor.ActorRef + +package object actor { + def invokeNext()(implicit self: ActorRef): Unit = { + self ! Next + } + + object Next +} diff --git a/server/src/main/scala/com/lbs/server/exception/UserNotFoundException.scala b/server/src/main/scala/com/lbs/server/exception/UserNotFoundException.scala new file mode 100644 index 0000000..72c0c7c --- /dev/null +++ b/server/src/main/scala/com/lbs/server/exception/UserNotFoundException.scala @@ -0,0 +1,26 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.exception + +case class UserNotFoundException(chatId: Long) extends Exception(s"Luxmed username for char with id $chatId") diff --git a/server/src/main/scala/com/lbs/server/lang/En.scala b/server/src/main/scala/com/lbs/server/lang/En.scala new file mode 100644 index 0000000..7175d70 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/lang/En.scala @@ -0,0 +1,351 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.lang + +import java.time.ZonedDateTime +import java.util.Locale + +import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit, ValuationsResponse} +import com.lbs.server.actor.Book +import com.lbs.server.actor.StaticData.StaticDataConfig +import com.lbs.server.repository.model.{Bug, Monitoring} +import com.lbs.server.util.DateTimeUtil.{formatDate, formatDateTime, minutesSinceBeginOf2018} + +object En extends Lang { + + override def id: Int = 0 + + override def locale: Locale = Locale.ENGLISH + + override def label: String = "🇺🇸󠁧󠁢󠁥󠁮󠁧󠁿English" + + override protected def withPages(message: String, page: Int, pages: Int): String = { + if (pages > 1) s"$message. Page ${page + 1} of $pages" + else message + } + + override def unableToCancelUpcomingVisit(reason: String): String = + s"⚠ Unable to cancel upcoming visit! Reason: $reason" + + override def appointmentHasBeenCancelled: String = + s"👍 Your appointment has been cancelled!" + + override def yes: String = "Yes" + + override def no: String = "No" + + override def noUpcomingVisits: String = + "ℹ No upcoming visits found" + + override def areYouSureToCancelAppointment(visit: ReservedVisit): 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} + |""".stripMargin + + override def chooseDateFrom: String = " Please choose date from" + + override def chooseDateTo: String = " Please choose date to" + + override def findTerms: String = "🔍 Find terms" + + override def modifyDate: String = "📅 Modify date" + + override def bookingSummary(bookingData: Book.BookingData): String = + s"🦄 Ok! We are going to book a service ${bookingData.serviceId.name}" + + s" with a doctor chosen ${bookingData.doctorId.name}" + + s" in ${bookingData.clinicId.name} clinic" + + s" of the ${bookingData.cityId.name} city." + + s"\nDesired dates: ${formatDate(bookingData.dateFrom, locale)} -> ${formatDate(bookingData.dateTo, locale)}" + + s"\nTime: ${timeOfDay(bookingData.timeOfDay)}" + + s"\n\n Now choose your action" + + override def noTermsFound: String = + s"""ℹ No available terms found + | + |What do you want to do next?""".stripMargin + + override def createMonitoring: String = "👀 Create monitoring" + + override def cancel: String = "Cancel" + + override def book: String = "Book" + + override def confirmAppointment(term: AvailableVisitsTermPresentation, valuations: ValuationsResponse): String = + + s""" ${valuations.optionsQuestion.getOrElse("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 + + override def appointmentIsConfirmed: String = "👍 Your appointment has been confirmed!" + + override def monitoringHasBeenCreated: String = "👍 Monitoring has been created! List of active /monitorings" + + override def unableToCreateMonitoring: String = s"👎 Unable to create monitoring. Please create a /bug" + + override def chooseTypeOfMonitoring: String = " Please choose type of monitoring you want" + + override def bookByApplication: String = "👾 Book by application" + + override def bookManually: String = "👤 Book manually" + + override def city: String = "city" + + override def clinic: String = "clinic" + + override def service: String = "service" + + override def doctor: String = "doctor" + + override def previous: String = "Previous" + + override def next: String = "Next" + + override def noActiveMonitorings: String = "ℹ You don't have active monitorings. Create new one /book" + + override def deactivateMonitoring(monitoring: Monitoring): String = + s""" Are you sure want to deactivate monitoring? + | + |📅 ${formatDate(monitoring.dateFrom, locale)} -> ${formatDate(monitoring.dateTo, locale)} + |⏱ ${timeOfDay(monitoring.timeOfDay)} + |${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName} + |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} + |${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName}""".stripMargin + + override def deactivated: String = "👍 Deactivated! List of active /monitorings" + + override def any: String = "Any" + + override def pressAny: String = s"or press $any button" + + override def pleaseEnterStaticDataNameOrAny(config: StaticDataConfig): String = + withAnyVariant( + s""" Please enter a ${config.name} name + |For example: ${config.example}""".stripMargin, + config.isAnyAllowed) + + override def pleaseEnterStaticDataNameOrPrevious(config: StaticDataConfig): String = + s""" Please enter a ${config.name} name + |For example: ${config.example} + | + |or choose a ${config.name} from previous searches""".stripMargin + + override def staticDataIs(config: StaticDataConfig, label: String): String = + s" ${capitalizeFirstLetter(config.name)} is $label" + + override def pleaseChooseStaticDataNameOrAny(config: StaticDataConfig): String = + withAnyVariant(s" Please choose a ${config.name}", config.isAnyAllowed) + + override def staticNotFound(config: StaticDataConfig): String = + withAnyVariant( + s""" Nothing was found 😔 + |Please enter a ${config.name} name again""", config.isAnyAllowed) + + override def loginAndPasswordAreOk: String = + s"""✅ Congrats! Login and password are OK! + |Now you can change the language /settings + """.stripMargin + + override def provideUsername: String = + s"""ℹ You must be logged in using your Luxmed credentials + | + | Please provide username""".stripMargin + + override def providePassword: String = " Please provide password" + + override def visitsHistoryIsEmpty: String = "ℹ No visits in your history" + + override def help: String = + s"""ℹ This is non official bot for Portal Pacienta LUX MED. + |With its help you can book a visit to the doctor, create term monitorings, view upcoming visits and visit history + | + | Supported commands + |/login - enter Luxmed credentials + |/book - make an appointment + |/monitorings - available terms monitoring + |/history - visits history + |/visits - upcoming visits + |/settings - change language + |/bug - submit an issue""".stripMargin + + override def dateFromIs(dateFrom: ZonedDateTime): String = s"📅 Date from is ${formatDate(dateFrom, locale)}" + + override def dateToIs(dateTo: ZonedDateTime): 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} + | /book_${page}_$index + | + |""".stripMargin + + 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} + | /repeat_${page}_$index + | + |""".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} + | /cancel_${page}_$index + | + |""".stripMargin + + override def upcomingVisitsHeader(page: Int, pages: Int): String = + withPages(" Reserved visits", page, pages) + + override def bugEntry(bug: Bug, page: Int, index: Int): String = + s"""⏱ ${formatDateTime(bug.submitted, locale)} + |Description: ${bug.details} + |State: ${if (bug.resolved) "✅ Resolved" else "🚫 Unresolved"} + | + |""".stripMargin + + override def bugsHeader(page: Int, pages: Int): String = + withPages(" Submitted issues", page, pages) + + override def monitoringEntry(monitoring: Monitoring, page: Int, index: Int): String = + s"""📅 ${formatDate(monitoring.dateFrom, locale)} -> ${formatDate(monitoring.dateTo, locale)} + |⏱ ${timeOfDay(monitoring.timeOfDay)} + |${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName} + |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} + |${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName} + |${capitalizeFirstLetter(city)}: ${monitoring.cityName} + |Type: ${if (monitoring.autobook) "Auto" else "Manual"} + | /cancel_${page}_$index + | + |""".stripMargin + + override def monitoringsHeader(page: Int, pages: Int): String = + s" Active monitorings." + + override def invalidLoginOrPassword: String = + """❗ You have entered invalid login or password or changed it via site. + |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} + |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} + |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + |${capitalizeFirstLetter(city)}: ${monitoring.cityName} + |/reserve_${monitoring.recordId}_${term.scheduleId}_${minutesSinceBeginOf2018(term.visitDate.startDateTime)} + | + |""".stripMargin + + override def availableTermsHeader(size: Int): String = + s"""✅ $size terms were found by monitoring. We showed you the closest 5. + | Please choose one to reserve""".stripMargin + + override def nothingWasFoundByMonitoring(monitoring: Monitoring): String = + s"""❗ Nothing was found by your monitoring. Monitoring has been disabled as outdated. + | + |📅 ${formatDate(monitoring.dateFrom, locale)} -> ${formatDate(monitoring.dateTo, locale)} + |⏱ ${timeOfDay(monitoring.timeOfDay)} + |${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName} + |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} + |${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName} + |${capitalizeFirstLetter(city)}: ${monitoring.cityName} + | + | Create new monitoring /book""".stripMargin + + override def appointmentIsBooked(term: AvailableVisitsTermPresentation, monitoring: Monitoring): String = + s"""👍 We just booked appointment for you! + | + |⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} + |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} + |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + |${capitalizeFirstLetter(city)}: ${monitoring.cityName}""".stripMargin + + override def maximumMonitoringsLimitExceeded: String = "Maximum monitorings per user is 5" + + override def monitoringOfTheSameTypeExists: String = "You already have active monitoring for the same service /monitorings" + + override def termIsOutdated: String = + s"""❗️ Looks like the term is already booked by someone else + |Please try another one or create a new monitoring /book""".stripMargin + + override def loginHasChangedOrWrong: String = + """❗ You have entered invalid login or password or changed it via site. + |Please /login again and create a new monitoring /book. + """.stripMargin + + override def settingsHeader: String = " Please choose an action" + + override def language: String = "Change language" + + override def chooseLanguage: String = " Please choose a language" + + override def languageUpdated: String = "👍 Language was successfully changed!" + + override def appointmentWasNotCancelled: String = "👍 Appointment was not cancelled" + + override def monitoringWasNotDeactivated: String = "👍 Monitoring was not deactivated" + + override def bugAction: String = " Please choose an action" + + override def createNewBug: String = "🐞 Submit new" + + override def showSubmittedBugs: String = "👀 Show submitted" + + override def enterIssueDetails: String = " Please provide issue details" + + override def noSubmittedIssuesFound: String = "ℹ No submitted issues found" + + override def bugHasBeenCreated(bugId: Long): String = s"✅ Thank you for submitting bug #$bugId!" + + override def chooseTimeOfDay: String = " Please choose preferred time of day" + + override def afterFive: String = "After 17:00" + + override def nineToFive: String = "From 09:00 to 17:00" + + override def beforeNine: String = "Before 09:00" + + override def allDay: String = "All day" + + override def preferredTimeIs(time: Int): String = s"⏱ Preferred time is ${timeOfDay(time)}" +} diff --git a/server/src/main/scala/com/lbs/server/lang/Lang.scala b/server/src/main/scala/com/lbs/server/lang/Lang.scala new file mode 100644 index 0000000..268fe8f --- /dev/null +++ b/server/src/main/scala/com/lbs/server/lang/Lang.scala @@ -0,0 +1,236 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.lang + +import java.time.ZonedDateTime +import java.util.Locale + +import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit, ValuationsResponse} +import com.lbs.server.actor.Book.BookingData +import com.lbs.server.actor.StaticData.StaticDataConfig +import com.lbs.server.repository.model +import com.lbs.server.repository.model.Monitoring + +object Lang { + + val Langs: Seq[Lang] = Seq(En, Ua) + + private val LangsMap = Seq(En, Ua).map(e => e.id -> e).toMap + + def apply(id: Int): Lang = { + LangsMap.getOrElse(id, sys.error(s"Unknown language id $id")) + } +} + +trait Lang { + def id: Int + + def locale: Locale + + def label: String + + val timeOfDay = Map( + 0 -> allDay, + 1 -> beforeNine, + 2 -> nineToFive, + 3 -> afterFive + ) + + protected def capitalizeFirstLetter(str: String): String = { + val fistCapitalLetter = str.head.toTitleCase + fistCapitalLetter + str.tail + } + + protected def withPages(message: String, page: Int, pages: Int): String + + def unableToCancelUpcomingVisit(reason: String): String + + def appointmentHasBeenCancelled: String + + def yes: String + + def no: String + + def noUpcomingVisits: String + + def areYouSureToCancelAppointment(visit: ReservedVisit): String + + def chooseDateFrom: String + + def chooseDateTo: String + + def findTerms: String + + def modifyDate: String + + def bookingSummary(bookingData: BookingData): String + + def noTermsFound: String + + def createMonitoring: String + + def cancel: String + + def book: String + + def confirmAppointment(term: AvailableVisitsTermPresentation, valuations: ValuationsResponse): String + + def appointmentIsConfirmed: String + + def monitoringHasBeenCreated: String + + def unableToCreateMonitoring: String + + def chooseTypeOfMonitoring: String + + def bookByApplication: String + + def bookManually: String + + def city: String + + def clinic: String + + def service: String + + def doctor: String + + def previous: String + + def next: String + + def noActiveMonitorings: String + + def deactivateMonitoring(monitoring: Monitoring): String + + def deactivated: String + + def any: String + + def pressAny: String + + protected def withAnyVariant(message: String, isAnyAllowed: Boolean): String = { + if (isAnyAllowed) + message + "\n\n" + pressAny + else message + } + + def pleaseEnterStaticDataNameOrAny(config: StaticDataConfig): String + + def pleaseEnterStaticDataNameOrPrevious(config: StaticDataConfig): String + + def staticDataIs(config: StaticDataConfig, label: String): String + + def pleaseChooseStaticDataNameOrAny(config: StaticDataConfig): String + + def staticNotFound(config: StaticDataConfig): String + + def loginAndPasswordAreOk: String + + def provideUsername: String + + def providePassword: String + + def visitsHistoryIsEmpty: String + + def help: String + + def dateFromIs(dateFrom: ZonedDateTime): String + + def dateToIs(dateTo: ZonedDateTime): String + + def termEntry(term: AvailableVisitsTermPresentation, page: Int, index: Int): String + + def termsHeader(page: Int, pages: Int): String + + def historyEntry(visit: HistoricVisit, page: Int, index: Int): String + + def historyHeader(page: Int, pages: Int): String + + def upcomingVisitEntry(visit: ReservedVisit, page: Int, index: Int): String + + def upcomingVisitsHeader(page: Int, pages: Int): String + + def bugEntry(bug: model.Bug, page: Int, index: Int): String + + def bugsHeader(page: Int, pages: Int): String + + def monitoringEntry(monitoring: Monitoring, page: Int, index: Int): String + + def monitoringsHeader(page: Int, pages: Int): String + + def invalidLoginOrPassword: String + + def availableTermEntry(term: AvailableVisitsTermPresentation, monitoring: Monitoring, index: Int): String + + def availableTermsHeader(size: Int): String + + def nothingWasFoundByMonitoring(monitoring: Monitoring): String + + def appointmentIsBooked(term: AvailableVisitsTermPresentation, monitoring: Monitoring): String + + def maximumMonitoringsLimitExceeded: String + + def monitoringOfTheSameTypeExists: String + + def termIsOutdated: String + + def loginHasChangedOrWrong: String + + def settingsHeader: String + + def language: String + + def chooseLanguage: String + + def languageUpdated: String + + def appointmentWasNotCancelled: String + + def monitoringWasNotDeactivated: String + + def createNewBug: String + + def showSubmittedBugs: String + + def bugAction: String + + def bugHasBeenCreated(bugId: Long): String + + def noSubmittedIssuesFound: String + + def enterIssueDetails: String + + def chooseTimeOfDay: String + + def afterFive: String + + def nineToFive: String + + def beforeNine: String + + def allDay: String + + def preferredTimeIs(time: Int): String +} diff --git a/server/src/main/scala/com/lbs/server/lang/Localizable.scala b/server/src/main/scala/com/lbs/server/lang/Localizable.scala new file mode 100644 index 0000000..2ca3020 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/lang/Localizable.scala @@ -0,0 +1,34 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.lang + +import com.lbs.server.actor.Login.UserId + +trait Localizable { + protected def userId: UserId + + protected def localization: Localization + + protected def lang: Lang = Option(userId).map(uId => localization.lang(uId.userId)).getOrElse(En) +} diff --git a/server/src/main/scala/com/lbs/server/lang/Localization.scala b/server/src/main/scala/com/lbs/server/lang/Localization.scala new file mode 100644 index 0000000..3fe897d --- /dev/null +++ b/server/src/main/scala/com/lbs/server/lang/Localization.scala @@ -0,0 +1,63 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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 + +@Component +class Localization { + + @Autowired + private var dataService: DataService = _ + + private val cachedLangs = new ConcurrentHashMap[Long, Lang] + + def lang(userId: Long): Lang = { + cachedLangs.computeIfAbsent(userId, _ => { + val settings = dataService.findSettings(userId) + settings.map(s => Lang(s.lang)).getOrElse(En) + }) + + } + + def invalidateLangsCache(): Unit = { + cachedLangs.clear() + } + + def updateLanguage(userId: Long, lang: Lang): Unit = { + cachedLangs.put(userId, lang) + val settings = dataService.findSettings(userId) match { + case Some(exists) => + exists.setLang(lang.id) + exists + case None => model.Settings(userId, lang.id) + } + dataService.saveSettings(settings) + } +} diff --git a/server/src/main/scala/com/lbs/server/lang/Ua.scala b/server/src/main/scala/com/lbs/server/lang/Ua.scala new file mode 100644 index 0000000..594bfcd --- /dev/null +++ b/server/src/main/scala/com/lbs/server/lang/Ua.scala @@ -0,0 +1,350 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.lang + +import java.time.ZonedDateTime +import java.util.Locale + +import com.lbs.api.json.model.{AvailableVisitsTermPresentation, HistoricVisit, ReservedVisit, ValuationsResponse} +import com.lbs.server.actor.Book +import com.lbs.server.actor.StaticData.StaticDataConfig +import com.lbs.server.repository.model.{Bug, Monitoring} +import com.lbs.server.util.DateTimeUtil.{formatDate, formatDateTime, minutesSinceBeginOf2018} + +object Ua extends Lang { + + override def id: Int = 1 + + override def locale: Locale = new Locale("uk", "UA") + + override def label: String = "🇺🇦Українська" + + override protected def withPages(message: String, page: Int, pages: Int): String = { + if (pages > 1) s"$message. Сторінка ${page + 1} з $pages" + else message + } + + override def unableToCancelUpcomingVisit(reason: String): String = + s"⚠ Не вдається скасувати візит! Причина: $reason" + + override def appointmentHasBeenCancelled: String = + s"👍 Ваш візит було скасовано!" + + override def yes: String = "Так" + + override def no: String = "Ні" + + override def noUpcomingVisits: String = + "ℹ Не знайдено жодного майбутнього візиту" + + override def areYouSureToCancelAppointment(visit: ReservedVisit): String = + s""" Ви впевнені, що хочете скасувати візит? + | + |⏱ ${formatDateTime(visit.visitDate.startDateTime, locale)} + |${capitalizeFirstLetter(doctor)}: ${visit.doctorName} + |${capitalizeFirstLetter(service)}: ${visit.service.name} + |${capitalizeFirstLetter(clinic)}: ${visit.clinic.name} + |""".stripMargin + + override def chooseDateFrom: String = " Будь ласка, виберіть початкову дату" + + override def chooseDateTo: String = " Будь ласка, виберіть кінцеву дату" + + override def findTerms: String = "🔍 Знайти терміни" + + override def modifyDate: String = "📅 Змінити дату" + + override def bookingSummary(bookingData: Book.BookingData): String = + s"🦄 Супер! Ми збираємося зарезервувати послугу ${bookingData.serviceId.name}" + + s" з обраним лікарем ${bookingData.doctorId.name}" + + s" в ${bookingData.clinicId.name} клініці" + + s" міста ${bookingData.cityId.name}." + + s"\nБажані дати: ${formatDate(bookingData.dateFrom, locale)} -> ${formatDate(bookingData.dateTo, locale)}" + + s"\nЧас: ${timeOfDay(bookingData.timeOfDay)}" + + s"\n\n Тепер оберіть наступну дію" + + override def noTermsFound: String = + s"""ℹ Терміни відсутні + | + |Що ви хочете зробити далі?""".stripMargin + + override def createMonitoring: String = "👀 Створити моніторінг" + + override def cancel: String = "Відмінити" + + override def book: String = "Зарезервувати" + + override def confirmAppointment(term: AvailableVisitsTermPresentation, valuations: ValuationsResponse): String = + + s""" ${valuations.optionsQuestion.getOrElse("Ви хотіли б підтвердити резервацію візиту?")} + | + |⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} + |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + | + |ℹ${valuations.visitTermVariants.head.infoMessage}""".stripMargin + + override def appointmentIsConfirmed: String = "👍 Ваш візит було підтверджено!" + + override def monitoringHasBeenCreated: String = "👍 Моніторинг був створений! Список активних /monitorings" + + override def unableToCreateMonitoring: String = s"👎 Не вдається створити моніторинг. Будь ласка, створіть /bug" + + override def chooseTypeOfMonitoring: String = " Будь ласка, виберіть тип моніторингу" + + override def bookByApplication: String = "👾 Автоматична резервація" + + override def bookManually: String = "👤 Ручна резервація" + + override def city: String = "місто" + + override def clinic: String = "клініка" + + override def service: String = "послуга" + + override def doctor: String = "лікар" + + override def previous: String = "Попередня" + + override def next: String = "Наступна" + + override def noActiveMonitorings: String = "ℹ У вас немає активних моніторингів. Створити новий /book" + + override def deactivateMonitoring(monitoring: Monitoring): String = + s""" Ви впевнені, що хочете вимкнути моніторинг? + | + |📅 ${formatDate(monitoring.dateFrom, locale)} -> ${formatDate(monitoring.dateTo, locale)} + |⏱ ${timeOfDay(monitoring.timeOfDay)} + |${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName} + |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} + |${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName}""".stripMargin + + override def deactivated: String = "👍 Деактивовано! Список активних /monitorings" + + override def any: String = "Будь-який" + + override def pressAny: String = s"або натисніть кнопку $any" + + override def pleaseEnterStaticDataNameOrAny(config: StaticDataConfig): String = + withAnyVariant( + s""" Будь ласка, введіть ${config.name} + |Наприклад: ${config.example}""".stripMargin, + config.isAnyAllowed) + + override def pleaseEnterStaticDataNameOrPrevious(config: StaticDataConfig): String = + s""" Будь ласка, введіть ${config.name} + |Наприклад: ${config.example} + | + |або виберіть ${config.name} з попередніх пошуків""".stripMargin + + override def staticDataIs(config: StaticDataConfig, label: String): String = + s" ${capitalizeFirstLetter(config.name)} $label" + + override def pleaseChooseStaticDataNameOrAny(config: StaticDataConfig): String = + withAnyVariant(s" Будь ласка, виберіть ${config.name}", config.isAnyAllowed) + + override def staticNotFound(config: StaticDataConfig): String = + withAnyVariant( + s""" Нічого не знайдено 😔 + |Будь ласка, введіть ${config.name} знову""".stripMargin, config.isAnyAllowed) + + override def loginAndPasswordAreOk: String = + s"""✅ Супер! Логін і пароль збережено + |Тепер ви можете змінити мову /settings""".stripMargin + + override def provideUsername: String = + s"""ℹ Ви повинні увійти в систему, використовуючи облікові дані Luxmed + | + | Будь ласка, введіть ім'я користувача""".stripMargin + + override def providePassword: String = " Будь ласка, введіть пароль" + + override def visitsHistoryIsEmpty: String = "ℹ Немає візитів в вашій історії" + + override def help: String = + s"""ℹ Це неофіційний бот для Порталу Пацієнта LUX MED. + |Завдяки йому ви можете зарезервувати візит до лікаря, створити моніторинг доступних термінів, переглянути історію та майбутні візити + | + | Підтримувані команди + |/login - ввести облікові дані Luxmed + |/book - призначити візит + |/monitorings - моніторінг доступних термінів + |/history - історія візитів + |/visits - майбутні візити + |/settings - змінити мову + |/bug - відправити баг""".stripMargin + + override def dateFromIs(dateFrom: ZonedDateTime): String = s"📅 Початкова дата ${formatDate(dateFrom, locale)}" + + override def dateToIs(dateTo: ZonedDateTime): 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} + | /book_${page}_$index + | + |""".stripMargin + + 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} + | /repeat_${page}_$index + | + |""".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} + | /cancel_${page}_$index + | + |""".stripMargin + + override def upcomingVisitsHeader(page: Int, pages: Int): String = + withPages(" Зарезервовані візити", page, pages) + + override def bugEntry(bug: Bug, page: Int, index: Int): String = + s"""⏱ ${formatDateTime(bug.submitted, locale)} + |Опис: ${bug.details} + |Статус: ${if (bug.resolved) "✅ Вирішено" else "🚫 Невирішено"} + | + |""".stripMargin + + override def bugsHeader(page: Int, pages: Int): String = + withPages(" Створені баги", page, pages) + + override def monitoringEntry(monitoring: Monitoring, page: Int, index: Int): String = + s"""📅 ${formatDate(monitoring.dateFrom, locale)} -> ${formatDate(monitoring.dateTo, locale)} + |⏱ ${timeOfDay(monitoring.timeOfDay)} + |${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName} + |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} + |${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName} + |${capitalizeFirstLetter(city)}: ${monitoring.cityName} + |Тип: ${if (monitoring.autobook) "Автоматичний" else "Ручний"} + | /cancel_${page}_$index + | + |""".stripMargin + + override def monitoringsHeader(page: Int, pages: Int): String = + s" Активні моніторінги" + + override def invalidLoginOrPassword: String = + """❗ Ви ввели невірний логін або пароль, або змінили його через сайт. + |Ваші моніторинги були видалені. Будь ласка, /login знову і створіть нові моніторинги. + """.stripMargin + + override def availableTermEntry(term: AvailableVisitsTermPresentation, monitoring: Monitoring, index: Int): String = + s"""⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} + |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} + |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + |${capitalizeFirstLetter(city)}: ${monitoring.cityName} + |/reserve_${monitoring.recordId}_${term.scheduleId}_${minutesSinceBeginOf2018(term.visitDate.startDateTime)} + | + |""".stripMargin + + override def availableTermsHeader(size: Int): String = + s"""✅ $size термінів було знайдено за допомогою моніторінгу. Ми показали вам найближчі 5. + | Будь ласка, оберіть один щоб заререзвувати""".stripMargin + + override def nothingWasFoundByMonitoring(monitoring: Monitoring): String = + s"""❗ Нічого не знайдено за вашим моніторингом. Моніторинг був вимкнений як застарілий. + | + |📅 ${formatDate(monitoring.dateFrom, locale)} -> ${formatDate(monitoring.dateTo, locale)} + |⏱ ${timeOfDay(monitoring.timeOfDay)} + |${capitalizeFirstLetter(doctor)}: ${monitoring.doctorName} + |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} + |${capitalizeFirstLetter(clinic)}: ${monitoring.clinicName} + |${capitalizeFirstLetter(city)}: ${monitoring.cityName} + | + | Створити новий моніторінг /book""".stripMargin + + override def appointmentIsBooked(term: AvailableVisitsTermPresentation, monitoring: Monitoring): String = + s"""👍 Ми зерезевували візит для вас! + | + |⏱ ${formatDateTime(term.visitDate.startDateTime, locale)} + |${capitalizeFirstLetter(doctor)}: ${term.doctor.name} + |${capitalizeFirstLetter(service)}: ${monitoring.serviceName} + |${capitalizeFirstLetter(clinic)}: ${term.clinic.name} + |${capitalizeFirstLetter(city)}: ${monitoring.cityName}""".stripMargin + + override def maximumMonitoringsLimitExceeded: String = "Максимальна кількість моніторінгів 5" + + override def monitoringOfTheSameTypeExists: String = "У вас вже є активний моніторинг на таку ж саму послугу /monitorings" + + override def termIsOutdated: String = + s"""❗️ Схоже, що термін вже не є доступним + |Будь ласка, спробуйте інший або створіть новий моніторинг /book""".stripMargin + + override def loginHasChangedOrWrong: String = + """❗ Ви ввели невірний і логін або пароль або змінили його через сайт. + |Будь ласка, /login знову і створіть новий моніторинг/book. + """.stripMargin + + override def settingsHeader: String = " Оберіть дію" + + override def language: String = "Змінтини мову" + + override def chooseLanguage: String = " Будь ласка, оберіть мову" + + override def languageUpdated: String = "👍 Мову успішно змінено!" + + override def appointmentWasNotCancelled: String = "👍 Візит не було скасовано" + + override def monitoringWasNotDeactivated: String = "👍 Моніторінг не було деактивовано" + + override def bugAction: String = " Оберіть дію" + + override def createNewBug: String = "🐞 Створити новий" + + override def showSubmittedBugs: String = "👀 Показати створені" + + override def enterIssueDetails: String = " Будь ласка, введіть деталі проблеми" + + override def noSubmittedIssuesFound: String = "ℹ Створених вами багів не знайдено" + + override def bugHasBeenCreated(bugId: Long): String = s"✅ Дякуємо за відправлений баг #$bugId!" + + override def chooseTimeOfDay: String = " Будь ласка, оберіть бажаний час" + + override def afterFive: String = "Після 17:00" + + override def nineToFive: String = "Від 09:00 до 17:00" + + override def beforeNine: String = "До 09:00" + + override def allDay: String = "Весь день" + + override def preferredTimeIs(time: Int): String = s"⏱ Бажаний час ${timeOfDay(time)}" +} diff --git a/server/src/main/scala/com/lbs/server/repository/DataRepository.scala b/server/src/main/scala/com/lbs/server/repository/DataRepository.scala new file mode 100644 index 0000000..d6dfea7 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/DataRepository.scala @@ -0,0 +1,192 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository + +import java.time.ZonedDateTime + +import com.lbs.server.repository.model.{Bug, CityHistory, ClinicHistory, Credentials, DoctorHistory, JLong, Monitoring, ServiceHistory, Settings, Source} +import javax.persistence.EntityManager +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Repository + +import scala.collection.JavaConverters._ + +@Repository +class DataRepository(@Autowired em: EntityManager) { + + private val maxHistory = 2 + + def getCityHistory(userId: Long): Seq[CityHistory] = { + em.createQuery( + """select city from CityHistory city where city.recordId in + | (select max(c.recordId) from CityHistory c where c.userId = :userId group by c.name order by MAX(c.time) desc) + | order by city.time desc""".stripMargin, classOf[CityHistory]) + .setParameter("userId", userId) + .setMaxResults(maxHistory) + .getResultList.asScala + } + + def getClinicHistory(userId: Long, cityId: Long): Seq[ClinicHistory] = { + em.createQuery( + """select clinic from ClinicHistory clinic where clinic.recordId in + | (select max(c.recordId) from ClinicHistory c where c.userId = :userId and c.cityId = :cityId group by c.name order by MAX(c.time) desc) + | order by clinic.time desc""".stripMargin, classOf[ClinicHistory]) + .setParameter("userId", userId) + .setParameter("cityId", cityId) + .setMaxResults(maxHistory) + .getResultList.asScala + } + + def getServiceHistory(userId: Long, cityId: Long, clinicId: Option[Long]): Seq[ServiceHistory] = { + val query = em.createQuery( + s"""select service from ServiceHistory service where service.recordId in + | (select max(s.recordId) from ServiceHistory s where s.userId = :userId and s.cityId = :cityId + | and s.clinicId ${clinicId.map(_ => "= :clinicId").getOrElse("IS NULL")} group by s.name order by MAX(s.time) desc) + | order by service.time desc""".stripMargin, classOf[ServiceHistory]) + .setParameter("userId", userId) + .setParameter("cityId", cityId) + .setMaxResults(maxHistory) + + clinicId.map(id => query.setParameter("clinicId", id)).getOrElse(query).getResultList.asScala + } + + def getDoctorHistory(userId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long): Seq[DoctorHistory] = { + val query = em.createQuery( + s"""select doctor from DoctorHistory doctor where doctor.recordId in + | (select max(d.recordId) from DoctorHistory d where d.userId = :userId + | and d.cityId = :cityId and d.clinicId ${clinicId.map(_ => "= :clinicId").getOrElse("IS NULL")} + | and d.serviceId = :serviceId group by d.name order by MAX(d.time) desc) + | order by doctor.time desc""".stripMargin, classOf[DoctorHistory]) + .setParameter("userId", userId) + .setParameter("cityId", cityId) + .setParameter("serviceId", serviceId) + .setMaxResults(maxHistory) + + clinicId.map(id => query.setParameter("clinicId", id)).getOrElse(query).getResultList.asScala + } + + def findCredentials(userId: Long): Option[Credentials] = { + em.createQuery( + "select credentials from Credentials credentials where credentials.userId = :userId", classOf[Credentials]) + .setParameter("userId", userId) + .getResultList.asScala.headOption + } + + def getBugs(userId: Long): Seq[Bug] = { + em.createQuery( + """select bug from Bug bug where bug.userId = :userId order by bug.submitted desc""".stripMargin, classOf[Bug]) + .setParameter("userId", userId) + .setMaxResults(50) + .getResultList.asScala + } + + def getActiveMonitorings: Seq[Monitoring] = { + em.createQuery( + """select monitoring from Monitoring monitoring where monitoring.active = true""".stripMargin, classOf[Monitoring]) + .getResultList.asScala + } + + def getActiveMonitoringsCount(userId: Long): JLong = { + em.createQuery( + """select count(monitoring) from Monitoring monitoring where monitoring.active = true + | and monitoring.userId = :userId""".stripMargin, classOf[JLong]) + .setParameter("userId", userId) + .getSingleResult + } + + def getActiveMonitorings(userId: Long): Seq[Monitoring] = { + em.createQuery( + """select monitoring from Monitoring monitoring where monitoring.active = true + | and monitoring.userId = :userId order by monitoring.dateTo asc""".stripMargin, classOf[Monitoring]) + .setParameter("userId", userId) + .getResultList.asScala + } + + def findActiveMonitoring(userId: Long, cityId: Long, serviceId: Long): Option[Monitoring] = { + em.createQuery( + """select monitoring from Monitoring monitoring where monitoring.active = true + | and monitoring.userId = :userId + | and monitoring.cityId = :cityId + | and monitoring.serviceId = :serviceId""".stripMargin, classOf[Monitoring]) + .setParameter("userId", userId) + .setParameter("cityId", cityId) + .setParameter("serviceId", serviceId) + .getResultList.asScala.headOption + } + + def getActiveMonitoringsSince(since: ZonedDateTime): Seq[Monitoring] = { + em.createQuery( + """select monitoring from Monitoring monitoring where monitoring.active = true + | and monitoring.created > :since""".stripMargin, classOf[Monitoring]) + .setParameter("since", since) + .getResultList.asScala + } + + def findMonitoring(userId: Long, monitoringId: Long): Option[Monitoring] = { + em.createQuery( + """select monitoring from Monitoring monitoring where monitoring.userId = :userId + | and monitoring.recordId = :monitoringId""".stripMargin, classOf[Monitoring]) + .setParameter("userId", userId) + .setParameter("monitoringId", monitoringId) + .getResultList.asScala.headOption + } + + def findSettings(userId: Long): Option[Settings] = { + em.createQuery( + "select settings from Settings settings where settings.userId = :userId", classOf[Settings]) + .setParameter("userId", userId) + .getResultList.asScala.headOption + } + + def findUserId(chatId: String, sourceSystemId: Long): Option[JLong] = { + em.createQuery( + "select source.userId from Source source where source.chatId = :chatId" + + " and source.sourceSystemId = :sourceSystemId", classOf[JLong]) + .setParameter("chatId", chatId) + .setParameter("sourceSystemId", sourceSystemId) + .getResultList.asScala.headOption + } + + def findCredentialsByUsername(username: String): Option[Credentials] = { + em.createQuery( + "select credentials from Credentials credentials where credentials.username = :username", classOf[Credentials]) + .setParameter("username", username) + .getResultList.asScala.headOption + } + + def findSource(chatId: String, sourceSystemId: Long, userId: Long): Option[Source] = { + em.createQuery( + "select source from Source source where source.chatId = :chatId" + + " and source.sourceSystemId = :sourceSystemId" + + " and userId = :userId", classOf[Source]) + .setParameter("chatId", chatId) + .setParameter("sourceSystemId", sourceSystemId) + .setParameter("userId", userId) + .getResultList.asScala.headOption + } + + def saveEntity[T](entity: T): T = { + em.merge(entity) + } +} diff --git a/server/src/main/scala/com/lbs/server/repository/model/Bug.scala b/server/src/main/scala/com/lbs/server/repository/model/Bug.scala new file mode 100644 index 0000000..deef0e2 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/Bug.scala @@ -0,0 +1,68 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + +import java.time.ZonedDateTime + +import javax.persistence.{Access, AccessType, Column, Entity} + +import scala.beans.BeanProperty + +@Entity +@Access(AccessType.FIELD) +class Bug extends RecordId { + @BeanProperty + @Column(name = "user_id", nullable = false) + var userId: JLong = _ + + @BeanProperty + @Column(name = "source_system_id", nullable = false) + var sourceSystemId: JLong = _ + + @BeanProperty + @Column(nullable = false) + var details: String = _ + + @BeanProperty + @Column(nullable = false) + var resolved: Boolean = false + + @BeanProperty + @Column(nullable = false) + var submitted: ZonedDateTime = ZonedDateTime.now() +} + +object Bug { + def apply(userId: Long, sourceSystemId: Long, details: String, resolved: Boolean = false, submitted: ZonedDateTime = ZonedDateTime.now()): Bug = { + val bug = new Bug + bug.userId = userId + bug.sourceSystemId = sourceSystemId + bug.details = details + bug.resolved = resolved + bug.submitted = submitted + bug + } +} + + 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 new file mode 100644 index 0000000..c01440a --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/CityHistory.scala @@ -0,0 +1,61 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + +import java.time.ZonedDateTime + +import javax.persistence.{Access, AccessType, Column, Entity} + +import scala.beans.BeanProperty + +@Entity +@Access(AccessType.FIELD) +class CityHistory extends History with RecordId { + @BeanProperty + @Column(nullable = false) + var id: JLong = _ + + @BeanProperty + @Column(nullable = false) + var name: String = _ + + @BeanProperty + @Column(name = "user_id", nullable = false) + var userId: JLong = _ + + @BeanProperty + @Column(nullable = false) + var time: ZonedDateTime = _ +} + +object CityHistory { + def apply(userId: Long, id: Long, name: String, time: ZonedDateTime): CityHistory = { + val city = new CityHistory + city.userId = userId + city.id = id + city.name = name + city.time = time + city + } +} 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 new file mode 100644 index 0000000..9995348 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/ClinicHistory.scala @@ -0,0 +1,66 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + +import java.time.ZonedDateTime + +import javax.persistence.{Access, AccessType, Column, Entity} + +import scala.beans.BeanProperty + +@Entity +@Access(AccessType.FIELD) +class ClinicHistory extends History with RecordId { + @BeanProperty + @Column(nullable = false) + var id: JLong = _ + + @BeanProperty + @Column(nullable = false) + var name: String = _ + + @BeanProperty + @Column(name = "user_id", nullable = false) + var userId: JLong = _ + + @BeanProperty + @Column(name = "city_id", nullable = false) + var cityId: JLong = _ + + @BeanProperty + @Column(nullable = false) + var time: ZonedDateTime = _ +} + +object ClinicHistory { + def apply(userId: Long, id: Long, name: String, cityId: Long, time: ZonedDateTime): ClinicHistory = { + val clinic = new ClinicHistory + clinic.userId = userId + clinic.id = id + clinic.name = name + clinic.time = time + clinic.cityId = cityId + clinic + } +} 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 new file mode 100644 index 0000000..e0cd62c --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/Credentials.scala @@ -0,0 +1,55 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + +import javax.persistence._ + +import scala.beans.BeanProperty + +@Entity +@Access(AccessType.FIELD) +class Credentials { + @Id + @BeanProperty + @Column(name = "user_id", nullable = false) + var userId: JLong = _ + + @BeanProperty + @Column(nullable = false) + var username: String = _ + + @BeanProperty + @Column(nullable = false) + var password: String = _ +} + +object Credentials { + def apply(userId: Long, username: String, password: String): Credentials = { + val credentials = new Credentials + credentials.userId = userId + credentials.username = username + credentials.password = password + credentials + } +} 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 new file mode 100644 index 0000000..2b31fe8 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/DoctorHistory.scala @@ -0,0 +1,76 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + +import java.time.ZonedDateTime + +import javax.persistence.{Access, AccessType, Column, Entity} + +import scala.beans.BeanProperty + +@Entity +@Access(AccessType.FIELD) +class DoctorHistory extends History with RecordId { + @BeanProperty + @Column(nullable = false) + var id: JLong = _ + + @BeanProperty + @Column(nullable = false) + var name: String = _ + + @BeanProperty + @Column(name = "user_id", nullable = false) + var userId: JLong = _ + + @BeanProperty + @Column(name = "city_id", nullable = false) + var cityId: JLong = _ + + @BeanProperty + @Column(name = "clinic_id", nullable = true) + var clinicId: JLong = _ + + @BeanProperty + @Column(name = "service_id", nullable = false) + var serviceId: JLong = _ + + @BeanProperty + @Column(nullable = false) + var time: ZonedDateTime = _ +} + +object DoctorHistory { + def apply(userId: Long, id: Long, name: String, cityId: Long, clinicId: Option[Long], serviceId: Long, time: ZonedDateTime): DoctorHistory = { + val doctor = new DoctorHistory + doctor.userId = userId + doctor.id = id + doctor.name = name + doctor.time = time + doctor.cityId = cityId + doctor.clinicId = clinicId + doctor.serviceId = serviceId + doctor + } +} diff --git a/server/src/main/scala/com/lbs/server/repository/model/History.scala b/server/src/main/scala/com/lbs/server/repository/model/History.scala new file mode 100644 index 0000000..48bdc34 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/History.scala @@ -0,0 +1,31 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + + +trait History { + def id: JLong + + def name: String +} diff --git a/server/src/main/scala/com/lbs/server/repository/model/Monitoring.scala b/server/src/main/scala/com/lbs/server/repository/model/Monitoring.scala new file mode 100644 index 0000000..3d3275a --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/Monitoring.scala @@ -0,0 +1,132 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + +import java.time.ZonedDateTime + +import javax.persistence.{Access, AccessType, Column, Entity} + +import scala.beans.BeanProperty + +@Entity +@Access(AccessType.FIELD) +class Monitoring extends RecordId { + @BeanProperty + @Column(name = "user_id", nullable = false) + var userId: JLong = _ + + + @BeanProperty + @Column(name = "chat_id", nullable = false) + var chatId: String = _ + + @BeanProperty + @Column(name = "source_system_id", nullable = false) + var sourceSystemId: JLong = _ + + @BeanProperty + @Column(name = "city_id", nullable = false) + var cityId: JLong = _ + + @BeanProperty + @Column(name = "city_name", nullable = false) + var cityName: String = _ + + @BeanProperty + @Column(name = "clinic_id", nullable = true) + var clinicId: JLong = _ + + @BeanProperty + @Column(name = "clinic_name", nullable = false) + var clinicName: String = _ + + @BeanProperty + @Column(name = "service_id", nullable = false) + var serviceId: JLong = _ + + @BeanProperty + @Column(name = "service_name", nullable = false) + var serviceName: String = _ + + @BeanProperty + @Column(name = "doctor_id", nullable = true) + var doctorId: JLong = _ + + @BeanProperty + @Column(name = "doctor_name", nullable = false) + var doctorName: String = _ + + @BeanProperty + @Column(name = "date_from", nullable = false) + var dateFrom: ZonedDateTime = _ + + @BeanProperty + @Column(name = "date_to", nullable = false) + var dateTo: ZonedDateTime = _ + + @BeanProperty + @Column(name = "time_of_day", nullable = true) + var timeOfDay: JInt = 0 + + @BeanProperty + @Column(nullable = false) + var autobook: Boolean = false + + @BeanProperty + @Column(nullable = false) + var created: ZonedDateTime = _ + + @BeanProperty + @Column(nullable = false) + var active: Boolean = true +} + +object Monitoring { + def apply(userId: Long, chatId: String, sourceSystemId: Long, cityId: Long, cityName: String, clinicId: Option[Long], clinicName: String, + serviceId: Long, serviceName: String, doctorId: Option[Long], doctorName: String, dateFrom: ZonedDateTime, + dateTo: ZonedDateTime, autobook: Boolean = false, created: ZonedDateTime = ZonedDateTime.now(), timeOfDay: Int, + active: Boolean = true): Monitoring = { + val monitoring = new Monitoring + monitoring.userId = userId + monitoring.chatId = chatId + monitoring.sourceSystemId = sourceSystemId + monitoring.cityId = cityId + monitoring.cityName = cityName + monitoring.clinicId = clinicId + monitoring.clinicName = clinicName + monitoring.serviceId = serviceId + monitoring.serviceName = serviceName + monitoring.doctorId = doctorId + monitoring.doctorName = doctorName + monitoring.dateFrom = dateFrom + monitoring.dateTo = dateTo + monitoring.timeOfDay = timeOfDay + monitoring.autobook = autobook + monitoring.created = created + monitoring.active = active + monitoring + } +} + + 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 new file mode 100644 index 0000000..f0a74a4 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/RecordId.scala @@ -0,0 +1,36 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + +import javax.persistence._ + +import scala.beans.BeanProperty + +@Access(AccessType.FIELD) +trait RecordId extends Serializable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @BeanProperty + var recordId: JLong = _ +} \ No newline at end of file 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 new file mode 100644 index 0000000..17728fe --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/ServiceHistory.scala @@ -0,0 +1,71 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + +import java.time.ZonedDateTime + +import javax.persistence.{Access, AccessType, Column, Entity} + +import scala.beans.BeanProperty + +@Entity +@Access(AccessType.FIELD) +class ServiceHistory extends History with RecordId { + @BeanProperty + @Column(nullable = false) + var id: JLong = _ + + @BeanProperty + @Column(nullable = false) + var name: String = _ + + @BeanProperty + @Column(name = "userId_id", nullable = false) + var userId: JLong = _ + + @BeanProperty + @Column(name = "city_id", nullable = false) + var cityId: JLong = _ + + @BeanProperty + @Column(name = "clinic_id", nullable = true) + var clinicId: JLong = _ + + @BeanProperty + @Column(nullable = false) + var time: ZonedDateTime = _ +} + +object ServiceHistory { + def apply(userId: Long, id: Long, name: String, cityId: Long, clinicId: Option[Long], time: ZonedDateTime): ServiceHistory = { + val service = new ServiceHistory + service.userId = userId + service.id = id + service.name = name + service.time = time + service.cityId = cityId + service.clinicId = clinicId + service + } +} 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 new file mode 100644 index 0000000..f71281d --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/Settings.scala @@ -0,0 +1,55 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + +import javax.persistence.{Access, AccessType, Column, Entity} + +import scala.beans.BeanProperty + +@Entity +@Access(AccessType.FIELD) +class Settings extends RecordId { + @BeanProperty + @Column(name = "user_id", unique = true, nullable = false) + var userId: JLong = _ + + @BeanProperty + @Column(nullable = false) + var lang: Int = 0 //En by default +} + +object Settings { + def apply(userId: Long, lang: Int): Settings = { + val settings = new Settings + settings.userId = userId + settings.lang = lang + settings + } +} + + + + + + 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 new file mode 100644 index 0000000..cd0fb0d --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/Source.scala @@ -0,0 +1,56 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + +import javax.persistence._ + +import scala.beans.BeanProperty + +@Entity +@Access(AccessType.FIELD) +class Source extends RecordId { + @BeanProperty + @Column(name = "chat_id", nullable = false) + var chatId: String = _ + + @BeanProperty + @Column(name = "source_system_id", nullable = false) + var sourceSystemId: JLong = _ + + @BeanProperty + @Column(name = "user_id", nullable = false) + var userId: JLong = _ +} + +object Source { + def apply(chatId: String, sourceSystemId: Long, userId: Long): Source = { + val source = new Source + source.chatId = chatId + source.sourceSystemId = sourceSystemId + source.userId = userId + source + } +} + + 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 new file mode 100644 index 0000000..fd2cce7 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/SystemUser.scala @@ -0,0 +1,34 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository.model + +import javax.persistence._ + +@Entity +@Access(AccessType.FIELD) +class SystemUser extends RecordId + + + + diff --git a/server/src/main/scala/com/lbs/server/repository/model/package.scala b/server/src/main/scala/com/lbs/server/repository/model/package.scala new file mode 100644 index 0000000..ecb71df --- /dev/null +++ b/server/src/main/scala/com/lbs/server/repository/model/package.scala @@ -0,0 +1,35 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.repository + +import scala.language.implicitConversions + +package object model { + type JLong = java.lang.Long + type JInt = java.lang.Integer + + implicit def JLongToOptionLong(jLong: JLong): Option[Long] = Option(jLong).map(_.longValue()) + + implicit def OptionLongToJLong(long: Option[Long]): JLong = long.map(Long.box).orNull +} diff --git a/server/src/main/scala/com/lbs/server/service/ApiService.scala b/server/src/main/scala/com/lbs/server/service/ApiService.scala new file mode 100644 index 0000000..d1b7fb7 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/service/ApiService.scala @@ -0,0 +1,128 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.service + +import java.time.ZonedDateTime + +import com.lbs.api.LuxmedApi +import com.lbs.api.json.model._ +import org.jasypt.util.text.TextEncryptor +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import scalaj.http.HttpResponse + +@Service +class ApiService extends SessionSupport { + + @Autowired + protected var dataService: DataService = _ + @Autowired + private var textEncryptor: TextEncryptor = _ + + def getAllCities(userId: Long): Either[Throwable, List[IdName]] = + withSession(userId) { session => + LuxmedApi.reservationFilter(session.accessToken, session.tokenType).map(_.cities) + } + + def getAllClinics(userId: Long, cityId: Long): Either[Throwable, List[IdName]] = + withSession(userId) { session => + LuxmedApi.reservationFilter(session.accessToken, + session.tokenType, cityId = Some(cityId)).map(_.clinics) + } + + def getAllServices(userId: Long, cityId: Long, clinicId: Option[Long]): Either[Throwable, List[IdName]] = + withSession(userId) { session => + LuxmedApi.reservationFilter(session.accessToken, + session.tokenType, cityId = Some(cityId), + clinicId = clinicId).map(_.services) + } + + def getAllDoctors(userId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long): Either[Throwable, List[IdName]] = + withSession(userId) { session => + LuxmedApi.reservationFilter(session.accessToken, + session.tokenType, cityId = Some(cityId), + clinicId = clinicId, serviceId = Some(serviceId)).map(_.doctors) + } + + def getDefaultPayer(userId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long): Either[Throwable, Option[IdName]] = + withSession(userId) { session => + LuxmedApi.reservationFilter(session.accessToken, + session.tokenType, cityId = Some(cityId), + clinicId = clinicId, serviceId = Some(serviceId)).map(_.defaultPayer) + } + + def getAvailableTerms(userId: 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): Either[Throwable, List[AvailableVisitsTermPresentation]] = + withSession(userId) { session => + getDefaultPayer(userId, cityId, clinicId, serviceId).flatMap { case Some(payerId) => + LuxmedApi.availableTerms(session.accessToken, session.tokenType, payerId.id, cityId, clinicId, serviceId, doctorId, + fromDate, toDate, timeOfDay, languageId, findFirstFreeTerm).map(_.availableVisitsTermPresentation) + } + } + + def temporaryReservation(userId: Long, temporaryReservationRequest: TemporaryReservationRequest, valuationsRequest: ValuationsRequest): Either[Throwable, (TemporaryReservationResponse, ValuationsResponse)] = + withSession(userId) { session => + LuxmedApi.temporaryReservation(session.accessToken, session.tokenType, temporaryReservationRequest) match { + case Left(ex) => Left(ex) + case Right(temporaryReservation) => + LuxmedApi.valuations(session.accessToken, session.tokenType, valuationsRequest) match { + case Left(ex) => Left(ex) + case Right(valuationsResponse) => Right(temporaryReservation -> valuationsResponse) + } + } + } + + def deleteTemporaryReservation(userId: Long, temporaryReservationId: Long): Either[Throwable, HttpResponse[String]] = + withSession(userId) { session => + LuxmedApi.deleteTemporaryReservation(session.accessToken, session.tokenType, temporaryReservationId) + } + + def reservation(userId: Long, reservationRequest: ReservationRequest): Either[Throwable, ReservationResponse] = + withSession(userId) { session => + LuxmedApi.reservation(session.accessToken, session.tokenType, reservationRequest) + } + + def visitsHistory(userId: Long, fromDate: ZonedDateTime = ZonedDateTime.now().minusYears(1), + toDate: ZonedDateTime = ZonedDateTime.now(), page: Int = 1, pageSize: Int = 100): Either[Throwable, List[HistoricVisit]] = + withSession(userId) { session => + LuxmedApi.visitsHistory(session.accessToken, session.tokenType, fromDate, toDate, page, pageSize).map(_.historicVisits) + } + + def reservedVisits(userId: Long, fromDate: ZonedDateTime = ZonedDateTime.now(), + toDate: ZonedDateTime = ZonedDateTime.now().plusMonths(3)): Either[Throwable, List[ReservedVisit]] = + withSession(userId) { session => + LuxmedApi.reservedVisits(session.accessToken, session.tokenType, fromDate, toDate).map(_.reservedVisits) + } + + def deleteReservation(userId: Long, reservationId: Long): Either[Throwable, HttpResponse[String]] = + withSession(userId) { session => + LuxmedApi.deleteReservation(session.accessToken, session.tokenType, reservationId) + } + + def login(username: String, password: String): Either[Throwable, LoginResponse] = { + LuxmedApi.login(username, textEncryptor.decrypt(password)) + } + +} diff --git a/server/src/main/scala/com/lbs/server/service/DataService.scala b/server/src/main/scala/com/lbs/server/service/DataService.scala new file mode 100644 index 0000000..0d45055 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/service/DataService.scala @@ -0,0 +1,163 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +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.actor.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 + +@Service +class DataService { + + @Autowired + private var dataRepository: DataRepository = _ + + def getLatestCities(userId: Long): Seq[IdName] = { + dataRepository.getCityHistory(userId).mapTo[IdName] + } + + def getLatestClinicsByCityId(userId: Long, cityId: Long): Seq[IdName] = { + dataRepository.getClinicHistory(userId, cityId).mapTo[IdName] + } + + def getLatestServicesByCityIdAndClinicId(userId: Long, cityId: Long, clinicId: Option[Long]): Seq[IdName] = { + dataRepository.getServiceHistory(userId, cityId, clinicId).mapTo[IdName] + } + + def getLatestDoctorsByCityIdAndClinicIdAndServiceId(userId: Long, cityId: Long, clinicId: Option[Long], serviceId: Long): Seq[IdName] = { + dataRepository.getDoctorHistory(userId, cityId, clinicId, serviceId).mapTo[IdName] + } + + def getCredentials(userId: Long): Option[Credentials] = { + dataRepository.findCredentials(userId) + } + + @Transactional + def submitBug(userId: Long, sourceSystemId: Long, details: String): Option[Long] = { + dataRepository.saveEntity(Bug(userId, sourceSystemId, details)).recordId + } + + def getBugs(chatId: Long): Seq[Bug] = { + dataRepository.getBugs(chatId) + } + + @Transactional + def saveMonitoring(monitoring: Monitoring): Monitoring = { + dataRepository.saveEntity(monitoring) + } + + def getActiveMonitorings: Seq[Monitoring] = { + dataRepository.getActiveMonitorings + } + + def getActiveMonitoringsCount(userId: Long): Long = { + dataRepository.getActiveMonitoringsCount(userId) + } + + def getActiveMonitorings(userId: Long): Seq[Monitoring] = { + dataRepository.getActiveMonitorings(userId) + } + + def findActiveMonitoring(userId: Long, cityId: Long, serviceId: Long): Option[Monitoring] = { + dataRepository.findActiveMonitoring(userId, cityId, serviceId) + } + + def getActiveMonitoringsSince(since: ZonedDateTime): Seq[Monitoring] = { + dataRepository.getActiveMonitoringsSince(since) + } + + def findMonitoring(userId: Long, monitoringId: Long): Option[Monitoring] = { + dataRepository.findMonitoring(userId, monitoringId) + } + + def findSettings(userId: Long): Option[Settings] = { + dataRepository.findSettings(userId) + } + + def findUserIdBySource(source: MessageSource): Option[Long] = { + dataRepository.findUserId(source.chatId, source.sourceSystem.id).map(_.toLong) + } + + def findCredentialsByUsername(username: String): Option[Credentials] = { + dataRepository.findCredentialsByUsername(username) + } + + @Transactional + def saveSettings(settings: Settings): Settings = { + dataRepository.saveEntity(settings) + } + + @Transactional + def saveCredentials(source: MessageSource, username: String, password: String): Credentials = { + val credentialsMaybe = findCredentialsByUsername(username) + credentialsMaybe match { + case Some(credentials) => //user already exists + val sourceMaybe = dataRepository.findSource(source.chatId, source.sourceSystem.id, credentials.userId) + sourceMaybe match { + case Some(_) => //source already exists. Just update credentials + case None => //add new source + val src = Source(source.chatId, source.sourceSystem.id, credentials.userId) + dataRepository.saveEntity(src) + } + credentials.username = username + credentials.password = password + dataRepository.saveEntity(credentials) + case None => //new user + val user = dataRepository.saveEntity(new SystemUser) + val src = Source(source.chatId, source.sourceSystem.id, user.recordId) + dataRepository.saveEntity(src) + val credentials = Credentials(user.recordId, username, password) + dataRepository.saveEntity(credentials) + } + } + + @Transactional + def storeAppointment(userId: Long, bookingData: BookingData): Unit = { + val time = ZonedDateTime.now() + val cityId = bookingData.cityId + val clinicId = bookingData.clinicId + val serviceId = bookingData.serviceId + val doctorId = bookingData.doctorId + + val city = CityHistory(userId, cityId.id, cityId.name, time) + dataRepository.saveEntity(city) + + val clinicMaybe = clinicId.optionalId.map(id => ClinicHistory(userId, id, clinicId.name, cityId.id, time)) + clinicMaybe.foreach(dataRepository.saveEntity) + + val service = ServiceHistory(userId, serviceId.id, serviceId.name, cityId.id, clinicId.optionalId, time) + dataRepository.saveEntity(service) + + val doctorMaybe = doctorId.optionalId.map(id => DoctorHistory(userId, id, doctorId.name, cityId.id, clinicId.optionalId, serviceId.id, time)) + doctorMaybe.foreach(dataRepository.saveEntity) + } +} diff --git a/server/src/main/scala/com/lbs/server/service/MonitoringService.scala b/server/src/main/scala/com/lbs/server/service/MonitoringService.scala new file mode 100644 index 0000000..dfa1dcc --- /dev/null +++ b/server/src/main/scala/com/lbs/server/service/MonitoringService.scala @@ -0,0 +1,258 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.service + +import java.time.ZonedDateTime +import java.util.concurrent.ScheduledFuture + +import com.lbs.api.json.model.{AvailableVisitsTermPresentation, ReservationRequest, TemporaryReservationRequest, ValuationsRequest} +import com.lbs.bot.Bot +import com.lbs.bot.model.{MessageSource, MessageSourceSystem} +import com.lbs.common.{Logger, Scheduler} +import com.lbs.server.lang.Localization +import com.lbs.server.repository.model._ +import com.lbs.server.util.DateTimeUtil._ +import com.lbs.server.util.ServerModelConverters._ +import javax.annotation.PostConstruct +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service + +import scala.collection.mutable +import scala.concurrent.duration._ +import scala.util.Random + +@Service +class MonitoringService extends Logger { + + @Autowired + private var bot: Bot = _ + @Autowired + private var dataService: DataService = _ + @Autowired + private var apiService: ApiService = _ + @Autowired + private var localization: Localization = _ + + private var activeMonitorings = mutable.Map.empty[JLong, (Monitoring, ScheduledFuture[_])] + + private val dbChecker = new Scheduler(1) + + private val monitoringExecutor = new Scheduler(10) + + private val MaxDelay = 10.minute + + private val PeriodBase = 10.minute + + private val PeriodMaxDelta = 5.minute + + private def delay = Random.nextInt(MaxDelay.toSeconds.toInt).seconds + + private def period = (PeriodBase.toSeconds + Random.nextInt(PeriodMaxDelta.toSeconds.toInt)).seconds + + private var checkedOn: ZonedDateTime = _ + + def notifyUserAboutTerms(terms: Seq[AvailableVisitsTermPresentation], monitoring: Monitoring): Unit = { + deactivateMonitoring(monitoring.recordId) + + val fiveTerms = terms.take(5).zipWithIndex //send only 5 closest terms + val messages = lang(monitoring.userId) + + val message = messages.availableTermsHeader(terms.length) + "\n\n" + + fiveTerms.map { case (term, index) => + messages.availableTermEntry(term, monitoring, index) + }.mkString + + bot.sendMessage(monitoring.source, message) + } + + private def monitor(monitoring: Monitoring): Unit = { + LOG.debug(s"Looking for available terms. Monitoring [#${monitoring.recordId}]") + val dateFrom = optimizeDateFrom(monitoring.dateFrom) + val termsEither = apiService.getAvailableTerms(monitoring.userId, monitoring.cityId, monitoring.clinicId, monitoring.serviceId, + monitoring.doctorId, dateFrom, Some(monitoring.dateTo)) + termsEither match { + case Right(terms) => + if (terms.nonEmpty) { + LOG.debug(s"Found ${terms.length} terms by monitoring [${monitoring.recordId}]") + if (monitoring.autobook) { + val term = terms.head + bookAppointment(term, monitoring) + } else { + notifyUserAboutTerms(terms, monitoring) + } + } else { + LOG.debug(s"No new terms found for monitoring [#${monitoring.recordId}]") + } + case Left(ex) if ex.getMessage.toLowerCase.contains("invalid login or password") => + LOG.error(s"User entered invalid name or password. Monitoring will be disabled", ex) + bot.sendMessage(monitoring.source, lang(monitoring.userId).invalidLoginOrPassword) + val activeUserMonitorings = dataService.getActiveMonitorings(monitoring.userId) + activeUserMonitorings.foreach { m => + deactivateMonitoring(m.recordId) + } + case Left(ex) => LOG.error(s"Unable to receive terms by monitoring [#${monitoring.recordId}]", ex) + } + } + + private def optimizeDateFrom(date: ZonedDateTime) = { + val now = ZonedDateTime.now() + if (date.isBefore(now)) now else date + } + + private def initializeMonitorings(allMonitorings: Seq[Monitoring]): Unit = { + allMonitorings.foreach { monitoring => + if (monitoring.active && !activeMonitorings.contains(monitoring.recordId)) { + val delaySnapshot = delay + val periodSnapshot = period + val future = monitoringExecutor.schedule(monitor(monitoring), delaySnapshot, periodSnapshot) + LOG.debug(s"Scheduled monitoring: [#${monitoring.recordId}] with delay: $delaySnapshot and period: $periodSnapshot") + activeMonitorings += (monitoring.recordId -> (monitoring -> future)) + } + } + } + + private def initializeNewMonitorings(): Unit = { + LOG.debug(s"Looking for new monitorings created since $checkedOn") + val currentTime = ZonedDateTime.now() + val monitorings = dataService.getActiveMonitoringsSince(checkedOn) + LOG.debug(s"New active monitorings found: ${monitorings.length}") + checkedOn = currentTime + initializeMonitorings(monitorings) + } + + def notifyChatAboutDisabledMonitoring(monitoring: Monitoring): Unit = { + bot.sendMessage(monitoring.source, lang(monitoring.userId).nothingWasFoundByMonitoring(monitoring)) + } + + private def disableOutdated(): Unit = { + val now = ZonedDateTime.now() + val toDisable = activeMonitorings.collect { case (id, (monitoring, _)) if monitoring.dateTo.isBefore(now) => + id -> monitoring + } + + toDisable.foreach { case (id, monitoring) => + LOG.debug(s"Monitoring [#$id] is going to be disable as outdated") + notifyChatAboutDisabledMonitoring(monitoring) + deactivateMonitoring(id) + } + } + + private def updateMonitorings(): Unit = { + initializeNewMonitorings() + disableOutdated() + } + + private def initializeDbChecker(): Unit = { + dbChecker.schedule(updateMonitorings(), 1.minute) + } + + private def bookAppointment(term: AvailableVisitsTermPresentation, monitoring: Monitoring): Unit = { + val temporaryReservationRequest = term.mapTo[TemporaryReservationRequest] + val valuationsRequest = term.mapTo[ValuationsRequest] + val reservationMaybe = for { + okResponse <- apiService.temporaryReservation(monitoring.userId, temporaryReservationRequest, valuationsRequest) + (temporaryReservation, valuations) = okResponse + temporaryReservationId = temporaryReservation.id + visitTermVariant = valuations.visitTermVariants.head + reservationRequest = (temporaryReservationId, visitTermVariant, term).mapTo[ReservationRequest] + reservation <- apiService.reservation(monitoring.userId, reservationRequest) + } yield reservation + + reservationMaybe match { + case Right(_) => + bot.sendMessage(monitoring.source, lang(monitoring.userId).appointmentIsBooked(term, monitoring)) + deactivateMonitoring(monitoring.recordId) + case Left(ex) => + LOG.error(s"Unable to book appointment by monitoring [${monitoring.recordId}]", ex) + } + + } + + def deactivateMonitoring(monitoringId: JLong): Unit = { + activeMonitorings.remove(monitoringId).foreach { + case (monitoring, future) => + LOG.debug(s"Deactivating monitoring [#$monitoringId]") + if (!future.isCancelled) { + future.cancel(true) + monitoring.active = false + dataService.saveMonitoring(monitoring) + } + } + } + + def createMonitoring(monitoring: Monitoring): Monitoring = { + val userMonitoringsCount = dataService.getActiveMonitoringsCount(monitoring.userId) + require(userMonitoringsCount + 1 <= 5, lang(monitoring.userId).maximumMonitoringsLimitExceeded) + val activeMonitoring = dataService.findActiveMonitoring(monitoring.userId, monitoring.cityId, monitoring.serviceId) + require(activeMonitoring.isEmpty, lang(monitoring.userId).monitoringOfTheSameTypeExists) + dataService.saveMonitoring(monitoring) + } + + def getActiveMonitorings(chatId: Long): Seq[Monitoring] = { + dataService.getActiveMonitorings(chatId) + } + + def bookAppointmentByScheduleId(userId: Long, monitoringId: Long, scheduleId: Long, time: Long): Unit = { + val monitoringMaybe = dataService.findMonitoring(userId, monitoringId) + monitoringMaybe match { + case Some(monitoring) => + val termsEither = apiService.getAvailableTerms(monitoring.userId, monitoring.cityId, monitoring.clinicId, monitoring.serviceId, + monitoring.doctorId, monitoring.dateFrom, Some(monitoring.dateTo)) + termsEither match { + case Right(terms) => + val termMaybe = terms.find(term => term.scheduleId == scheduleId && minutesSinceBeginOf2018(term.visitDate.startDateTime) == time) + termMaybe match { + case Some(term) => + bookAppointment(term, monitoring) + case None => + bot.sendMessage(monitoring.source, lang(monitoring.userId).termIsOutdated) + } + case Left(ex) if ex.getMessage.toLowerCase.contains("invalid login or password") => + LOG.error(s"User entered invalid name or password. Monitoring will be disabled", ex) + bot.sendMessage(monitoring.source, lang(monitoring.userId).loginHasChangedOrWrong) + case Left(ex) => LOG.error(s"Error occurred during receiving terms for monitoring [#${monitoring.recordId}]", ex) + } + case None => + LOG.debug(s"Monitoring [#$monitoringId] not found in db") + } + } + + implicit class MonitoringAsSource(monitoring: Monitoring) { + def source: MessageSource = MessageSource( + MessageSourceSystem(monitoring.sourceSystemId), monitoring.chatId + ) + } + + + private def lang(chatId: Long) = localization.lang(chatId) + + @PostConstruct + private def initialize(): Unit = { + checkedOn = ZonedDateTime.now() + val monitorings = dataService.getActiveMonitorings + LOG.debug(s"Active monitorings found: ${monitorings.length}") + initializeMonitorings(monitorings) + initializeDbChecker() + } +} diff --git a/server/src/main/scala/com/lbs/server/service/SessionSupport.scala b/server/src/main/scala/com/lbs/server/service/SessionSupport.scala new file mode 100644 index 0000000..442d0ab --- /dev/null +++ b/server/src/main/scala/com/lbs/server/service/SessionSupport.scala @@ -0,0 +1,92 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server.service + +import com.lbs.api.json.model.LoginResponse +import com.lbs.common.ParametrizedLock +import com.lbs.server.exception.UserNotFoundException +import org.slf4j.LoggerFactory + +import scala.collection.mutable + +trait SessionSupport { + + private val Log = LoggerFactory.getLogger(classOf[SessionSupport]) + + case class Session(accessToken: String, tokenType: String) + + def login(username: String, password: String): Either[Throwable, LoginResponse] + + protected def dataService: DataService + + private val sessions = mutable.Map[Long, Session]() + + private val lock = new ParametrizedLock[Long] + + protected def withSession[T](userId: Long)(fn: Session => Either[Throwable, T]): Either[Throwable, T] = + lock.obtainLock(userId).synchronized { + + def auth: Either[Throwable, Session] = { + val credentialsMaybe = dataService.getCredentials(userId) + credentialsMaybe match { + case Some(credentials) => + val loginResponse = login(credentials.username, credentials.password) + loginResponse.map(r => Session(r.accessToken, r.tokenType)) + case None => Left(UserNotFoundException(userId)) + } + } + + def session: Either[Throwable, Session] = { + sessions.get(userId) match { + case Some(sess) => Right(sess) + case None => + auth match { + case Right(sess) => + sessions.put(userId, sess) + Right(sess) + case left => left + } + } + } + + session match { + case Right(s) => + fn(s) match { + case Left(ex) if ex.getMessage.contains("session has expired") => + Log.debug(s"The session for user with chat id: $userId has expired. Try to relogin") + sessions.remove(userId) + session.flatMap(fn) + case another => + Log.debug(s"Call to remote api function has completed with result:\n$another") + another + } + case Left(ex) => Left(ex) + } + } + + def addSession(userId: Long, accessToken: String, tokenType: String): Unit = + lock.obtainLock(userId).synchronized { + sessions.put(userId, Session(accessToken, tokenType)) + } +} diff --git a/server/src/main/scala/com/lbs/server/util/package.scala b/server/src/main/scala/com/lbs/server/util/package.scala new file mode 100644 index 0000000..186a7a9 --- /dev/null +++ b/server/src/main/scala/com/lbs/server/util/package.scala @@ -0,0 +1,154 @@ +/** + * MIT License + * + * Copyright (c) 2018 Yevhen Zadyra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lbs.server + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} +import java.util.Locale + +import com.lbs.api.json.model._ +import com.lbs.bot.model.Message +import com.lbs.common.ModelConverters +import com.lbs.server.actor.Book.BookingData +import com.lbs.server.actor.Login.UserId +import com.lbs.server.repository.model.{History, Monitoring} + +import scala.collection.generic.CanBuildFrom +import scala.language.{higherKinds, implicitConversions} + + +package object util { + + object ServerModelConverters extends ModelConverters { + + implicit val BookingDataToMonitoringConverter: + ObjectConverter[(UserId, BookingData), Monitoring] = + new ObjectConverter[(UserId, BookingData), Monitoring] { + override def convert[Z <: (UserId, BookingData)](data: Z): Monitoring = { + val (userId, bookingData) = data.asInstanceOf[(UserId, BookingData)] + Monitoring( + userId = userId.userId, + chatId = userId.source.chatId, + sourceSystemId = userId.source.sourceSystem.id, + cityId = bookingData.cityId.id, + cityName = bookingData.cityId.name, + clinicId = bookingData.clinicId.optionalId, + clinicName = bookingData.clinicId.name, + serviceId = bookingData.serviceId.id, + serviceName = bookingData.serviceId.name, + doctorId = bookingData.doctorId.optionalId, + doctorName = bookingData.doctorId.name, + dateFrom = bookingData.dateFrom, + dateTo = bookingData.dateTo, + timeOfDay = bookingData.timeOfDay, + autobook = bookingData.autobook + ) + } + } + + implicit val AvailableVisitsTermPresentationToTemporaryReservationRequestConverter: + ObjectConverter[AvailableVisitsTermPresentation, TemporaryReservationRequest] = + new ObjectConverter[AvailableVisitsTermPresentation, TemporaryReservationRequest] { + override def convert[Z <: AvailableVisitsTermPresentation](term: Z): TemporaryReservationRequest = { + TemporaryReservationRequest( + clinicId = term.clinic.id, + doctorId = term.doctor.id, + payerDetailsList = term.payerDetailsList, + referralRequiredByService = term.referralRequiredByService, + roomId = term.roomId, + serviceId = term.serviceId, + startDateTime = term.visitDate.startDateTime + ) + } + } + + implicit val TmpReservationIdWithValuationsToReservationRequestConverter: + ObjectConverter[(Long, VisitTermVariant, AvailableVisitsTermPresentation), ReservationRequest] = + new ObjectConverter[(Long, VisitTermVariant, AvailableVisitsTermPresentation), ReservationRequest] { + override def convert[Z <: (Long, VisitTermVariant, AvailableVisitsTermPresentation)](any: Z): ReservationRequest = { + val (tmpReservationId, valuations, term) = any.asInstanceOf[(Long, VisitTermVariant, AvailableVisitsTermPresentation)] + 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 AvailableVisitsTermPresentationToValuationRequestConverter: + ObjectConverter[AvailableVisitsTermPresentation, ValuationsRequest] = + new ObjectConverter[AvailableVisitsTermPresentation, ValuationsRequest] { + override def convert[Z <: AvailableVisitsTermPresentation](term: Z): ValuationsRequest = { + ValuationsRequest( + clinicId = term.clinic.id, + doctorId = term.doctor.id, + payerDetailsList = term.payerDetailsList, + referralRequiredByService = term.referralRequiredByService, + roomId = term.roomId, + serviceId = term.serviceId, + startDateTime = term.visitDate.startDateTime + ) + } + } + + implicit val HistoryToIdNameConverter: CollectionConverter[History, IdName] = new CollectionConverter[History, IdName] { + override def convert[Z <: History, Col[X] <: Iterable[X]](col: Col[Z])(implicit bf: CanBuildFrom[Col[Z], IdName, Col[IdName]]): Col[IdName] = { + col.map(history => IdName(history.id, history.name))(collection.breakOut) + } + } + } + + object MessageExtractors { + + object Text { + def unapply(msg: Message): Option[String] = msg.text + } + + object TextOpt { + def unapply(msg: Message): Option[Option[String]] = Some(msg.text) + } + + } + + object DateTimeUtil { + private val DateFormat: Locale => DateTimeFormatter = locale => DateTimeFormatter.ofPattern("dd MMM yyyy", locale) + + private val DateTimeFormat: Locale => DateTimeFormatter = locale => DateTimeFormatter.ofPattern("EEE',' dd MMM yyyy',' HH:mm", locale) + + def formatDate(date: ZonedDateTime, locale: Locale): String = date.format(DateFormat(locale)) + + 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 epochMinutes(time: ZonedDateTime): Long = time.toInstant.getEpochSecond / 60 + + def minutesSinceBeginOf2018(time: ZonedDateTime): Long = epochMinutes(time) - EpochMinutesTillBeginOf2018 + } + +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..dfb941a --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +include 'common' +include 'bot' +include 'api' +include 'server'