diff --git a/.circleci/config.yml b/.circleci/config.yml index c27bb37db..b78ae7f28 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,7 @@ -version: 2 +version: 2.1 + +orbs: + packtracker: packtracker/report@2.2.2 defaults: &defaults working_directory: ~/itbio @@ -36,15 +39,9 @@ jobs: key: v1-node_modules-{{ checksum "frontend/yarn.lock" }} paths: - frontend/node_modules - - run: | - cd frontend - yarn lint - run: | cd frontend yarn test - - run: | - cd frontend - yarn packtracker build-docker: <<: *defaults machine: true @@ -72,6 +69,10 @@ jobs: workflows: version: 2 + packtracker: + jobs: + - packtracker/report: + project_root: "./frontend" build: jobs: - test-backend diff --git a/Makefile b/Makefile index 6d543b029..3ae335fd5 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,9 @@ TAG := $$(git rev-parse --short=8 HEAD) FRONTEND_IMG := ${NAME}-frontend:${TAG} BACKEND_IMG := ${NAME}-backend:${TAG} DIRECTORY := $$(pwd) - + frontend: - cd ${DIRECTORY}/frontend; docker build -t ${FRONTEND_IMG} . + cd ${DIRECTORY}/frontend; docker build -t ${FRONTEND_IMG} --build-arg TAG=$$(git rev-parse HEAD) . docker tag ${FRONTEND_IMG} ${NAME}-frontend:latest backend: @@ -18,7 +18,7 @@ docker: frontend backend push: docker push ${NAME}-frontend docker push ${NAME}-backend - + login: @docker login -u ${DOCKER_USER} -p ${DOCKER_PASS} diff --git a/backend/build.gradle b/backend/build.gradle index ddfd7e546..eabae193b 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -2,9 +2,9 @@ import java.time.ZonedDateTime buildscript { ext { - kotlinVersion = '1.2.71' - springBootVersion = '2.0.5.RELEASE' - junitJupiterVersion = '5.3.1' + kotlinVersion = '1.3.40' + springBootVersion = '2.1.7.RELEASE' + junitJupiterVersion = '5.5.1' } repositories { @@ -19,10 +19,10 @@ buildscript { } plugins { - id("org.ajoberstar.grgit") version '3.0.0-rc.2' - id("net.researchgate.release") version '2.7.0' - id('com.google.cloud.tools.jib') version '0.9.11' - id "com.github.ben-manes.versions" version '0.20.0' + id("org.ajoberstar.grgit") version '3.1.1' + id("net.researchgate.release") version '2.8.1' + id('com.google.cloud.tools.jib') version '1.5.0' + id "com.github.ben-manes.versions" version '0.22.0' } apply plugin: "kotlin" @@ -45,6 +45,8 @@ repositories { dependencies { compile("com.graphql-java:graphql-spring-boot-starter:5.0.2") +// UI for GraphQL queries available at /graphiql +// compile("com.graphql-java:graphiql-spring-boot-starter:5.0.2") compile("com.graphql-java:graphql-java-tools:5.2.4") compile("com.graphql-java:graphql-java:9.4") compile("org.springframework.boot:spring-boot-starter-web-services:$springBootVersion") @@ -53,8 +55,8 @@ dependencies { compile("org.springframework.boot:spring-boot-starter-cache:$springBootVersion") compile("org.springframework.boot:spring-boot-starter-security:$springBootVersion") compile("org.springframework.boot:spring-boot-starter-mustache:$springBootVersion") - compile("org.springframework.security:spring-security-jwt:1.0.9.RELEASE") - compile("org.springframework.security.oauth:spring-security-oauth2:2.3.3.RELEASE") + compile("org.springframework.security:spring-security-jwt:1.0.10.RELEASE") + compile("org.springframework.security.oauth:spring-security-oauth2:2.3.6.RELEASE") compile("org.springframework.boot:spring-boot-devtools:$springBootVersion") @@ -63,19 +65,19 @@ dependencies { compile("org.springframework.data:spring-data-commons") compile("com.github.ben-manes.caffeine:caffeine:2.+") - compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.7") - compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.7") - compile("com.googlecode.libphonenumber:libphonenumber:8.9.15") - compile("com.google.guava:guava:26.0-jre") - compile("net.sf.biweekly:biweekly:0.6.2") + compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.9") + compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.9") + compile("com.googlecode.libphonenumber:libphonenumber:8.10.17") + compile("com.google.guava:guava:28.0-jre") + compile("net.sf.biweekly:biweekly:0.6.3") /* The following are needed under java 11 */ - runtime("org.apache.commons:commons-lang3:3.8.1") + runtime("org.apache.commons:commons-lang3:3.9") runtime("javax.xml.bind:jaxb-api:2.3.0") runtime("com.sun.xml.bind:jaxb-impl:2.3.0") runtime("com.sun.xml.bind:jaxb-core:2.3.0") - runtime("javax.activation:activation:1.1.1") - compile("org.apache.httpcomponents:httpclient:4.5.4") + runtime("javax.activation:activation:1.1") + compile("org.apache.httpcomponents:httpclient:4.5.9") compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}") compile("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") @@ -84,7 +86,7 @@ dependencies { testCompile("org.springframework.boot:spring-boot-starter-test:$springBootVersion") testCompile("org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}") testRuntime("org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}") - testCompile("org.mockito:mockito-core:2.23.0") + testCompile("org.mockito:mockito-core:2.28.2") } diff --git a/backend/gradle/wrapper/gradle-wrapper.jar b/backend/gradle/wrapper/gradle-wrapper.jar index 0d4a95168..87b738cbd 100644 Binary files a/backend/gradle/wrapper/gradle-wrapper.jar and b/backend/gradle/wrapper/gradle-wrapper.jar differ diff --git a/backend/gradle/wrapper/gradle-wrapper.properties b/backend/gradle/wrapper/gradle-wrapper.properties index e0b3fb8d7..ef9a9e05e 100644 --- a/backend/gradle/wrapper/gradle-wrapper.properties +++ b/backend/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/backend/gradlew b/backend/gradlew index cccdd3d51..af6708ff2 100755 --- a/backend/gradlew +++ b/backend/gradlew @@ -28,7 +28,7 @@ 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="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/backend/gradlew.bat b/backend/gradlew.bat index e95643d6a..0f8d5937c 100644 --- a/backend/gradlew.bat +++ b/backend/gradlew.bat @@ -14,7 +14,7 @@ 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= +set DEFAULT_JVM_OPTS="-Xmx64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/backend/mongo-migrations b/backend/mongo-migrations index aaea27481..ee7fb5090 100644 --- a/backend/mongo-migrations +++ b/backend/mongo-migrations @@ -1,6 +1,11 @@ ### Pending migrations ### N/A +### Migrations already done in deployed application ### +db.movie.update({}, {$rename:{"sfId":"filmstadenId"}}, false, true); +db.movie.update({}, {$rename:{"sfSlug":"filmstadenSlug"}}, false, true); +db.user.update({}, {$rename:{"sfMembershipId":"filmstadenMembershipId"}}, false, true); + ### Migrations already done in deployed application ### # Change fields to @DBRef on all showings db.showing.find({}).forEach(s => { diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/Application.kt b/backend/src/main/kotlin/rocks/didit/sefilm/Application.kt index 6c7180b0e..ca2c56c80 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/Application.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/Application.kt @@ -45,7 +45,7 @@ import rocks.didit.sefilm.graphql.GraphqlExceptionHandler import rocks.didit.sefilm.notification.MailSettings import rocks.didit.sefilm.notification.PushoverSettings import rocks.didit.sefilm.services.SlugService -import rocks.didit.sefilm.services.external.SFService +import rocks.didit.sefilm.services.external.FilmstadenService import rocks.didit.sefilm.utils.MovieFilterUtil import rocks.didit.sefilm.web.controllers.CalendarController import java.math.BigDecimal @@ -57,184 +57,182 @@ import java.math.BigDecimal @EnableScheduling @EnableConfigurationProperties(Properties::class) class Application { - private val log = LoggerFactory.getLogger(Application::class.java) - - companion object { - const val API_BASE_PATH = "/api" - } - - @Bean - fun customMongoConverters(): MongoCustomConversions { - val converters = listOf(ImdbIdConverter(), TmdbIdConverter()) - return MongoCustomConversions(converters) - } - - @Bean - fun httpClient(): HttpClient { - return HttpClientBuilder.create() - .setUserAgent("Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0") - .build() - } - - @Bean - fun httpRequestFactory(httpClient: HttpClient) = HttpComponentsClientHttpRequestFactory(httpClient) - - @Bean - @Primary - fun getRestClient(requestFactory: HttpComponentsClientHttpRequestFactory): RestTemplate { - val restClient = RestTemplate(requestFactory) - restClient.errorHandler = ExternalProviderErrorHandler() - return restClient - } - - @Bean - fun getHttpEntityWithJsonAcceptHeader(): HttpEntity { - val httpHeaders = HttpHeaders() - httpHeaders.accept = listOf(MediaType.APPLICATION_JSON_UTF8) - httpHeaders["User-Agent"] = listOf("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36") - return HttpEntity(httpHeaders) - } - - @Bean - fun corsConfigurer(properties: Properties): WebMvcConfigurer { - return object : WebMvcConfigurer { - override fun addCorsMappings(registry: CorsRegistry) { - registry.addMapping("/graphql") - .allowedOrigins(properties.baseUrl.frontend, properties.baseUrl.api) - .allowedMethods("GET", "POST", "HEAD") - .allowCredentials(false) - registry.addMapping("${CalendarController.PATH}/**") - .allowedOrigins(properties.baseUrl.frontend, properties.baseUrl.api) - .allowedMethods("GET", "HEAD") - .allowCredentials(false) - } + private val log = LoggerFactory.getLogger(Application::class.java) + + companion object { + const val API_BASE_PATH = "/api" + } + + @Bean + fun httpClient(): HttpClient { + return HttpClientBuilder.create() + .setUserAgent("Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0") + .build() + } + + @Bean + fun httpRequestFactory(httpClient: HttpClient) = HttpComponentsClientHttpRequestFactory(httpClient) + + @Bean + @Primary + fun getRestClient(requestFactory: HttpComponentsClientHttpRequestFactory): RestTemplate { + val restClient = RestTemplate(requestFactory) + restClient.errorHandler = ExternalProviderErrorHandler() + return restClient + } + + @Bean + fun getHttpEntityWithJsonAcceptHeader(): HttpEntity { + val httpHeaders = HttpHeaders() + httpHeaders.accept = listOf(MediaType.APPLICATION_JSON_UTF8) + httpHeaders["User-Agent"] = listOf("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36") + return HttpEntity(httpHeaders) + } + + @Bean + fun corsConfigurer(properties: Properties): WebMvcConfigurer { + return object : WebMvcConfigurer { + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/graphql") + .allowedOrigins(properties.baseUrl.frontend, properties.baseUrl.api) + .allowedMethods("GET", "POST", "HEAD") + .allowCredentials(false) + registry.addMapping("${CalendarController.PATH}/**") + .allowedOrigins(properties.baseUrl.frontend, properties.baseUrl.api) + .allowedMethods("GET", "HEAD") + .allowCredentials(false) + } + } } - } - - @Bean - fun removeUnwantedMovies(movieRepository: MovieRepository, titleExtensions: MovieFilterUtil) = ApplicationRunner { _ -> - val unwantedMovies = movieRepository - .findAll() - .filter { titleExtensions.isMovieUnwantedBasedOnGenre(it.genres) || titleExtensions.isTitleUnwanted(it.title) } - log.info("Deleting ${unwantedMovies.size} unwanted movies") - movieRepository.deleteAll(unwantedMovies) - } - - @Bean - fun createSlugsAndWebIds(showingRepository: ShowingRepository, slugService: SlugService) = ApplicationRunner { _ -> - val showingsWithMissingWebId = showingRepository - .findAll() - .filter { - it.webId == Base64ID.MISSING - } - - val updatedShowings = showingsWithMissingWebId.map { - it.copy(webId = Base64ID.random(), slug = slugService.generateSlugFor(it)) + + @Bean + fun removeUnwantedMovies(movieRepository: MovieRepository, titleExtensions: MovieFilterUtil) = ApplicationRunner { _ -> + val unwantedMovies = movieRepository + .findAll() + .filter { titleExtensions.isMovieUnwantedBasedOnGenre(it.genres) || titleExtensions.isTitleUnwanted(it.title) } + log.info("Deleting ${unwantedMovies.size} unwanted movies") + movieRepository.deleteAll(unwantedMovies) } - showingRepository.saveAll(updatedShowings) - } - - @Bean - fun trimMovieNames(movieRepository: MovieRepository, titleExtensions: MovieFilterUtil) = ApplicationRunner { _ -> - movieRepository.findAll() - .filter { - titleExtensions.titleRequiresTrimming(it.title) - } - .forEach { - val newTitle = titleExtensions.trimTitle(it.title) - val updatedMovie = it.copy(title = newTitle) - log.info("Updating title: '${it.title}' -> '$newTitle'") - movieRepository.save(updatedMovie) - } - } - - private data class LocationAliasDTO(val sfId: String, val alias: List) - - @Bean - fun seedInitialData( - locationsRepo: LocationRepository, - budordRepo: BudordRepository, - sfService: SFService, - properties: Properties - ) = ApplicationRunner { _ -> - if (!properties.enableSeeding) { - log.info("Seeding not enabled, ignoring...") - return@ApplicationRunner + + @Bean + fun createSlugsAndWebIds(showingRepository: ShowingRepository, slugService: SlugService) = ApplicationRunner { _ -> + val showingsWithMissingWebId = showingRepository + .findAll() + .filter { + it.webId == Base64ID.MISSING + } + + val updatedShowings = showingsWithMissingWebId.map { + it.copy(webId = Base64ID.random(), slug = slugService.generateSlugFor(it)) + } + showingRepository.saveAll(updatedShowings) } - if (!properties.tmdb.apiKeyExists()) { - log.warn("TMDB api key not set. Some features will not work properly!") + + @Bean + fun trimMovieNames(movieRepository: MovieRepository, titleExtensions: MovieFilterUtil) = ApplicationRunner { _ -> + movieRepository.findAll() + .filter { + titleExtensions.titleRequiresTrimming(it.title) + } + .forEach { + val newTitle = titleExtensions.trimTitle(it.title) + val updatedMovie = it.copy(title = newTitle) + log.info("Updating title: '${it.title}' -> '$newTitle'") + movieRepository.save(updatedMovie) + } } - val objectMapper: ObjectMapper = Jackson2ObjectMapperBuilder.json().build() - - val budordResource = ClassPathResource("seeds/budord.json") - val budords: List = objectMapper.readValue(budordResource.inputStream) - budordRepo.saveAll(budords) - log.info("Seeded budord with ${budords.size} values") - - val locationsResource = ClassPathResource("seeds/locations.json") - val locations: List = objectMapper.readValue(locationsResource.inputStream) - - val locationNameAliasResource = ClassPathResource("seeds/sf-location-aliases.json") - val locationNameAlias: List = objectMapper.readValue(locationNameAliasResource.inputStream) - - val defaultCity = properties.defaultCity - val locationsFromSF = sfService.getLocationsInCity(properties.defaultCity) - .map { - Location( - it.title, - it.address.city["alias"], - it.address.city["name"], - it.address.streetAddress, - it.address.postalCode, - it.address.postalAddress, - BigDecimal(it.address.coordinates.latitude), - BigDecimal(it.address.coordinates.longitude), - it.ncgId, - alias = locationNameAlias.firstOrNull { alias -> alias.sfId == it.ncgId }?.alias ?: listOf() - ) - } - - locationsRepo.saveAll(locations) - log.info("Seeded locations with ${locations.size} values") - - log.info("Seeded ${locationsRepo.saveAll(locationsFromSF).count()} locations from SF for city: $defaultCity") - } - - @Bean - fun graphqlExecutionStrategy(graphqlExceptionHandler: GraphqlExceptionHandler) = - AsyncExecutionStrategy(graphqlExceptionHandler) - - @Bean - fun graphQLServletObjectMapperConfigurer(): ObjectMapperConfigurer { - return ObjectMapperConfigurer { - it.findAndRegisterModules() - .registerModule(JavaTimeModule()) - .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + private data class LocationAliasDTO(val filmstadenId: String, val alias: List) + + @Bean + fun seedInitialData( + locationsRepo: LocationRepository, + budordRepo: BudordRepository, + filmstadenService: FilmstadenService, + properties: Properties + ) = ApplicationRunner { _ -> + if (!properties.enableSeeding) { + log.info("Seeding not enabled, ignoring...") + return@ApplicationRunner + } + + log.info("Reassignment enabled=${properties.enableReassignment}") + + if (!properties.tmdb.apiKeyExists()) { + log.warn("TMDB api key not set. Some features will not work properly!") + } + + val objectMapper: ObjectMapper = Jackson2ObjectMapperBuilder.json().build() + + val budordResource = ClassPathResource("seeds/budord.json") + val budords: List = objectMapper.readValue(budordResource.inputStream) + budordRepo.saveAll(budords) + log.info("Seeded budord with ${budords.size} values") + + val locationsResource = ClassPathResource("seeds/locations.json") + val locations: List = objectMapper.readValue(locationsResource.inputStream) + + val locationNameAliasResource = ClassPathResource("seeds/filmstaden-location-aliases.json") + val locationNameAlias: List = objectMapper.readValue(locationNameAliasResource.inputStream) + + val defaultCity = properties.defaultCity + val locationsFromFilmstaden = filmstadenService.getLocationsInCity(properties.defaultCity) + .map { + Location( + it.title, + it.address.city["alias"], + it.address.city["name"], + it.address.streetAddress, + it.address.postalCode, + it.address.postalAddress, + BigDecimal(it.address.coordinates.latitude), + BigDecimal(it.address.coordinates.longitude), + it.ncgId, + alias = locationNameAlias.firstOrNull { alias -> alias.filmstadenId == it.ncgId }?.alias + ?: listOf() + ) + } + + locationsRepo.saveAll(locations) + log.info("Seeded locations with ${locations.size} values") + + log.info("Seeded ${locationsRepo.saveAll(locationsFromFilmstaden).count()} locations from Filmstaden for city: $defaultCity") } - } - - @Bean - fun graphQLToolsObjectMapperConfig(): com.coxautodev.graphql.tools.ObjectMapperConfigurer { - return com.coxautodev.graphql.tools.ObjectMapperConfigurer { mapper, _ -> - mapper.findAndRegisterModules() - .registerModule(JavaTimeModule()) - .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + + @Bean + fun graphqlExecutionStrategy(graphqlExceptionHandler: GraphqlExceptionHandler) = + AsyncExecutionStrategy(graphqlExceptionHandler) + + @Bean + fun graphQLServletObjectMapperConfigurer(): ObjectMapperConfigurer { + return ObjectMapperConfigurer { + it.findAndRegisterModules() + .registerModule(JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + } } - } - - @Bean - fun schemaParserOptions(objConfigurer: com.coxautodev.graphql.tools.ObjectMapperConfigurer) = SchemaParserOptions - .newOptions() - .objectMapperConfigurer(objConfigurer) - .build() - - @Bean - fun schemaDictionary() = SchemaParserDictionary() - .add(MailSettings::class) - .add(PushoverSettings::class) + + @Bean + fun graphQLToolsObjectMapperConfig(): com.coxautodev.graphql.tools.ObjectMapperConfigurer { + return com.coxautodev.graphql.tools.ObjectMapperConfigurer { mapper, _ -> + mapper.findAndRegisterModules() + .registerModule(JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + } + } + + @Bean + fun schemaParserOptions(objConfigurer: com.coxautodev.graphql.tools.ObjectMapperConfigurer) = SchemaParserOptions + .newOptions() + .objectMapperConfigurer(objConfigurer) + .build() + + @Bean + fun schemaDictionary() = SchemaParserDictionary() + .add(MailSettings::class) + .add(PushoverSettings::class) } fun main(args: Array) { - runApplication(*args) + runApplication(*args) } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/Exceptions.kt b/backend/src/main/kotlin/rocks/didit/sefilm/Exceptions.kt index edb3eb620..d3352c09c 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/Exceptions.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/Exceptions.kt @@ -5,26 +5,26 @@ import rocks.didit.sefilm.domain.UserID import java.util.* class NotFoundException(what: String, userID: UserID? = null, showingId: UUID? = null) : - KnownException("Could not find $what", userID, showingId) + KnownException("Could not find $what", userID, showingId) class MissingPhoneNumberException(userID: UserID) : - KnownException("You are missing a phone number.", userID) + KnownException("You are missing a phone number.", userID) class MissingParametersException(what: String = "") : KnownException("Some required parameters were missing: $what") class ExternalProviderException(msg: String) : - KnownException("Unable to fetch information from external provider: $msg") + KnownException("Unable to fetch information from external provider: $msg") class MissingAPIKeyException(service: APIService) : KnownException("The API key is missing or invalid for the $service service") class TicketsAlreadyBoughtException(userID: UserID, showingId: UUID) : - KnownException("The action is not allowed since the tickets for this showing is already bought", userID, showingId) + KnownException("The action is not allowed since the tickets for this showing is already bought", userID, showingId) class TicketAlreadyInUserException(userID: UserID) : - KnownException("One or more of your företagsbiljeter is already in use", userID) + KnownException("One or more of your företagsbiljeter is already in use", userID) class UserAlreadyAttendedException(userID: UserID) : - KnownException("The user has already attended this showing", userID) + KnownException("The user has already attended this showing", userID) class TicketNotFoundException(ticketNumber: TicketNumber) : KnownException("Ticket $ticketNumber not found") @@ -33,22 +33,22 @@ class DuplicateTicketException(msg: String = "") : KnownException("Found duplica class TicketAlreadyUsedException(whichTicket: TicketNumber) : KnownException("The ticket $whichTicket has already been used") class TicketExpiredException(whichTicket: TicketNumber) : - KnownException("The ticket $whichTicket has expired or will expire before the showing will be bought") + KnownException("The ticket $whichTicket has expired or will expire before the showing will be bought") class TicketInUseException(whichTicket: TicketNumber) : - KnownException("The ticket $whichTicket is already in use on a showing and cannot be removed") + KnownException("The ticket $whichTicket is already in use on a showing and cannot be removed") -class SfTicketException(msg: String) : KnownException(msg) +class FilmstadenTicketException(msg: String) : KnownException(msg) open class KnownException( - msg: String, - val whichUser: UserID? = null, - val whichShowing: UUID? = null + msg: String, + val whichUser: UserID? = null, + val whichShowing: UUID? = null ) : RuntimeException(msg) /** Services which uses some form of tokens, and that can cause InvalidTokenException */ enum class APIService { - Pushover, - IMDb, - TMDb + Pushover, + IMDb, + TMDb } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/Extensions.kt b/backend/src/main/kotlin/rocks/didit/sefilm/Extensions.kt index 1f7c3b5bb..748067aeb 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/Extensions.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/Extensions.kt @@ -9,14 +9,14 @@ import rocks.didit.sefilm.domain.TMDbID import rocks.didit.sefilm.domain.UserID import rocks.didit.sefilm.services.UserService -internal fun currentLoggedInUser(): UserID { - val authentication: Authentication? = SecurityContextHolder.getContext().authentication - if (authentication?.isAuthenticated != true) { - throw IllegalStateException("Cannot get current user if user isn't authenticated") - } +internal fun currentLoggedInUserId(): UserID { + val authentication: Authentication? = SecurityContextHolder.getContext().authentication + if (authentication?.isAuthenticated != true) { + throw IllegalStateException("Cannot get current user if user isn't authenticated") + } - val principal = authentication.principal as OpenIdConnectUserDetails - return UserID(principal.userId) + val principal = authentication.principal as OpenIdConnectUserDetails + return UserID(principal.userId) } internal fun String.toImdbId() = IMDbID.valueOf(this) @@ -25,7 +25,7 @@ internal fun Long.toTmdbId() = TMDbID.valueOf(this) inline fun T?.orElseThrow(exceptionSupplier: () -> E): T = this ?: throw exceptionSupplier() internal fun UserID.lookupUsing(userService: UserService): User { - return userService.getCompleteUser(this).orElseThrow { NotFoundException("user", userID = this) } + return userService.getCompleteUser(this).orElseThrow { NotFoundException("user", userID = this) } } internal fun T.logger() = lazy { LoggerFactory.getLogger(this::class.java) } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/Properties.kt b/backend/src/main/kotlin/rocks/didit/sefilm/Properties.kt index 155ea2253..e6ae3ec17 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/Properties.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/Properties.kt @@ -6,44 +6,45 @@ import org.springframework.stereotype.Component @ConfigurationProperties("sefilm", ignoreInvalidFields = true) @Component class Properties { - var baseUrl = BaseUrl() - var tmdb = Tmdb() - var enableSeeding: Boolean = true - var defaultCity: String = "GB" + var baseUrl = BaseUrl() + var tmdb = Tmdb() + var enableSeeding: Boolean = true + var enableReassignment: Boolean = true + var defaultCity: String = "GB" - var google = Google() + var google = Google() - var notification = Notification() + var notification = Notification() - class Tmdb { - var apikey: String? = null - fun apiKeyExists(): Boolean { - return !apikey.isNullOrBlank() + class Tmdb { + var apikey: String? = null + fun apiKeyExists(): Boolean { + return !apikey.isNullOrBlank() + } + } + + class Google { + var clientId: String = "" + var jwkUri: String = "" + } + + class BaseUrl { + var api: String = "N/A" + var frontend: String = "N/A" + } + + class Notification { + var provider = Provider() + } + + class Provider { + var pushover = Pushover() + } + + class Pushover { + var enabled: Boolean = false + var apiToken: String? = null + var url: String = "" + var validateUrl: String = "" } - } - - class Google { - var clientId: String = "" - var jwkUri: String = "" - } - - class BaseUrl { - var api: String = "N/A" - var frontend: String = "N/A" - } - - class Notification { - var provider = Provider() - } - - class Provider { - var pushover = Pushover() - } - - class Pushover { - var enabled: Boolean = false - var apiToken: String? = null - var url: String = "" - var validateUrl: String = "" - } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/SecurityConfiguration.kt b/backend/src/main/kotlin/rocks/didit/sefilm/SecurityConfiguration.kt index 9b9ff8324..d6f405f60 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/SecurityConfiguration.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/SecurityConfiguration.kt @@ -29,116 +29,116 @@ import java.time.Instant class OpenIdConnectUserDetails(userInfo: Map) : UserDetails { - val userId: String = userInfo.getValue("sub") as String - private val username: String? = userInfo["email"] as String - val firstName: String? = userInfo["given_name"] as String - val lastName: String? = userInfo["family_name"] as String - val avatarUrl: String? = userInfo["picture"] as String - - override fun getUsername(): String? = username - fun getName(): String? = "$firstName $lastName" - override fun getAuthorities() = listOf(SimpleGrantedAuthority("ROLE_USER")) - override fun getPassword(): String? = null - override fun isAccountNonExpired(): Boolean = true - override fun isAccountNonLocked(): Boolean = true - override fun isCredentialsNonExpired(): Boolean = true - override fun isEnabled(): Boolean = true - - override fun toString(): String { - return "OpenIdConnectUserDetails(userId='$userId', username=$username, firstName=$firstName, lastName=$lastName, avatarUrl=$avatarUrl)" - } + val userId: String = userInfo.getValue("sub") as String + private val username: String? = userInfo["email"] as String + val firstName: String? = userInfo["given_name"] as String + val lastName: String? = userInfo["family_name"] as String + val avatarUrl: String? = userInfo["picture"] as String + + override fun getUsername(): String? = username + fun getName(): String? = "$firstName $lastName" + override fun getAuthorities() = listOf(SimpleGrantedAuthority("ROLE_USER")) + override fun getPassword(): String? = null + override fun isAccountNonExpired(): Boolean = true + override fun isAccountNonLocked(): Boolean = true + override fun isCredentialsNonExpired(): Boolean = true + override fun isEnabled(): Boolean = true + + override fun toString(): String { + return "OpenIdConnectUserDetails(userId='$userId', username=$username, firstName=$firstName, lastName=$lastName, avatarUrl=$avatarUrl)" + } } class OpenidUserAuthConverter : UserAuthenticationConverter { - override fun extractAuthentication(map: Map): Authentication? { - val user = OpenIdConnectUserDetails(map) - return UsernamePasswordAuthenticationToken(user, "N/A", user.authorities) - } + override fun extractAuthentication(map: Map): Authentication? { + val user = OpenIdConnectUserDetails(map) + return UsernamePasswordAuthenticationToken(user, "N/A", user.authorities) + } - override fun convertUserAuthentication(userAuthentication: Authentication) = - throw UnsupportedOperationException("This operation is not supported") + override fun convertUserAuthentication(userAuthentication: Authentication) = + throw UnsupportedOperationException("This operation is not supported") } @Component class LoginListener(private val userRepository: UserRepository) : ApplicationListener { - private val log = LoggerFactory.getLogger(LoginListener::class.java) - - override fun onApplicationEvent(event: AuthenticationSuccessEvent) { - createOrUpdateUser(event.authentication) - } - - private fun createOrUpdateUser(authentication: Authentication) { - val principal = authentication.principal as OpenIdConnectUserDetails? - ?: throw IllegalStateException("Successful authentication without a principal") - - val maybeUser: User? = userRepository.findById(UserID(principal.userId)).orElse(null) - if (maybeUser == null) { - val newUser = User( - id = UserID(principal.userId), - name = "${principal.firstName ?: ""} ${principal.lastName ?: ""}", - firstName = principal.firstName, - lastName = principal.lastName, - nick = principal.firstName ?: "Houdini", - email = principal.username ?: "", - avatar = principal.avatarUrl, - lastLogin = Instant.now(), - signupDate = Instant.now() - ) - userRepository.save(newUser) - log.info("Created new user ${newUser.name} (${newUser.id})") - } else { - val updatedUser = maybeUser.copy( - name = "${principal.firstName} ${principal.lastName}", - firstName = principal.firstName, - lastName = principal.lastName, - avatar = principal.avatarUrl, - lastLogin = Instant.now() - ) - if (maybeUser != updatedUser) { - userRepository.save(updatedUser) - } + private val log = LoggerFactory.getLogger(LoginListener::class.java) + + override fun onApplicationEvent(event: AuthenticationSuccessEvent) { + createOrUpdateUser(event.authentication) + } + + private fun createOrUpdateUser(authentication: Authentication) { + val principal = authentication.principal as OpenIdConnectUserDetails? + ?: throw IllegalStateException("Successful authentication without a principal") + + val maybeUser: User? = userRepository.findById(UserID(principal.userId)).orElse(null) + if (maybeUser == null) { + val newUser = User( + id = UserID(principal.userId), + name = "${principal.firstName ?: ""} ${principal.lastName ?: ""}", + firstName = principal.firstName, + lastName = principal.lastName, + nick = principal.firstName ?: "Houdini", + email = principal.username ?: "", + avatar = principal.avatarUrl, + lastLogin = Instant.now(), + signupDate = Instant.now() + ) + userRepository.save(newUser) + log.info("Created new user ${newUser.name} (${newUser.id})") + } else { + val updatedUser = maybeUser.copy( + name = "${principal.firstName} ${principal.lastName}", + firstName = principal.firstName, + lastName = principal.lastName, + avatar = principal.avatarUrl, + lastLogin = Instant.now() + ) + if (maybeUser != updatedUser) { + userRepository.save(updatedUser) + } + } } - } } @Configuration @EnableWebSecurity @EnableResourceServer class ResourceServerConfig( - private val properties: Properties + private val properties: Properties ) : ResourceServerConfigurerAdapter() { - override fun configure(resources: ResourceServerSecurityConfigurer) { - resources - .resourceId(properties.google.clientId) - .stateless(true) - } - - override fun configure(http: HttpSecurity) { - http - .cors().and() - .antMatcher("/**") - .authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/graphql").permitAll() - .antMatchers(HttpMethod.GET, "${CalendarController.PATH}/**").permitAll() - .antMatchers(HttpMethod.HEAD, "${CalendarController.PATH}/**").permitAll() - .antMatchers(HttpMethod.OPTIONS, "${CalendarController.PATH}/**").permitAll() - .antMatchers(HttpMethod.GET, "${MetaController.PATH}/**").permitAll() - .anyRequest().fullyAuthenticated() - } - - @Bean - fun jwkTokenStore(accessTokenConverter: AccessTokenConverter): JwkTokenStore { - return JwkTokenStore(properties.google.jwkUri, accessTokenConverter) - } - - @Bean - fun accessTokenConverter(userAuthenticationConverter: UserAuthenticationConverter): AccessTokenConverter { - val default = DefaultAccessTokenConverter() - default.setUserTokenConverter(userAuthenticationConverter) - return default - } - - @Bean - fun userAuthenticationConverter() = OpenidUserAuthConverter() + override fun configure(resources: ResourceServerSecurityConfigurer) { + resources + .resourceId(properties.google.clientId) + .stateless(true) + } + + override fun configure(http: HttpSecurity) { + http + .cors().and() + .antMatcher("/**") + .authorizeRequests() + .antMatchers(HttpMethod.OPTIONS, "/graphql").permitAll() + .antMatchers(HttpMethod.GET, "${CalendarController.PATH}/**").permitAll() + .antMatchers(HttpMethod.HEAD, "${CalendarController.PATH}/**").permitAll() + .antMatchers(HttpMethod.OPTIONS, "${CalendarController.PATH}/**").permitAll() + .antMatchers(HttpMethod.GET, "${MetaController.PATH}/**").permitAll() + .anyRequest().fullyAuthenticated() + } + + @Bean + fun jwkTokenStore(accessTokenConverter: AccessTokenConverter): JwkTokenStore { + return JwkTokenStore(properties.google.jwkUri, accessTokenConverter) + } + + @Bean + fun accessTokenConverter(userAuthenticationConverter: UserAuthenticationConverter): AccessTokenConverter { + val default = DefaultAccessTokenConverter() + default.setUserTokenConverter(userAuthenticationConverter) + return default + } + + @Bean + fun userAuthenticationConverter() = OpenidUserAuthConverter() } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/clients/ImdbClient.kt b/backend/src/main/kotlin/rocks/didit/sefilm/clients/ImdbClient.kt index 8f4a2ed74..b4a6a0652 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/clients/ImdbClient.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/clients/ImdbClient.kt @@ -24,111 +24,111 @@ import rocks.didit.sefilm.domain.dto.TmdbMovieDetails @Component class ImdbClient( - private val restTemplate: RestTemplate, - private val httpEntity: HttpEntity, - private val properties: Properties + private val restTemplate: RestTemplate, + private val httpEntity: HttpEntity, + private val properties: Properties ) { - companion object { - private const val TMDB_EXTERNAL_SEARCH_URL = - "https://api.themoviedb.org/3/find/%s?api_key=%s&language=en-US&external_source=imdb_id" - private const val TMDB_INFO_URL = "https://api.themoviedb.org/3/movie/%d?api_key=%s&language=en-US" - private const val SEARCH_URL = - "https://v2.sg.media-imdb.com/suggests/%s/%s.json" // Spaces should be replaced with _ - - private fun getSearchUrl(query: String): String { - if (query.isBlank()) throw IllegalArgumentException("Query cannot be blank") - - val normalized = query.removeSwedishChars() - val encodedQuery = UriUtils.encode(normalized.replace(" ", "_"), Charsets.UTF_8) - return SEARCH_URL.format(normalized[0], encodedQuery) + companion object { + private const val TMDB_EXTERNAL_SEARCH_URL = + "https://api.themoviedb.org/3/find/%s?api_key=%s&language=en-US&external_source=imdb_id" + private const val TMDB_INFO_URL = "https://api.themoviedb.org/3/movie/%d?api_key=%s&language=en-US" + private const val SEARCH_URL = + "https://v2.sg.media-imdb.com/suggests/%s/%s.json" // Spaces should be replaced with _ + + private fun getSearchUrl(query: String): String { + if (query.isBlank()) throw IllegalArgumentException("Query cannot be blank") + + val normalized = query.removeSwedishChars() + val encodedQuery = UriUtils.encode(normalized.replace(" ", "_"), Charsets.UTF_8) + return SEARCH_URL.format(normalized[0], encodedQuery) + } + + private fun String.removeSwedishChars() = + this.toLowerCase() + .replace("å", "a") + .replace("ä", "a") + .replace("ö", "o") + .replace("!", "") + .replace("?", "") + .replace("'", "") + .replace(":", "") + .replace("é", "e") } - private fun String.removeSwedishChars() = - this.toLowerCase() - .replace("å", "a") - .replace("ä", "a") - .replace("ö", "o") - .replace("!", "") - .replace("?", "") - .replace("'", "") - .replace(":", "") - .replace("é", "e") - } - - private val log = LoggerFactory.getLogger(ImdbClient::class.java) - - private fun lookupTmdbIdFromImdbId(imdbId: IMDbID): TMDbID { - validateProviderId(imdbId) - validateApiKeyExists(imdbId) - val url = TMDB_EXTERNAL_SEARCH_URL.format(imdbId.value, properties.tmdb.apikey) - val results = restTemplate.exchange( - url, - HttpMethod.GET, - httpEntity, - TmdbFindExternalResults::class.java::class - ).body - - val movieResults = results?.movie_results ?: listOf() - if (movieResults.isEmpty()) { - return TMDbID.UNKNOWN + private val log = LoggerFactory.getLogger(ImdbClient::class.java) + + private fun lookupTmdbIdFromImdbId(imdbId: IMDbID): TMDbID { + validateProviderId(imdbId) + validateApiKeyExists(imdbId) + val url = TMDB_EXTERNAL_SEARCH_URL.format(imdbId.value, properties.tmdb.apikey) + val results = restTemplate.exchange( + url, + HttpMethod.GET, + httpEntity, + TmdbFindExternalResults::class.java::class + ).body + + val movieResults = results?.movie_results ?: listOf() + if (movieResults.isEmpty()) { + return TMDbID.UNKNOWN + } + return TMDbID.valueOf(movieResults[0].id) } - return TMDbID.valueOf(movieResults[0].id) - } - - fun search(title: String): List { - val url = getSearchUrl(title) - val resultWithCallback = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String::javaClass).body - val resultWithoutCallback = removeCallback(resultWithCallback ?: "") - return jsonToSearchResult(resultWithoutCallback).d - ?.filter { it.q == "feature" } ?: listOf() - } - - /** - * Lookup TMDb movie details based on the supplied IMDb ID. - * @throws MissingAPIKeyException if the TMDb API-key hasn't been supplied - * @throws ExternalProviderException if the movie was not found or if the ID didn't uniquely identify the movie - */ - fun movieDetails(imdbId: IMDbID): TmdbMovieDetails { - validateProviderId(imdbId) - validateApiKeyExists(imdbId) - - val tmdbId = lookupTmdbIdFromImdbId(imdbId) - if (tmdbId.isNotSupplied()) { - throw ExternalProviderException("[IMDb] movie[$imdbId] not found") + + fun search(title: String): List { + val url = getSearchUrl(title) + val resultWithCallback = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String::javaClass).body + val resultWithoutCallback = removeCallback(resultWithCallback ?: "") + return jsonToSearchResult(resultWithoutCallback).d + ?.filter { it.q == "feature" } ?: listOf() + } + + /** + * Lookup TMDb movie details based on the supplied IMDb ID. + * @throws MissingAPIKeyException if the TMDb API-key hasn't been supplied + * @throws ExternalProviderException if the movie was not found or if the ID didn't uniquely identify the movie + */ + fun movieDetails(imdbId: IMDbID): TmdbMovieDetails { + validateProviderId(imdbId) + validateApiKeyExists(imdbId) + + val tmdbId = lookupTmdbIdFromImdbId(imdbId) + if (tmdbId.isNotSupplied()) { + throw ExternalProviderException("[IMDb] movie[$imdbId] not found") + } + return movieDetailsExact(tmdbId) } - return movieDetailsExact(tmdbId) - } - - fun movieDetailsExact(tmdbId: TMDbID): TmdbMovieDetails { - validateProviderId(tmdbId) - validateApiKeyExists(tmdbId) - - val url = TMDB_INFO_URL.format(tmdbId.value, properties.tmdb.apikey) - return restTemplate.exchange(url, HttpMethod.GET, httpEntity, TmdbMovieDetails::class).body - ?: throw ExternalProviderException("[TMDb] Null response body") - } - - private fun validateProviderId(id: ExternalProviderId) { - if (id.isNotSupplied()) throw IllegalArgumentException("Provider ID is not supplied. Got: $id") - } - - private fun validateApiKeyExists(id: ExternalProviderId) { - if (!properties.tmdb.apiKeyExists()) { - log.warn("TMDb API key not set. Unable to fetch info for $id") - throw MissingAPIKeyException(APIService.TMDb) + + fun movieDetailsExact(tmdbId: TMDbID): TmdbMovieDetails { + validateProviderId(tmdbId) + validateApiKeyExists(tmdbId) + + val url = TMDB_INFO_URL.format(tmdbId.value, properties.tmdb.apikey) + return restTemplate.exchange(url, HttpMethod.GET, httpEntity, TmdbMovieDetails::class).body + ?: throw ExternalProviderException("[TMDb] Null response body") + } + + private fun validateProviderId(id: ExternalProviderId) { + if (id.isNotSupplied()) throw IllegalArgumentException("Provider ID is not supplied. Got: $id") + } + + private fun validateApiKeyExists(id: ExternalProviderId) { + if (!properties.tmdb.apiKeyExists()) { + log.warn("TMDb API key not set. Unable to fetch info for $id") + throw MissingAPIKeyException(APIService.TMDb) + } + } + + /** IMDb returns jsonp with a callback we need to remove */ + private fun removeCallback(result: String): String { + if (result.isBlank()) throw IllegalArgumentException("Result with callback mustn't be empty") + val firstParenthesis = result.indexOf("(") + 1 + val lastParenthesis = result.lastIndexOf(")") + return result.substring(firstParenthesis, lastParenthesis) + } + + private fun jsonToSearchResult(json: String): ImdbSearchResults { + val objectMapper = Jackson2ObjectMapperBuilder.json().build() + return objectMapper.readValue(json) } - } - - /** IMDb returns jsonp with a callback we need to remove */ - private fun removeCallback(result: String): String { - if (result.isBlank()) throw IllegalArgumentException("Result with callback mustn't be empty") - val firstParenthesis = result.indexOf("(") + 1 - val lastParenthesis = result.lastIndexOf(")") - return result.substring(firstParenthesis, lastParenthesis) - } - - private fun jsonToSearchResult(json: String): ImdbSearchResults { - val objectMapper = Jackson2ObjectMapperBuilder.json().build() - return objectMapper.readValue(json) - } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/configuration/MongoConfiguration.kt b/backend/src/main/kotlin/rocks/didit/sefilm/configuration/MongoConfiguration.kt index cb164a7f7..9b9af54f5 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/configuration/MongoConfiguration.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/configuration/MongoConfiguration.kt @@ -11,9 +11,9 @@ import rocks.didit.sefilm.database.TmdbIdConverter @EnableMongoAuditing class MongoConfiguration { - @Bean - fun customMongoConverters(): MongoCustomConversions { - val converters = listOf(ImdbIdConverter(), TmdbIdConverter()) - return MongoCustomConversions(converters) - } + @Bean + fun customMongoConverters(): MongoCustomConversions { + val converters = listOf(ImdbIdConverter(), TmdbIdConverter()) + return MongoCustomConversions(converters) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/configuration/UserAuditorAware.kt b/backend/src/main/kotlin/rocks/didit/sefilm/configuration/UserAuditorAware.kt index 4cbf6f8bb..ef30195e9 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/configuration/UserAuditorAware.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/configuration/UserAuditorAware.kt @@ -8,5 +8,5 @@ import java.util.* @Component class UserAuditorAware(private val userService: UserService) : AuditorAware { - override fun getCurrentAuditor() = Optional.ofNullable(userService.currentUserOrNull()) + override fun getCurrentAuditor() = Optional.ofNullable(userService.currentUserOrNull()) } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/Converters.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/Converters.kt index 1923242aa..705901cde 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/Converters.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/Converters.kt @@ -7,23 +7,23 @@ import rocks.didit.sefilm.domain.TMDbID @Component class ImdbIdConverter : Converter { - override fun convert(source: String?): IMDbID { - if (source == null) { - return IMDbID.MISSING + override fun convert(source: String?): IMDbID { + if (source == null) { + return IMDbID.MISSING + } + if (source == "N/A") { + return IMDbID.UNKNOWN + } + return IMDbID.valueOf(source) } - if (source == "N/A") { - return IMDbID.UNKNOWN - } - return IMDbID.valueOf(source) - } } @Component class TmdbIdConverter : Converter { - override fun convert(source: Long?): TMDbID { - if (source == null) { - return TMDbID.MISSING + override fun convert(source: Long?): TMDbID { + if (source == null) { + return TMDbID.MISSING + } + return TMDbID.valueOf(source) } - return TMDbID.valueOf(source) - } } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/BioBudord.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/BioBudord.kt index 7f0fa8499..c48e56b84 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/BioBudord.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/BioBudord.kt @@ -5,7 +5,7 @@ import org.springframework.data.mongodb.core.mapping.Document @Document data class BioBudord( - @Id - val number: Long = -1, - val phrase: String = "" + @Id + val number: Long = -1, + val phrase: String = "" ) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/SfMeta.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/FilmstadenMeta.kt similarity index 53% rename from backend/src/main/kotlin/rocks/didit/sefilm/database/entities/SfMeta.kt rename to backend/src/main/kotlin/rocks/didit/sefilm/database/entities/FilmstadenMeta.kt index 7eed73db0..4d3d08bfd 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/SfMeta.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/FilmstadenMeta.kt @@ -6,11 +6,11 @@ import org.springframework.data.mongodb.core.mapping.Document import java.time.Instant @Document -class SfMeta( - @Id - @JsonIgnore - val key: String = "sfpopulate", - val timestamp: Instant? = null, - val description: String = "N/A", - val value: Any? = null +class FilmstadenMeta( + @Id + @JsonIgnore + val key: String = "filmstadenPopulate", + val timestamp: Instant? = null, + val description: String = "N/A", + val value: Any? = null ) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Location.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Location.kt index 0abf1b8a7..1a399b32d 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Location.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Location.kt @@ -6,33 +6,33 @@ import java.math.BigDecimal @Document data class Location( - @Id - val name: String? = null, - /** This is the city alias that SF uses. GB is the alias for Göteborg e.g. */ - val cityAlias: String? = null, - val city: String? = null, - val streetAddress: String? = null, - val postalCode: String? = null, - val postalAddress: String? = null, - val latitude: BigDecimal? = null, - val longitude: BigDecimal? = null, - val sfId: String? = null, - val alias: List = listOf() + @Id + val name: String? = null, + /** This is the city alias that Filmstaden uses. GB is the alias for Göteborg e.g. */ + val cityAlias: String? = null, + val city: String? = null, + val streetAddress: String? = null, + val postalCode: String? = null, + val postalAddress: String? = null, + val latitude: BigDecimal? = null, + val longitude: BigDecimal? = null, + val filmstadenId: String? = null, + val alias: List = listOf() ) { - fun isSfLocation() = sfId != null + fun isFilmstadenLocation() = filmstadenId != null - fun hasAlias(other: String) = alias.contains(other) + fun hasAlias(other: String) = alias.contains(other) - fun formatAddress(): String { - if (name == null) { - return "" - } + fun formatAddress(): String { + if (name == null) { + return "" + } - return if (streetAddress != null) { - "$name, $streetAddress" - } else { - name + return if (streetAddress != null) { + "$name, $streetAddress" + } else { + name + } } - } } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Movie.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Movie.kt index 8534670b4..9a4027a15 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Movie.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Movie.kt @@ -13,35 +13,35 @@ import java.util.* @Document data class Movie( - @Id - val id: UUID = UUID.randomUUID(), - val imdbId: IMDbID = IMDbID.MISSING, - val sfId: String? = null, - val sfSlug: String? = null, - val tmdbId: TMDbID = TMDbID.MISSING, - val title: String = "", - val synopsis: String? = null, - val originalTitle: String? = null, - val releaseDate: LocalDate = LocalDate.now(), - val productionYear: Int? = null, - val runtime: Duration = Duration.ZERO, - val poster: String? = null, - val genres: Collection = emptyList(), - @LastModifiedDate - val lastModifiedDate: Instant = Instant.EPOCH, - /** How popular the movie is, between 0 and infinity. Updated regularly */ - val popularity: Double = 0.0, - val popularityLastUpdated: Instant = Instant.EPOCH, - /** If the movie is archived then it will be excluded from common functions - * such as scheduled updates and it won't be visible to end users */ - val archived: Boolean = false + @Id + val id: UUID = UUID.randomUUID(), + val imdbId: IMDbID = IMDbID.MISSING, + val filmstadenId: String? = null, + val filmstadenSlug: String? = null, + val tmdbId: TMDbID = TMDbID.MISSING, + val title: String = "", + val synopsis: String? = null, + val originalTitle: String? = null, + val releaseDate: LocalDate = LocalDate.now(), + val productionYear: Int? = null, + val runtime: Duration = Duration.ZERO, + val poster: String? = null, + val genres: Collection = emptyList(), + @LastModifiedDate + val lastModifiedDate: Instant = Instant.EPOCH, + /** How popular the movie is, between 0 and infinity. Updated regularly */ + val popularity: Double = 0.0, + val popularityLastUpdated: Instant = Instant.EPOCH, + /** If the movie is archived then it will be excluded from common functions + * such as scheduled updates and it won't be visible to end users */ + val archived: Boolean = false ) { - /** Should we do an extended query to find more information about this movie? */ - fun needsMoreInfo() = - synopsis == null || poster == null || (durationUntilRelease().toDays() < 14 && runtime == Duration.ZERO) + /** Should we do an extended query to find more information about this movie? */ + fun needsMoreInfo() = + synopsis == null || poster == null || (durationUntilRelease().toDays() < 14 && runtime == Duration.ZERO) - fun isPopularityOutdated() = Duration.between(popularityLastUpdated, Instant.now()).toDays() >= 2 + fun isPopularityOutdated() = Duration.between(popularityLastUpdated, Instant.now()).toDays() >= 2 - fun durationUntilRelease(): Duration = Duration.between(LocalDateTime.now(), releaseDate.atTime(0, 0)) + fun durationUntilRelease(): Duration = Duration.between(LocalDateTime.now(), releaseDate.atTime(0, 0)) } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/ParticipantPaymentInfo.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/ParticipantPaymentInfo.kt index 626462249..84ce3ac19 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/ParticipantPaymentInfo.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/ParticipantPaymentInfo.kt @@ -8,12 +8,12 @@ import java.util.* @Document(collection = "participantInfo") data class ParticipantPaymentInfo( - @Id - val id: UUID = UUID.randomUUID(), - val userId: UserID = UserID(), - val showingId: UUID = UUID.randomUUID(), - val hasPaid: Boolean = false, - val amountOwed: SEK = SEK(0) + @Id + val id: UUID = UUID.randomUUID(), + val userId: UserID = UserID(), + val showingId: UUID = UUID.randomUUID(), + val hasPaid: Boolean = false, + val amountOwed: SEK = SEK(0) ) diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Showing.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Showing.kt index 6ba3c1aed..81c88cbb1 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Showing.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Showing.kt @@ -9,58 +9,58 @@ import rocks.didit.sefilm.MissingParametersException import rocks.didit.sefilm.domain.Base64ID import rocks.didit.sefilm.domain.Participant import rocks.didit.sefilm.domain.SEK -import rocks.didit.sefilm.domain.dto.SfLiteScreenDTO +import rocks.didit.sefilm.domain.dto.FilmstadenLiteScreenDTO import rocks.didit.sefilm.domain.dto.ShowingDTO import java.time.Instant import java.time.LocalDate -import java.time.LocalDateTime import java.time.LocalTime import java.util.* @Document data class Showing( - @Id - val id: UUID = UUID.randomUUID(), - val webId: Base64ID = Base64ID.MISSING, - val slug: String = "slug-less", - val date: LocalDate? = null, - val time: LocalTime? = null, - @DBRef - val movie: Movie = Movie(), - val location: Location? = null, - val sfScreen: SfLiteScreenDTO? = null, - val private: Boolean = false, - val price: SEK? = null, - val ticketsBought: Boolean = false, - @DBRef - val admin: User = User(), - @DBRef - val payToUser: User = admin, - val expectedBuyDate: LocalDate? = null, - val participants: Set = setOf(), - @LastModifiedDate - val lastModifiedDate: Instant = Instant.EPOCH, - @CreatedDate - val createdDate: Instant = Instant.now() + @Id + val id: UUID = UUID.randomUUID(), + val webId: Base64ID = Base64ID.MISSING, + val slug: String = "slug-less", + val date: LocalDate? = null, + val time: LocalTime? = null, + @DBRef + val movie: Movie = Movie(), + val location: Location? = null, + val filmstadenScreen: FilmstadenLiteScreenDTO? = null, + val private: Boolean = false, + val price: SEK? = null, + val ticketsBought: Boolean = false, + @DBRef + val admin: User = User(), + @DBRef + val payToUser: User = admin, + val expectedBuyDate: LocalDate? = null, + val participants: Set = setOf(), + @LastModifiedDate + val lastModifiedDate: Instant = Instant.EPOCH, + @CreatedDate + val createdDate: Instant = Instant.now(), + val filmstadenRemoteEntityId: String? = null ) { - - fun toDto() = ShowingDTO( - id = id, - webId = webId, - slug = slug, - date = date ?: throw MissingParametersException("date"), - time = time ?: throw MissingParametersException("time"), - movieId = movie.id, - location = location ?: throw MissingParametersException("location"), - sfScreen = sfScreen, - private = private, - price = price, - ticketsBought = ticketsBought, - admin = admin.id, - payToUser = payToUser.id, - expectedBuyDate = expectedBuyDate, - participants = participants.map { it.toDto() }, - lastModifiedDate = lastModifiedDate, - createdDate = createdDate - ) + fun toDto() = ShowingDTO( + id = id, + webId = webId, + slug = slug, + date = date ?: throw MissingParametersException("date"), + time = time ?: throw MissingParametersException("time"), + movieId = movie.id, + location = location ?: throw MissingParametersException("location"), + filmstadenScreen = filmstadenScreen, + private = private, + price = price, + ticketsBought = ticketsBought, + admin = admin.id, + payToUser = payToUser.id, + expectedBuyDate = expectedBuyDate, + participants = participants.map { it.toDto() }, + lastModifiedDate = lastModifiedDate, + createdDate = createdDate, + filmstadenRemoteEntityId = filmstadenRemoteEntityId + ) } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Ticket.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Ticket.kt index fd53ed190..96485bc76 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Ticket.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/Ticket.kt @@ -9,23 +9,23 @@ import java.util.* @Document data class Ticket( - @Id - val id: String = "", - val showingId: UUID, - val assignedToUser: UserID, - val profileId: String?, - val barcode: String, - val customerType: String, - val customerTypeDefinition: String, - val cinema: String, - val cinemaCity: String?, - val screen: String, - val seat: Seat, - val date: LocalDate, - val time: LocalTime, - val movieName: String, - val movieRating: String, // 15 år, 11 år etc. - val showAttributes: List // "textad", "en" etc + @Id + val id: String = "", + val showingId: UUID, + val assignedToUser: UserID, + val profileId: String?, + val barcode: String, + val customerType: String, + val customerTypeDefinition: String, + val cinema: String, + val cinemaCity: String?, + val screen: String, + val seat: Seat, + val date: LocalDate, + val time: LocalTime, + val movieName: String, + val movieRating: String, // 15 år, 11 år etc. + val showAttributes: List // "textad", "en" etc ) data class Seat(val row: Int, val number: Int) diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/User.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/User.kt index b6c8e8c80..5e79c57ed 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/User.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/entities/User.kt @@ -4,9 +4,9 @@ import org.springframework.data.annotation.Id import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.mongodb.core.index.Indexed import org.springframework.data.mongodb.core.mapping.Document +import rocks.didit.sefilm.domain.FilmstadenMembershipId import rocks.didit.sefilm.domain.Foretagsbiljett import rocks.didit.sefilm.domain.PhoneNumber -import rocks.didit.sefilm.domain.SfMembershipId import rocks.didit.sefilm.domain.UserID import rocks.didit.sefilm.domain.dto.LimitedUserDTO import rocks.didit.sefilm.notification.NotificationSettings @@ -15,25 +15,25 @@ import java.util.* @Document data class User( - @Id - val id: UserID = UserID("N/A"), - val name: String? = null, - val firstName: String? = null, - val lastName: String? = null, - val nick: String? = null, - val email: String = "", - @Indexed(unique = true, sparse = true) - val sfMembershipId: SfMembershipId? = null, - val phone: PhoneNumber? = null, - val avatar: String? = null, - val foretagsbiljetter: List = emptyList(), - val calendarFeedId: UUID? = UUID.randomUUID(), - val notificationSettings: NotificationSettings = NotificationSettings(false, listOf(), listOf()), - val lastLogin: Instant = Instant.ofEpochSecond(0L), - val signupDate: Instant = Instant.ofEpochSecond(0L), - @LastModifiedDate - val lastModifiedDate: Instant = Instant.ofEpochSecond(0L) + @Id + val id: UserID = UserID("N/A"), + val name: String? = null, + val firstName: String? = null, + val lastName: String? = null, + val nick: String? = null, + val email: String = "", + @Indexed(unique = true, sparse = true) + val filmstadenMembershipId: FilmstadenMembershipId? = null, + val phone: PhoneNumber? = null, + val avatar: String? = null, + val foretagsbiljetter: List = emptyList(), + val calendarFeedId: UUID? = UUID.randomUUID(), + val notificationSettings: NotificationSettings = NotificationSettings(false, listOf(), listOf()), + val lastLogin: Instant = Instant.ofEpochSecond(0L), + val signupDate: Instant = Instant.ofEpochSecond(0L), + @LastModifiedDate + val lastModifiedDate: Instant = Instant.ofEpochSecond(0L) ) { - fun toLimitedUserDTO() = - LimitedUserDTO(this.id, this.name, this.firstName, this.lastName, this.nick, this.phone?.number, this.avatar) + fun toLimitedUserDTO() = + LimitedUserDTO(this.id, this.name, this.firstName, this.lastName, this.nick, this.phone?.number, this.avatar) } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/SfMetaRepository.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/FilmstadenMetaRepository.kt similarity index 55% rename from backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/SfMetaRepository.kt rename to backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/FilmstadenMetaRepository.kt index c34b37a29..8e8e9239d 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/SfMetaRepository.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/FilmstadenMetaRepository.kt @@ -2,7 +2,7 @@ package rocks.didit.sefilm.database.repositories import org.springframework.data.repository.CrudRepository import org.springframework.stereotype.Repository -import rocks.didit.sefilm.database.entities.SfMeta +import rocks.didit.sefilm.database.entities.FilmstadenMeta @Repository -interface SfMetaRepository : CrudRepository \ No newline at end of file +interface FilmstadenMetaRepository : CrudRepository \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/LocationRepository.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/LocationRepository.kt index 0054477a7..0a7182edd 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/LocationRepository.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/LocationRepository.kt @@ -7,5 +7,5 @@ import java.util.* @Repository interface LocationRepository : CrudRepository { - fun findByNameIgnoreCaseOrAliasIgnoreCase(id: String, alias: String): Optional + fun findByNameIgnoreCaseOrAliasIgnoreCase(id: String, alias: String): Optional } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/MovieRepository.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/MovieRepository.kt index f8aaee0ca..914911bd1 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/MovieRepository.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/MovieRepository.kt @@ -7,5 +7,5 @@ import java.util.* @Repository interface MovieRepository : CrudRepository { - fun findByArchivedOrderByPopularityDesc(archived: Boolean = false): List + fun findByArchivedOrderByPopularityDesc(archived: Boolean = false): List } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/ParticipantPaymentInfoRepository.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/ParticipantPaymentInfoRepository.kt index c9315568d..cfd7dab84 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/ParticipantPaymentInfoRepository.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/ParticipantPaymentInfoRepository.kt @@ -8,7 +8,7 @@ import java.util.* @Repository interface ParticipantPaymentInfoRepository : CrudRepository { - fun findByShowingId(showingId: UUID): Collection - fun findByShowingIdAndUserId(showingId: UUID, userId: UserID): Optional - fun deleteByShowingIdAndUserId(showingId: UUID, userId: UserID) + fun findByShowingId(showingId: UUID): Collection + fun findByShowingIdAndUserId(showingId: UUID, userId: UserID): Optional + fun deleteByShowingIdAndUserId(showingId: UUID, userId: UserID) } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/ShowingRepository.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/ShowingRepository.kt index 10752635a..f40bebd28 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/ShowingRepository.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/ShowingRepository.kt @@ -8,7 +8,7 @@ import java.util.* @Repository interface ShowingRepository : CrudRepository { - fun findByPrivateOrderByDateDesc(isPrivate: Boolean): List - fun findByMovieIdOrderByDateDesc(movieId: UUID): List - fun findByWebId(webId: Base64ID): Optional + fun findByPrivateOrderByDateDesc(isPrivate: Boolean): List + fun findByMovieIdOrderByDateDesc(movieId: UUID): List + fun findByWebId(webId: Base64ID): Optional } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/TicketRepository.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/TicketRepository.kt index 4b6eaf524..a619ab066 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/TicketRepository.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/TicketRepository.kt @@ -8,9 +8,9 @@ import java.util.* @Repository interface TicketRepository : CrudRepository { - fun findByShowingIdAndAssignedToUser(showingId: UUID, userId: UserID): List + fun findByShowingIdAndAssignedToUser(showingId: UUID, userId: UserID): List - fun findByShowingId(showingId: UUID): List + fun findByShowingId(showingId: UUID): List - fun findByAssignedToUser(userId: UserID): List + fun findByAssignedToUser(userId: UserID): List } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/UserRepository.kt b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/UserRepository.kt index 6f2e82b17..8375cb850 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/UserRepository.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/database/repositories/UserRepository.kt @@ -3,14 +3,14 @@ package rocks.didit.sefilm.database.repositories import org.springframework.data.repository.CrudRepository import org.springframework.stereotype.Repository import rocks.didit.sefilm.database.entities.User -import rocks.didit.sefilm.domain.SfMembershipId +import rocks.didit.sefilm.domain.FilmstadenMembershipId import rocks.didit.sefilm.domain.TicketNumber import rocks.didit.sefilm.domain.UserID import java.util.* @Repository interface UserRepository : CrudRepository { - fun findBySfMembershipId(sfMembershipId: SfMembershipId): User? - fun findByCalendarFeedId(calendarFeedId: UUID): User? - fun existsByForetagsbiljetterNumber(biljett: TicketNumber): Boolean + fun findByFilmstadenMembershipId(filmstadenMembershipId: FilmstadenMembershipId): User? + fun findByCalendarFeedId(calendarFeedId: UUID): User? + fun existsByForetagsbiljetterNumber(biljett: TicketNumber): Boolean } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/Base64ID.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/Base64ID.kt index 18fa09274..607dd1832 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/Base64ID.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/Base64ID.kt @@ -5,39 +5,39 @@ import java.security.SecureRandom import java.util.* data class Base64ID(val id: String = MISSING_VALUE) { - companion object { - const val STANDARD_LENGTH = 7 - const val MISSING_VALUE = "MISSING-BASE64-ID" + companion object { + const val STANDARD_LENGTH = 7 + const val MISSING_VALUE = "MISSING-BASE64-ID" - val MISSING = Base64ID(MISSING_VALUE) + val MISSING = Base64ID(MISSING_VALUE) - fun random(): Base64ID { - val random = SecureRandom() - val bytes = ByteArray(128) - random.nextBytes(bytes) - val id = Base64.getEncoder() - .withoutPadding() - .encodeToString(bytes) - .substring(0, STANDARD_LENGTH) - .replace('+', randomChar(random)) - .replace('/', randomChar(random)) - return Base64ID(id) - } + fun random(): Base64ID { + val random = SecureRandom() + val bytes = ByteArray(128) + random.nextBytes(bytes) + val id = Base64.getEncoder() + .withoutPadding() + .encodeToString(bytes) + .substring(0, STANDARD_LENGTH) + .replace('+', randomChar(random)) + .replace('/', randomChar(random)) + return Base64ID(id) + } - private fun randomChar(random: SecureRandom): Char { - val value = random.nextInt(26) - return 'a' + value + private fun randomChar(random: SecureRandom): Char { + val value = random.nextInt(26) + return 'a' + value + } } - } - init { - if (id.isBlank()) { - throw IllegalArgumentException("ID may not be blank") + init { + if (id.isBlank()) { + throw IllegalArgumentException("ID may not be blank") + } } - } - @JsonValue - override fun toString(): String { - return id - } + @JsonValue + override fun toString(): String { + return id + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/Error.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/Error.kt index 17d77696a..157a329c2 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/Error.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/Error.kt @@ -10,27 +10,27 @@ import java.net.URI import java.time.Instant data class ErrorDTO( - val error: Boolean = true, - val timestamp: Instant = Instant.now(), - val status_code: Int = 500, - val status_text: String? = null, - val reason: String? = null + val error: Boolean = true, + val timestamp: Instant = Instant.now(), + val status_code: Int = 500, + val status_text: String? = null, + val reason: String? = null ) @Component class ExternalProviderErrorHandler : ResponseErrorHandler { - private val log = LoggerFactory.getLogger(ExternalProviderErrorHandler::class.java) - override fun hasError(response: ClientHttpResponse): Boolean { - return response.rawStatusCode >= 401 - } + private val log = LoggerFactory.getLogger(ExternalProviderErrorHandler::class.java) + override fun hasError(response: ClientHttpResponse): Boolean { + return response.rawStatusCode >= 401 + } - override fun handleError(url: URI, method: HttpMethod, response: ClientHttpResponse) { - log.warn("$method $url -> ${response.statusCode}: ${response.statusText}") - throw ExternalProviderException("${response.statusCode}: ${response.statusText}") - } + override fun handleError(url: URI, method: HttpMethod, response: ClientHttpResponse) { + log.warn("$method $url -> ${response.statusCode}: ${response.statusText}") + throw ExternalProviderException("${response.statusCode}: ${response.statusText}") + } - override fun handleError(response: ClientHttpResponse) { - log.warn("Error when fetching info from external provider: ${response.statusCode}: ${response.statusText}") - throw ExternalProviderException("${response.statusCode}: ${response.statusText}") - } + override fun handleError(response: ClientHttpResponse) { + log.warn("Error when fetching info from external provider: ${response.statusCode}: ${response.statusText}") + throw ExternalProviderException("${response.statusCode}: ${response.statusText}") + } } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/ExternalProviderId.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/ExternalProviderId.kt index d1e769f54..14b8963eb 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/ExternalProviderId.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/ExternalProviderId.kt @@ -3,128 +3,128 @@ package rocks.didit.sefilm.domain import com.fasterxml.jackson.annotation.JsonValue enum class ValueState { - Missing, - Unknown, - Supplied + Missing, + Unknown, + Supplied } sealed class ExternalProviderId(val state: ValueState) { - fun isMissing() = state == ValueState.Missing + fun isMissing() = state == ValueState.Missing - fun isUnknown() = state == ValueState.Unknown + fun isUnknown() = state == ValueState.Unknown - fun isSupplied() = state == ValueState.Supplied + fun isSupplied() = state == ValueState.Supplied - /** The ID is either Unknown or Missing */ - fun isNotSupplied() = !isSupplied() + /** The ID is either Unknown or Missing */ + fun isNotSupplied() = !isSupplied() - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other?.javaClass != javaClass) return false + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false - other as ExternalProviderId + other as ExternalProviderId - if (state != other.state) return false + if (state != other.state) return false - return true - } + return true + } - override fun hashCode(): Int { - return state.hashCode() - } + override fun hashCode(): Int { + return state.hashCode() + } - override fun toString(): String { - return "ExternalProviderId(state=$state)" - } + override fun toString(): String { + return "ExternalProviderId(state=$state)" + } } class IMDbID( - @JsonValue - val value: String? = null, - state: ValueState = ValueState.Missing + @JsonValue + val value: String? = null, + state: ValueState = ValueState.Missing ) : ExternalProviderId(state) { - companion object { - fun valueOf(imdbId: String) = IMDbID(imdbId, ValueState.Supplied) + companion object { + fun valueOf(imdbId: String) = IMDbID(imdbId, ValueState.Supplied) - /** Missing value but may be updated in the future */ - val MISSING = IMDbID() + /** Missing value but may be updated in the future */ + val MISSING = IMDbID() - /** We've searched for a value, but none were found */ - val UNKNOWN = IMDbID(null, ValueState.Unknown) - } + /** We've searched for a value, but none were found */ + val UNKNOWN = IMDbID(null, ValueState.Unknown) + } - init { - if (value != null && !value.matches(Regex("^tt[0-9]{7}"))) { - throw IllegalArgumentException("Illegal IMDb ID format: $value") + init { + if (value != null && !value.matches(Regex("^tt[0-9]{7}"))) { + throw IllegalArgumentException("Illegal IMDb ID format: $value") + } } - } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other?.javaClass != javaClass) return false - if (!super.equals(other)) return false + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + if (!super.equals(other)) return false - other as IMDbID + other as IMDbID - if (value != other.value) return false + if (value != other.value) return false - return true - } + return true + } - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + (value?.hashCode() ?: 0) - return result - } + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (value?.hashCode() ?: 0) + return result + } - override fun toString(): String { - return "IMDbID(value=$value, state=$state)" - } + override fun toString(): String { + return "IMDbID(value=$value, state=$state)" + } } class TMDbID( - @JsonValue - val value: Long? = null, - state: ValueState = ValueState.Missing + @JsonValue + val value: Long? = null, + state: ValueState = ValueState.Missing ) : ExternalProviderId(state) { - companion object { - fun valueOf(tmdbId: Long) = TMDbID(tmdbId, ValueState.Supplied) + companion object { + fun valueOf(tmdbId: Long) = TMDbID(tmdbId, ValueState.Supplied) - /** Missing value but may be updated in the future */ - val MISSING = TMDbID() + /** Missing value but may be updated in the future */ + val MISSING = TMDbID() - /** We've searched for a value, but none were found */ - val UNKNOWN = TMDbID(null, ValueState.Unknown) - } + /** We've searched for a value, but none were found */ + val UNKNOWN = TMDbID(null, ValueState.Unknown) + } - init { - if (value != null && value <= 0) { - throw IllegalArgumentException("Illegal TMDb ID. Got: $value") + init { + if (value != null && value <= 0) { + throw IllegalArgumentException("Illegal TMDb ID. Got: $value") + } } - } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other?.javaClass != javaClass) return false - if (!super.equals(other)) return false + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + if (!super.equals(other)) return false - other as TMDbID + other as TMDbID - if (value != other.value) return false + if (value != other.value) return false - return true - } + return true + } - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + (value?.hashCode() ?: 0) - return result - } + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (value?.hashCode() ?: 0) + return result + } - override fun toString(): String { - return "TMDbID(value=$value, State=$state)" - } + override fun toString(): String { + return "TMDbID(value=$value, State=$state)" + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/FilmstadenMembershipId.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/FilmstadenMembershipId.kt new file mode 100644 index 000000000..740dbab08 --- /dev/null +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/FilmstadenMembershipId.kt @@ -0,0 +1,33 @@ +package rocks.didit.sefilm.domain + +import com.fasterxml.jackson.annotation.JsonValue + +data class FilmstadenMembershipId(val value: String) { + companion object { + fun valueOf(profileId: String): FilmstadenMembershipId { + if (profileId[3] == '-' && profileId.length == 7) { + return FilmstadenMembershipId(profileId.toUpperCase()) + } + if (profileId.length != 6) { + throw IllegalArgumentException("$profileId does not look like a valid profileId as supplied by Filmstaden") + } + val split = profileId.split('-', limit = 2) + if (split.size == 2 && (split[0].length != 3 || split[1].length != 3)) { + throw IllegalArgumentException("'$profileId' is an invalid membership id. Expected XXX-XXX") + } + + return FilmstadenMembershipId("${profileId.substring(0, 3)}-${profileId.substring(3, profileId.length)}".toUpperCase()) + } + } + + init { + if (value.length < 6 || value.length > 7) { + throw IllegalArgumentException("The Filmstaden membership id has wrong size. Expected 6-7, got ${value.length}") + } + } + + @JsonValue + override fun toString(): String { + return value + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/Foretagsbiljett.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/Foretagsbiljett.kt index b0c761278..93c36adf8 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/Foretagsbiljett.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/Foretagsbiljett.kt @@ -5,28 +5,28 @@ import rocks.didit.sefilm.domain.dto.ForetagsbiljettDTO import java.time.LocalDate data class TicketNumber(@JsonValue val number: String) { - init { - if (number.length != 11) { - throw IllegalArgumentException("Ticket number has wrong size. Expected 11, got ${number.length}") + init { + if (number.length != 11) { + throw IllegalArgumentException("Ticket number has wrong size. Expected 11, got ${number.length}") + } + if (number.toLongOrNull() == null) { + throw IllegalArgumentException("'$number' is an invalid ticket number") + } } - if (number.toLongOrNull() == null) { - throw IllegalArgumentException("'$number' is an invalid ticket number") - } - } - override fun toString() = number + override fun toString() = number } data class Foretagsbiljett( - val number: TicketNumber, - val expires: LocalDate = LocalDate.now().plusYears(1) + val number: TicketNumber, + val expires: LocalDate = LocalDate.now().plusYears(1) ) { - companion object { - fun valueOf(dto: ForetagsbiljettDTO) = Foretagsbiljett(number = TicketNumber(dto.number), expires = dto.expires) - } + companion object { + fun valueOf(dto: ForetagsbiljettDTO) = Foretagsbiljett(number = TicketNumber(dto.number), expires = dto.expires) + } - enum class Status { - Available, Pending, Used, Expired - } + enum class Status { + Available, Pending, Used, Expired + } } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/Participant.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/Participant.kt index a7303be40..d01d417f4 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/Participant.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/Participant.kt @@ -3,22 +3,22 @@ package rocks.didit.sefilm.domain data class ParticipantDTO(val userId: UserID, val paymentType: PaymentType) sealed class Participant { - abstract val userId: UserID + abstract val userId: UserID - fun toDto(): ParticipantDTO { - val paymentType = when (this) { - is SwishParticipant -> PaymentType.Swish - is FtgBiljettParticipant -> PaymentType.Foretagsbiljett + fun toDto(): ParticipantDTO { + val paymentType = when (this) { + is SwishParticipant -> PaymentType.Swish + is FtgBiljettParticipant -> PaymentType.Foretagsbiljett + } + return ParticipantDTO(this.userId, paymentType) } - return ParticipantDTO(this.userId, paymentType) - } } data class SwishParticipant( - override val userId: UserID + override val userId: UserID ) : Participant() data class FtgBiljettParticipant( - override val userId: UserID, - val ticketNumber: TicketNumber + override val userId: UserID, + val ticketNumber: TicketNumber ) : Participant() diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/PaymentOption.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/PaymentOption.kt index 2596ce3b7..74079d794 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/PaymentOption.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/PaymentOption.kt @@ -1,7 +1,7 @@ package rocks.didit.sefilm.domain enum class PaymentType { - Swish, Foretagsbiljett + Swish, Foretagsbiljett } data class PaymentOption(val type: PaymentType, val ticketNumber: String? = null) diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/PhoneNumber.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/PhoneNumber.kt index 24773b03b..e494513a9 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/PhoneNumber.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/PhoneNumber.kt @@ -7,33 +7,33 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType.MOBILE import com.google.i18n.phonenumbers.Phonenumber data class PhoneNumber(var number: String = "") { - companion object { - private fun parsePhoneNumber(number: String): String { - val phoneUtil = PhoneNumberUtil.getInstance() - val parsedPhoneNumber: Phonenumber.PhoneNumber - try { - parsedPhoneNumber = phoneUtil.parse(number, "SE") - } catch (e: NumberParseException) { - throw IllegalArgumentException("'$number' is not a valid Swedish phone number") - } + companion object { + private fun parsePhoneNumber(number: String): String { + val phoneUtil = PhoneNumberUtil.getInstance() + val parsedPhoneNumber: Phonenumber.PhoneNumber + try { + parsedPhoneNumber = phoneUtil.parse(number, "SE") + } catch (e: NumberParseException) { + throw IllegalArgumentException("'$number' is not a valid Swedish phone number") + } - val numberType = phoneUtil.getNumberType(parsedPhoneNumber) - if (numberType != MOBILE) { - throw IllegalArgumentException("'$number' is not a mobile number. Got: $numberType type") - } + val numberType = phoneUtil.getNumberType(parsedPhoneNumber) + if (numberType != MOBILE) { + throw IllegalArgumentException("'$number' is not a mobile number. Got: $numberType type") + } - return phoneUtil.format(parsedPhoneNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL) + return phoneUtil.format(parsedPhoneNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL) + } } - } - init { - if (this.number.isNotBlank()) { - this.number = parsePhoneNumber(this.number) + init { + if (this.number.isNotBlank()) { + this.number = parsePhoneNumber(this.number) + } } - } - @JsonValue - override fun toString(): String { - return number - } + @JsonValue + override fun toString(): String { + return number + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/SEK.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/SEK.kt index 0695247a2..3b534762e 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/SEK.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/SEK.kt @@ -4,15 +4,15 @@ import com.fasterxml.jackson.annotation.JsonValue /** Swedish Crowns, represented by ören */ data class SEK(val ören: Long) { - init { - if (ören < 0) { - throw IllegalArgumentException("You cannot have negative money") + init { + if (ören < 0) { + throw IllegalArgumentException("You cannot have negative money") + } } - } - /** Rounds to nearest integer */ - fun toKronor(): Long = ören / 100 + /** Rounds to nearest integer */ + fun toKronor(): Long = ören / 100 - @JsonValue - fun toÖren(): Long = ören + @JsonValue + fun toÖren(): Long = ören } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/SfMembershipId.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/SfMembershipId.kt deleted file mode 100644 index 3f983e7f0..000000000 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/SfMembershipId.kt +++ /dev/null @@ -1,33 +0,0 @@ -package rocks.didit.sefilm.domain - -import com.fasterxml.jackson.annotation.JsonValue - -data class SfMembershipId(val value: String) { - companion object { - fun valueOf(profileId: String): SfMembershipId { - if (profileId[3] == '-' && profileId.length == 7) { - return SfMembershipId(profileId.toUpperCase()) - } - if (profileId.length != 6) { - throw IllegalArgumentException("$profileId does not look like a valid profileId as supplied by SF") - } - val split = profileId.split('-', limit = 2) - if (split.size == 2 && (split[0].length != 3 || split[1].length != 3)) { - throw IllegalArgumentException("'$profileId' is an invalid membership id. Expected XXX-XXX") - } - - return SfMembershipId("${profileId.substring(0, 3)}-${profileId.substring(3, profileId.length)}".toUpperCase()) - } - } - - init { - if (value.length < 6 || value.length > 7) { - throw IllegalArgumentException("The SF membership id has wrong size. Expected 6-7, got ${value.length}") - } - } - - @JsonValue - override fun toString(): String { - return value - } -} \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/Swish.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/Swish.kt index 171d0817b..d477f0237 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/Swish.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/Swish.kt @@ -5,25 +5,25 @@ import com.google.common.net.UrlEscapers import java.net.URI data class StringValue( - val value: String, - val editable: Boolean = false + val value: String, + val editable: Boolean = false ) data class IntValue( - val value: Long, - val editable: Boolean = false + val value: Long, + val editable: Boolean = false ) data class SwishDataDTO( - val version: Int = 1, - val payee: StringValue, - val amount: IntValue, - val message: StringValue + val version: Int = 1, + val payee: StringValue, + val amount: IntValue, + val message: StringValue ) { - fun generateUri(): URI { - val asString = jacksonObjectMapper().writeValueAsString(this) - val encodedData = UrlEscapers.urlFragmentEscaper().escape(asString) - return URI.create("swish://payment?data=$encodedData") - } + fun generateUri(): URI { + val asString = jacksonObjectMapper().writeValueAsString(this) + val encodedData = UrlEscapers.urlFragmentEscaper().escape(asString) + return URI.create("swish://payment?data=$encodedData") + } } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/UserID.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/UserID.kt index 4dba28666..440a4e758 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/UserID.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/UserID.kt @@ -3,12 +3,12 @@ package rocks.didit.sefilm.domain import com.fasterxml.jackson.annotation.JsonValue data class UserID(val id: String = "N/A") { - init { - if (id.isBlank()) { - throw IllegalArgumentException("Id may not be blank") + init { + if (id.isBlank()) { + throw IllegalArgumentException("Id may not be blank") + } } - } - @JsonValue - override fun toString() = id + @JsonValue + override fun toString() = id } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/AdminPaymentDetailsDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/AdminPaymentDetailsDTO.kt index c94a9d298..c88429010 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/AdminPaymentDetailsDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/AdminPaymentDetailsDTO.kt @@ -1,14 +1,14 @@ package rocks.didit.sefilm.domain.dto import rocks.didit.sefilm.database.entities.ParticipantPaymentInfo -import rocks.didit.sefilm.domain.SfMembershipId +import rocks.didit.sefilm.domain.FilmstadenMembershipId import rocks.didit.sefilm.domain.TicketNumber import rocks.didit.sefilm.domain.UserID data class AdminPaymentDetailsDTO( - val sfBuyLink: String? = null, - val sfData: List, - val participantPaymentInfos: Collection = emptyList() + val filmstadenBuyLink: String? = null, + val filmstadenData: List, + val participantPaymentInfos: Collection = emptyList() ) -data class UserAndSfData(val userId: UserID, val sfMembershipId: SfMembershipId?, val foretagsbiljett: TicketNumber?) \ No newline at end of file +data class UserAndFilmstadenData(val userId: UserID, val filmstadenMembershipId: FilmstadenMembershipId?, val foretagsbiljett: TicketNumber?) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/AttendeePaymentDetailsDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/AttendeePaymentDetailsDTO.kt index b0773885d..c05b3f2ce 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/AttendeePaymentDetailsDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/AttendeePaymentDetailsDTO.kt @@ -4,10 +4,10 @@ import rocks.didit.sefilm.domain.SEK import rocks.didit.sefilm.domain.UserID data class AttendeePaymentDetailsDTO( - val hasPaid: Boolean = false, - val amountOwed: SEK = SEK(0), - val payTo: UserID = UserID(), - val swishLink: String? = null, - val payToPhoneNumber: String = "", - val payerUserID: UserID = UserID() + val hasPaid: Boolean = false, + val amountOwed: SEK = SEK(0), + val payTo: UserID = UserID(), + val swishLink: String? = null, + val payToPhoneNumber: String = "", + val payerUserID: UserID = UserID() ) diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/CalendarEventDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/CalendarEventDTO.kt index 138684596..2142e1359 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/CalendarEventDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/CalendarEventDTO.kt @@ -5,30 +5,30 @@ import java.io.Serializable import java.time.ZonedDateTime data class CalendarEventDTO( - val summary: String, - val location: String? = null, - val attendees: List>, - val start: Map, - val end: Map, - val source: Map, - val description: String + val summary: String, + val location: String? = null, + val attendees: List>, + val start: Map, + val end: Map, + val source: Map, + val description: String ) { - companion object Factory { - fun of( - summary: String, location: Location?, emails: Collection, start: ZonedDateTime, end: ZonedDateTime, - sfBuyLink: String = "https://www.sf.se", description: String = "$summary @ ${location?.name}\n$sfBuyLink" - ): CalendarEventDTO { + companion object Factory { + fun of( + summary: String, location: Location?, emails: Collection, start: ZonedDateTime, end: ZonedDateTime, + filmstadenBuyLink: String = "https://www.filmstaden.se", description: String = "$summary @ ${location?.name}\n$filmstadenBuyLink" + ): CalendarEventDTO { - return CalendarEventDTO( - summary = summary, - location = location?.name ?: "N/A", - attendees = emails.map { mapOf("email" to it) }, - start = mapOf("dateTime" to start.toOffsetDateTime().toString()), - end = mapOf("dateTime" to end.toOffsetDateTime().toString()), - source = mapOf("title" to "SF Bio", "url" to sfBuyLink), - description = description - ) + return CalendarEventDTO( + summary = summary, + location = location?.name ?: "N/A", + attendees = emails.map { mapOf("email" to it) }, + start = mapOf("dateTime" to start.toOffsetDateTime().toString()), + end = mapOf("dateTime" to end.toOffsetDateTime().toString()), + source = mapOf("title" to "Filmstaden Bio", "url" to filmstadenBuyLink), + description = description + ) + } } - } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/CreateShowingDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/CreateShowingDTO.kt index 3b65a1392..4373ef7b8 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/CreateShowingDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/CreateShowingDTO.kt @@ -5,10 +5,11 @@ import java.time.LocalTime import java.util.* data class CreateShowingDTO( - val date: LocalDate?, - val time: LocalTime?, - val movieId: UUID?, - val location: String?, - val sfScreen: SfLiteScreenDTO?, - val expectedBuyDate: LocalDate? + val date: LocalDate, + val time: LocalTime, + val movieId: UUID, + val location: String, + val filmstadenScreen: FilmstadenLiteScreenDTO?, + val expectedBuyDate: LocalDate?, + val filmstadenRemoteEntityId: String? ) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ExternalMovieIdDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ExternalMovieIdDTO.kt index 805aa04bb..f33474ce9 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ExternalMovieIdDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ExternalMovieIdDTO.kt @@ -1,7 +1,7 @@ package rocks.didit.sefilm.domain.dto data class ExternalMovieIdDTO( - val imdb: String? = null, - val tmdb: Long? = null, - val sf: String? = null + val imdb: String? = null, + val tmdb: Long? = null, + val filmstaden: String? = null ) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/FilmstadenCityDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/FilmstadenCityDTO.kt new file mode 100644 index 000000000..4e5808f26 --- /dev/null +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/FilmstadenCityDTO.kt @@ -0,0 +1,3 @@ +package rocks.didit.sefilm.domain.dto + +data class FilmstadenCityAliasDTO(val name: String, val alias: String) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/FilmstadenDTOs.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/FilmstadenDTOs.kt new file mode 100644 index 000000000..198609b8f --- /dev/null +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/FilmstadenDTOs.kt @@ -0,0 +1,187 @@ +package rocks.didit.sefilm.domain.dto + +import org.slf4j.LoggerFactory +import java.time.Instant +import java.time.LocalDate +import java.time.ZonedDateTime + +data class FilmstadenRatingDTO( + val age: Int, + val ageAccompanied: Int, + val alias: String, + val displayName: String +) + +data class FilmstadenMovieDTO( + val ncgId: String, + val title: String, + val releaseDate: LocalDate, + val rating: FilmstadenRatingDTO, + val posterUrl: String?, + val slug: String, + val length: Int?, + val genres: List = listOf() +) + +data class FilmstadenOriginalLanguageDTO( + val alias: String, + val englishName: String, + val nativeName: String, + val displayName: String +) + +data class FilmstadenPersonDTO(val displayName: String?, val firstName: String?, val lastName: String?) + +data class FilmstadenGenreDTO(val name: String) + +data class FilmstadenExtendedMovieDTO( + val ncgId: String?, + val languageId: String?, + val originalLanguage: String?, + val originalLanguages: Collection?, + val productionYear: Int?, + val producers: Collection?, + val genres: Collection?, + val title: String?, + val originalTitle: String?, + val shortDescription: String?, + val longDescription: String?, + val releaseDate: LocalDate?, + val actors: Collection?, + val directors: Collection?, + val rating: FilmstadenRatingDTO?, + val length: Long?, + val posterUrl: String?, + val slug: String? +) + +data class FilmstadenAttributeDTO(val alias: String, val displayName: String) + +data class FilmstadenScreenDTO( + val ncgId: String, + val title: String, + val slug: String, + val seatCount: Int, + val remoteSystemAlias: String, + val remoteEntityId: String +) +fun FilmstadenScreenDTO.toFilmstadenLiteScreen() = FilmstadenLiteScreenDTO(this.ncgId, this.title) + +data class FilmstadenShowItemsDTO(val totalNbrOfItems: Int, val items: List) +data class FilmstadenLocationItemsDTO(val totalNbrOfItems: Int, val items: List) +data class FilmstadenMovieItemsDTO(val totalNbrOfItems: Int, val items: List) + +data class FilmstadenShowDTO( + val remoteSystemAlias: String, + val remoteEntityId: String, + val unnumbered: Boolean, + val time: ZonedDateTime, + val timeUtc: Instant, + val movie: FilmstadenMovieDTO, + val movieVersion: Map, + val attributes: List, + val cinema: FilmstadenCinemaWithAddressDTO, + val screen: FilmstadenScreenDTO +) + +data class FilmstadenCinemaWithAddressDTO( + val ncgId: String?, + val title: String, + val address: FilmstadenAddressDTO, + val slug: String, + val remoteSystemAlias: String, + val remoteEntityId: String +) + +data class FilmstadenAddressDTO( + val phoneNumber: String?, + val streetAddress: String, + val postalCode: String, + val postalAddress: String, + val city: Map, + val country: Map, + val coordinates: FilmstadenCoordinatesDTO +) + +data class FilmstadenCoordinatesDTO(val latitude: String, val longitude: String) + +data class FilmstadenLiteScreenDTO(val filmstadenId: String, val name: String) +data class FilmstadenShowingDTO( + val cinemaName: String, + val screen: FilmstadenLiteScreenDTO, + val seatCount: Int, + val timeUtc: Instant, + val tags: List, + val filmstadenRemoteEntityId: String +) { + companion object { + fun from(show: FilmstadenShowDTO) = FilmstadenShowingDTO( + show.cinema.title, + FilmstadenLiteScreenDTO(show.screen.ncgId, show.screen.title), + show.screen.seatCount, + show.timeUtc, + FilmstadenTag.convertTags(show.attributes), + show.remoteEntityId + ) + } +} + +data class FilmstadenSeatMapDTO( + val remoteSystemAlias: String, + val remoteEntityId: String, + val row: Int, + val number: Int, + val seatType: String, + val coordinates: FilmstadenSeatCoordinates, + val dimensions: FilmstadenSeatDimensions, + val languageId: String +) + +data class FilmstadenSeatCoordinates( + val x: Float, + val y: Float +) + +data class FilmstadenSeatDimensions( + val width: Int, + val height: Int +) + +enum class FilmstadenTag { + `18år`, + `3D`, + VIP, + TXT, + EN, + JA, + SV, + `Sommar på bio`, + `Utmärkt film`, + `Syntolkning`, + `Uppläst text`, + Klassiker, + Classic, + `Ej textad`, + Textad, + Familj, + Barnvagnsbio, + Knattebio, + Unknown; + + companion object { + private val log = LoggerFactory.getLogger(FilmstadenTag::class.java) + + fun convertTags(attributes: List): List { + return attributes.map { a -> + try { + FilmstadenTag.valueOf(a.displayName) + } catch (e: IllegalArgumentException) { + log.warn("FilmstadenTag with name $a does not exist") + return@map null + } + }.filterNotNull() + } + } +} + + diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/FilmstadenTicketsDTOs.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/FilmstadenTicketsDTOs.kt new file mode 100644 index 000000000..db7cb2282 --- /dev/null +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/FilmstadenTicketsDTOs.kt @@ -0,0 +1,41 @@ +package rocks.didit.sefilm.domain.dto + +import java.time.LocalDate +import java.time.LocalTime + +data class FilmstadenTicketDTO( + val id: String, + val profileId: String?, + val customerTypeDefinition: String, + val customerType: String, + val qrCode: String, + val toiletCode: String, + val isRefunded: Boolean, + val cinema: FilmstadenCinemaDTO, + val movie: FilmstadenNamedMovieDTO, + val show: FilmstadenTicketShowDTO, + val seat: FilmstadenSeatDTO, + val screen: FilmstadenTicketScreenDTO, + val receipt: Map, + val labels: Map, + val products: List> +) + +data class FilmstadenCinemaDTO(val city: FilmstadenCityDTO, val title: String, val company: Map) + +data class FilmstadenCityDTO(val name: String?) + +data class FilmstadenTicketScreenDTO(val title: String) + +data class FilmstadenNamedMovieDTO(val title: String, val rating: FilmstadenDisplayNameDTO) + +data class FilmstadenDisplayNameDTO(val displayName: String) + +data class FilmstadenSeatDTO(val number: Int, val row: Int, val section: String, val type: String, val unnumberedText: String?) + +data class FilmstadenTicketShowDTO( + val attributes: List, + val date: LocalDate, + val time: LocalTime, + val unnumbered: Boolean +) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ImdbDTOs.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ImdbDTOs.kt index c6d5d1264..66cf3e0a7 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ImdbDTOs.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ImdbDTOs.kt @@ -6,9 +6,9 @@ package rocks.didit.sefilm.domain.dto * @param d List of movie/shows/people results actually found */ data class ImdbSearchResults( - val v: Int, - val q: String, - val d: List? + val v: Int, + val q: String, + val d: List? ) /** @@ -20,11 +20,11 @@ data class ImdbSearchResults( * @param i Image url, width, and height */ data class ImdbResult( - val l: String, - val id: String, - val s: String?, - val y: Int?, - val q: String?, - val i: List? + val l: String, + val id: String, + val s: String?, + val y: Int?, + val q: String?, + val i: List? ) diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/LimitedUserDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/LimitedUserDTO.kt index 0f271316c..4d8997031 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/LimitedUserDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/LimitedUserDTO.kt @@ -3,12 +3,12 @@ package rocks.didit.sefilm.domain.dto import rocks.didit.sefilm.domain.UserID data class LimitedUserDTO( - val id: UserID = UserID("N/A"), - val name: String? = null, - val firstName: String? = null, - val lastName: String? = null, - val nick: String? = null, - val phone: String? = null, - val avatar: String? = null + val id: UserID = UserID("N/A"), + val name: String? = null, + val firstName: String? = null, + val lastName: String? = null, + val nick: String? = null, + val phone: String? = null, + val avatar: String? = null ) diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/NotificationSettingsInputDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/NotificationSettingsInputDTO.kt index ed7022603..91bbc39e4 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/NotificationSettingsInputDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/NotificationSettingsInputDTO.kt @@ -3,10 +3,10 @@ package rocks.didit.sefilm.domain.dto import rocks.didit.sefilm.notification.NotificationType data class NotificationSettingsInputDTO( - val notificationsEnabled: Boolean, - val enabledTypes: List = listOf(), - val pushover: PushoverInputDTO?, - val mail: MailInputDTO? + val notificationsEnabled: Boolean, + val enabledTypes: List = listOf(), + val pushover: PushoverInputDTO?, + val mail: MailInputDTO? ) data class PushoverInputDTO(val enabled: Boolean, val userKey: String, val device: String?) diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ParticipantPaymentInfoDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ParticipantPaymentInfoDTO.kt index 527857437..ea4c91100 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ParticipantPaymentInfoDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ParticipantPaymentInfoDTO.kt @@ -3,10 +3,10 @@ package rocks.didit.sefilm.domain.dto import java.util.* data class ParticipantPaymentInfoDTO( - val id: UUID = UUID.randomUUID(), - val userId: String? = null, - val showingId: UUID? = null, - val hasPaid: Boolean = false, - val amountOwed: Long = 0 + val id: UUID = UUID.randomUUID(), + val userId: String? = null, + val showingId: UUID? = null, + val hasPaid: Boolean = false, + val amountOwed: Long = 0 ) diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ResponseStatusDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ResponseStatusDTO.kt index 57b75e119..1be054f12 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ResponseStatusDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ResponseStatusDTO.kt @@ -1,8 +1,8 @@ package rocks.didit.sefilm.domain.dto sealed class ResponseStatusDTO(val success: Boolean) { - class SuccessfulStatusDTO(val message: String = "") : ResponseStatusDTO(true) - class FailStatusDTO(val message: String = "") : ResponseStatusDTO(false) + class SuccessfulStatusDTO(val message: String = "") : ResponseStatusDTO(true) + class FailStatusDTO(val message: String = "") : ResponseStatusDTO(false) } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SavedEntitiesDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SavedEntitiesDTO.kt index 86440c5b5..90afc1e5d 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SavedEntitiesDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SavedEntitiesDTO.kt @@ -1,6 +1,6 @@ package rocks.didit.sefilm.domain.dto data class SavedEntitiesDTO( - val count: Int = 0, - val message: String + val count: Int = 0, + val message: String ) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ScreenDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ScreenDTO.kt index b8d1887cb..ac0556389 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ScreenDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ScreenDTO.kt @@ -3,8 +3,8 @@ package rocks.didit.sefilm.domain.dto import java.time.LocalTime data class ScreenDTO( - val localTime: LocalTime, - val screen: String, - val cinema: String, - val tags: List + val localTime: LocalTime, + val screen: String, + val cinema: String, + val tags: List ) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SfCityDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SfCityDTO.kt deleted file mode 100644 index 71823223b..000000000 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SfCityDTO.kt +++ /dev/null @@ -1,3 +0,0 @@ -package rocks.didit.sefilm.domain.dto - -data class SfCityAliasDTO(val name: String, val alias: String) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SfDTOs.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SfDTOs.kt deleted file mode 100644 index d1be460bc..000000000 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SfDTOs.kt +++ /dev/null @@ -1,184 +0,0 @@ -package rocks.didit.sefilm.domain.dto - -import org.slf4j.LoggerFactory -import java.time.Instant -import java.time.LocalDate -import java.time.ZonedDateTime - -data class SfRatingDTO( - val age: Int, - val ageAccompanied: Int, - val alias: String, - val displayName: String -) - -data class SfMovieDTO( - val ncgId: String, - val title: String, - val releaseDate: LocalDate, - val rating: SfRatingDTO, - val posterUrl: String?, - val slug: String, - val length: Int?, - val genres: List = listOf() -) - -data class SfOriginalLanguageDTO( - val alias: String, - val englishName: String, - val nativeName: String, - val displayName: String -) - -data class SfPersonDTO(val displayName: String?, val firstName: String?, val lastName: String?) - -data class SfGenreDTO(val name: String) - -data class SfExtendedMovieDTO( - val ncgId: String?, - val languageId: String?, - val originalLanguage: String?, - val originalLanguages: Collection?, - val productionYear: Int?, - val producers: Collection?, - val genres: Collection?, - val title: String?, - val originalTitle: String?, - val shortDescription: String?, - val longDescription: String?, - val releaseDate: LocalDate?, - val actors: Collection?, - val directors: Collection?, - val rating: SfRatingDTO?, - val length: Long?, - val posterUrl: String?, - val slug: String? -) - -data class SfAttributeDTO(val alias: String, val displayName: String) - -data class SfScreenDTO( - val ncgId: String, - val title: String, - val slug: String, - val seatCount: Int, - val remoteSystemAlias: String, - val remoteEntityId: String -) - -data class SfShowItemsDTO(val totalNbrOfItems: Int, val items: List) -data class SfLocationItemsDTO(val totalNbrOfItems: Int, val items: List) -data class SfMovieItemsDTO(val totalNbrOfItems: Int, val items: List) - -data class SfShowDTO( - val remoteSystemAlias: String, - val remoteEntityId: String, - val unnumbered: Boolean, - val time: ZonedDateTime, - val timeUtc: Instant, - val movie: SfMovieDTO, - val movieVersion: Map, - val attributes: List, - val cinema: SfCinemaWithAddressDTO, - val screen: SfScreenDTO -) - -data class SfCinemaWithAddressDTO( - val ncgId: String?, - val title: String, - val address: SfAddressDTO, - val slug: String, - val remoteSystemAlias: String, - val remoteEntityId: String -) - -data class SfAddressDTO( - val phoneNumber: String?, - val streetAddress: String, - val postalCode: String, - val postalAddress: String, - val city: Map, - val country: Map, - val coordinates: SfCoordinatesDTO -) - -data class SfCoordinatesDTO(val latitude: String, val longitude: String) - -data class SfLiteScreenDTO(val sfId: String, val name: String) -data class SfShowingDTO( - val cinemaName: String, - val screen: SfLiteScreenDTO, - val seatCount: Int, - val timeUtc: Instant, - val tags: List -) { - companion object { - fun from(show: SfShowDTO) = SfShowingDTO( - show.cinema.title, - SfLiteScreenDTO(show.screen.ncgId, show.screen.title), - show.screen.seatCount, - show.timeUtc, - SfTag.convertTags(show.attributes) - ) - } -} - -data class SfSeatMapDTO( - val remoteSystemAlias: String, - val remoteEntityId: String, - val row: Int, - val number: Int, - val seatType: String, - val coordinates: SfSeatCoordinates, - val dimensions: SfSeatDimensions, - val languageId: String -) - -data class SfSeatCoordinates( - val x: Float, - val y: Float -) - -data class SfSeatDimensions( - val width: Int, - val height: Int -) - -enum class SfTag { - `18år`, - `3D`, - VIP, - TXT, - EN, - JA, - SV, - `Sommar på bio`, - `Utmärkt film`, - `Syntolkning`, - `Uppläst text`, - Klassiker, - Classic, - `Ej textad`, - Textad, - Familj, - Barnvagnsbio, - Knattebio, - Unknown; - - companion object { - private val log = LoggerFactory.getLogger(SfTag::class.java) - - fun convertTags(attributes: List): List { - return attributes.map { a -> - try { - SfTag.valueOf(a.displayName) - } catch (e: IllegalArgumentException) { - log.warn("SfTag with name $a does not exist") - return@map null - } - }.filterNotNull() - } - } -} - - diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SfTicketsDTOs.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SfTicketsDTOs.kt deleted file mode 100644 index 3a58e3601..000000000 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/SfTicketsDTOs.kt +++ /dev/null @@ -1,41 +0,0 @@ -package rocks.didit.sefilm.domain.dto - -import java.time.LocalDate -import java.time.LocalTime - -data class SfTicketDTO( - val id: String, - val profileId: String?, - val customerTypeDefinition: String, - val customerType: String, - val qrCode: String, - val toiletCode: String, - val isRefunded: Boolean, - val cinema: SfCinemaDTO, - val movie: SfNamedMovieDTO, - val show: SfTicketShowDTO, - val seat: SfSeatDTO, - val screen: SfTicketScreenDTO, - val receipt: Map, - val labels: Map, - val products: List> -) - -data class SfCinemaDTO(val city: SfCityDTO, val title: String, val company: Map) - -data class SfCityDTO(val name: String?) - -data class SfTicketScreenDTO(val title: String) - -data class SfNamedMovieDTO(val title: String, val rating: SfDisplayNameDTO) - -data class SfDisplayNameDTO(val displayName: String) - -data class SfSeatDTO(val number: Int, val row: Int, val section: String, val type: String, val unnumberedText: String?) - -data class SfTicketShowDTO( - val attributes: List, - val date: LocalDate, - val time: LocalTime, - val unnumbered: Boolean -) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ShowingDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ShowingDTO.kt index c43d4d76d..b160ba06f 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ShowingDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/ShowingDTO.kt @@ -12,25 +12,26 @@ import java.time.LocalTime import java.util.* data class ShowingDTO( - val id: UUID, - val webId: Base64ID, - val slug: String, - val date: LocalDate, - val time: LocalTime, - val movieId: UUID, - val location: Location, - val sfScreen: SfLiteScreenDTO?, - val private: Boolean, - val price: SEK?, - val ticketsBought: Boolean, - val admin: UserID, - val payToUser: UserID, - val expectedBuyDate: LocalDate?, - val participants: Collection, - val lastModifiedDate: Instant = Instant.EPOCH, - val createdDate: Instant = Instant.EPOCH + val id: UUID, + val webId: Base64ID, + val slug: String, + val date: LocalDate, + val time: LocalTime, + val movieId: UUID, + val location: Location, + val filmstadenScreen: FilmstadenLiteScreenDTO?, + val private: Boolean, + val price: SEK?, + val ticketsBought: Boolean, + val admin: UserID, + val payToUser: UserID, + val expectedBuyDate: LocalDate?, + val participants: Collection, + val lastModifiedDate: Instant = Instant.EPOCH, + val createdDate: Instant = Instant.EPOCH, + val filmstadenRemoteEntityId: String? ) { - fun fullDate(): LocalDateTime { - return LocalDateTime.of(date, time) - } + fun fullDate(): LocalDateTime { + return LocalDateTime.of(date, time) + } } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/TmdbDTOs.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/TmdbDTOs.kt index c7ee972f9..7c8a742ed 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/TmdbDTOs.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/TmdbDTOs.kt @@ -5,62 +5,62 @@ import java.time.LocalDate data class TmdbFindExternalResults(val movie_results: List) data class TmdbMovieResult( - val adult: Boolean, - val backdrop_path: String?, - val genre_ids: List, - val id: Long, - val original_language: String, - val original_title: String, - val overview: String?, - val release_date: LocalDate?, - val poster_path: String?, - val popularity: Double, - val title: String, - val video: Boolean, - val vote_average: Double, - val vote_count: Long + val adult: Boolean, + val backdrop_path: String?, + val genre_ids: List, + val id: Long, + val original_language: String, + val original_title: String, + val overview: String?, + val release_date: LocalDate?, + val poster_path: String?, + val popularity: Double, + val title: String, + val video: Boolean, + val vote_average: Double, + val vote_count: Long ) data class TmdbMovieDetails( - val adult: Boolean, - val belongs_to_collection: Any?, - val budget: Int, - val genres: List, - val homepage: String?, - val id: Long, - val imdb_id: String, - val original_language: String, - val original_title: String, - val overview: String?, - val popularity: Double, - val production_companies: List, - val production_countries: List, - val release_date: LocalDate?, - val revenue: Long, - val runtime: Int?, - val spoken_languages: List, - val status: String, - val tagline: String?, - val title: String, - val video: Boolean, - val vote_average: Double, - val vote_count: Long, - val poster_path: String?, - val backdrop_path: String? + val adult: Boolean, + val belongs_to_collection: Any?, + val budget: Int, + val genres: List, + val homepage: String?, + val id: Long, + val imdb_id: String, + val original_language: String, + val original_title: String, + val overview: String?, + val popularity: Double, + val production_companies: List, + val production_countries: List, + val release_date: LocalDate?, + val revenue: Long, + val runtime: Int?, + val spoken_languages: List, + val status: String, + val tagline: String?, + val title: String, + val video: Boolean, + val vote_average: Double, + val vote_count: Long, + val poster_path: String?, + val backdrop_path: String? ) { - fun fullPosterPath(): String? { - if (this.poster_path != null) { - return "https://image.tmdb.org/t/p/w500/$poster_path" + fun fullPosterPath(): String? { + if (this.poster_path != null) { + return "https://image.tmdb.org/t/p/w500/$poster_path" + } + return null } - return null - } - fun fullBackdropPath(): String? { - if (this.backdrop_path != null) { - return "https://image.tmdb.org/t/p/w750/$backdrop_path" + fun fullBackdropPath(): String? { + if (this.backdrop_path != null) { + return "https://image.tmdb.org/t/p/w750/$backdrop_path" + } + return null } - return null - } } data class TmdbGenre(val id: Int, val name: String) diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UpdateShowingDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UpdateShowingDTO.kt index e03d6d770..b132e6ab0 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UpdateShowingDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UpdateShowingDTO.kt @@ -4,11 +4,12 @@ import java.time.LocalDate import java.time.LocalTime data class UpdateShowingDTO( - val price: Long = 0, - val private: Boolean = false, - val payToUser: String = "N/A", - val expectedBuyDate: LocalDate? = null, - val location: String = "Filmstaden Bergakungen", - val sfScreen: SfLiteScreenDTO? = null, - val time: LocalTime = LocalTime.NOON + val price: Long = 0, + val private: Boolean = false, + val payToUser: String = "N/A", + val expectedBuyDate: LocalDate? = null, + val location: String = "Filmstaden Bergakungen", + val filmstadenRemoteEntityId: String? = null, + val time: LocalTime = LocalTime.NOON, + val date: LocalDate ) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UserDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UserDTO.kt index 198908cb0..f0a46ba00 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UserDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UserDTO.kt @@ -6,21 +6,21 @@ import java.time.Instant import java.util.* class UserDTO( - val id: UserID = UserID("N/A"), - val name: String? = null, - val firstName: String? = null, - val lastName: String? = null, - val nick: String? = null, - val email: String = "", - val sfMembershipId: String? = null, - val phone: String? = null, - val avatar: String? = null, - val foretagsbiljetter: List = emptyList(), - val notificationSettings: NotificationSettings, - val lastLogin: Instant = Instant.ofEpochSecond(0L), - val signupDate: Instant = Instant.ofEpochSecond(0L), - val calendarFeedId: UUID? + val id: UserID = UserID("N/A"), + val name: String? = null, + val firstName: String? = null, + val lastName: String? = null, + val nick: String? = null, + val email: String = "", + val filmstadenMembershipId: String? = null, + val phone: String? = null, + val avatar: String? = null, + val foretagsbiljetter: List = emptyList(), + val notificationSettings: NotificationSettings, + val lastLogin: Instant = Instant.ofEpochSecond(0L), + val signupDate: Instant = Instant.ofEpochSecond(0L), + val calendarFeedId: UUID? ) { - fun toLimitedUserDTO() = - LimitedUserDTO(this.id, this.name, this.firstName, this.lastName, this.nick, this.phone, this.avatar) + fun toLimitedUserDTO() = + LimitedUserDTO(this.id, this.name, this.firstName, this.lastName, this.nick, this.phone, this.avatar) } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UserDetailsDTO.kt b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UserDetailsDTO.kt index 435a144d7..c524a0d09 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UserDetailsDTO.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/domain/dto/UserDetailsDTO.kt @@ -4,9 +4,9 @@ import rocks.didit.sefilm.domain.Foretagsbiljett import java.time.LocalDate data class ForetagsbiljettDTO( - val number: String, - val expires: LocalDate = LocalDate.now().plusYears(1), - val status: Foretagsbiljett.Status? + val number: String, + val expires: LocalDate = LocalDate.now().plusYears(1), + val status: Foretagsbiljett.Status? ) -data class UserDetailsDTO(val nick: String?, val phone: String?, val sfMembershipId: String?) \ No newline at end of file +data class UserDetailsDTO(val nick: String?, val phone: String?, val filmstadenMembershipId: String?) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/events/EventLogger.kt b/backend/src/main/kotlin/rocks/didit/sefilm/events/EventLogger.kt index 34929deaa..18f231e92 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/events/EventLogger.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/events/EventLogger.kt @@ -8,20 +8,20 @@ import rocks.didit.sefilm.logger @Component @Profile("dev") class EventLogger { - companion object { - private val log by logger() - } + companion object { + private val log by logger() + } - @EventListener - fun logEvents(event: ITBioEvent) { - return when (event) { - is NewShowingEvent -> log.info("User ${event.triggeredBy.nick} created a new showing with ID=${event.showing.id} for movie '${event.showing.movie.title}'") - is UpdatedShowingEvent -> log.info("User ${event.triggeredBy.nick} updated a new showing with ID=${event.showing.id}") - is DeletedShowingEvent -> log.info("User ${event.triggeredBy.nick} deleted a new showing with ID=${event.showing.id}") - is TicketsBoughtEvent -> log.info("User ${event.triggeredBy.nick} bought tickets for showing ${event.showing.id}") - is UserAttendedEvent -> log.info("User ${event.triggeredBy.nick} attended showing ${event.showing.id} with payment type ${event.paymentType}") - is UserUnattendedEvent -> log.info("User ${event.triggeredBy.nick} unattended showing ${event.showing.id}") - is PushoverUserKeyInvalidEvent -> log.info("Got an invalid Pushover user key: ${event.userKey}") + @EventListener + fun logEvents(event: ITBioEvent) { + return when (event) { + is NewShowingEvent -> log.info("User ${event.triggeredBy.nick} created a new showing with ID=${event.showing.id} for movie '${event.showing.movie.title}'") + is UpdatedShowingEvent -> log.info("User ${event.triggeredBy.nick} updated a new showing with ID=${event.showing.id}") + is DeletedShowingEvent -> log.info("User ${event.triggeredBy.nick} deleted a new showing with ID=${event.showing.id}") + is TicketsBoughtEvent -> log.info("User ${event.triggeredBy.nick} bought tickets for showing ${event.showing.id}") + is UserAttendedEvent -> log.info("User ${event.triggeredBy.nick} attended showing ${event.showing.id} with payment type ${event.paymentType}") + is UserUnattendedEvent -> log.info("User ${event.triggeredBy.nick} unattended showing ${event.showing.id}") + is PushoverUserKeyInvalidEvent -> log.info("Got an invalid Pushover user key: ${event.userKey}") + } } - } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/events/EventPublisher.kt b/backend/src/main/kotlin/rocks/didit/sefilm/events/EventPublisher.kt index 2dbfc21a7..ecac208f4 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/events/EventPublisher.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/events/EventPublisher.kt @@ -5,9 +5,9 @@ import org.springframework.stereotype.Service @Service class EventPublisher( - private val eventPublisher: ApplicationEventPublisher) { + private val eventPublisher: ApplicationEventPublisher) { - fun publish(event: ITBioEvent) { - eventPublisher.publishEvent(event) - } + fun publish(event: ITBioEvent) { + eventPublisher.publishEvent(event) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/events/ITBioEvent.kt b/backend/src/main/kotlin/rocks/didit/sefilm/events/ITBioEvent.kt index 94057873e..66a244171 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/events/ITBioEvent.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/events/ITBioEvent.kt @@ -10,37 +10,35 @@ import rocks.didit.sefilm.notification.NotificationType sealed class ITBioEvent(src: Any) : ApplicationEvent(src) sealed class NotificationEvent(src: Any, val potentialRecipients: List = listOf(), val type: NotificationType) - : ITBioEvent(src) + : ITBioEvent(src) /** If {@link #potentialRecipients} is empty, all users will be potential recipients */ sealed class ShowingEvent( - src: Any, - val showing: Showing, - val triggeredBy: User, - satisfiesType: NotificationType, - potentialRecipients: List = listOf()) : NotificationEvent(src, potentialRecipients, satisfiesType) + src: Any, + val showing: Showing, + val triggeredBy: User, + satisfiesType: NotificationType, + potentialRecipients: List = listOf()) : NotificationEvent(src, potentialRecipients, satisfiesType) class NewShowingEvent(src: Any, showing: Showing, user: User) - : ShowingEvent(src, showing, user, NotificationType.NewShowing) + : ShowingEvent(src, showing, user, NotificationType.NewShowing) class UpdatedShowingEvent(src: Any, showing: Showing, user: User) - : ShowingEvent(src, showing, user, NotificationType.UpdateShowing, showing.allParticipants()) + : ShowingEvent(src, showing, user, NotificationType.UpdateShowing, showing.allParticipants()) class DeletedShowingEvent(src: Any, showing: Showing, user: User) - : ShowingEvent(src, showing, user, NotificationType.DeletedShowing, showing.allParticipants()) + : ShowingEvent(src, showing, user, NotificationType.DeletedShowing, showing.allParticipants()) class TicketsBoughtEvent(src: Any, showing: Showing, user: User) - : ShowingEvent(src, showing, user, NotificationType.TicketsBought, showing.allParticipants()) + : ShowingEvent(src, showing, user, NotificationType.TicketsBought, showing.allParticipants()) class UserAttendedEvent(src: Any, showing: Showing, user: User, val paymentType: PaymentType) - : ShowingEvent(src, showing, user, NotificationType.UserAttended, listOf(showing.admin.id)) + : ShowingEvent(src, showing, user, NotificationType.UserAttended, listOf(showing.admin.id)) class UserUnattendedEvent(src: Any, showing: Showing, user: User) - : ShowingEvent(src, showing, user, NotificationType.UserUnattended, listOf(showing.admin.id)) - -// TODO: class SfTicketsAvailableEvent(src: Any, showing: Showing, user: User): ShowingEvent(src, showing, user, NotificationType.SfTicketsAvailable) + : ShowingEvent(src, showing, user, NotificationType.UserUnattended, listOf(showing.admin.id)) class PushoverUserKeyInvalidEvent(src: Any, val userKey: String) : ITBioEvent(src) private fun Showing.allParticipants() = - this.participants.map { it.userId } + this.participants.map { it.userId } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/BioBudordResolver.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/BioBudordResolver.kt index c7b7b188d..4125db0ad 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/BioBudordResolver.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/BioBudordResolver.kt @@ -8,8 +8,8 @@ import rocks.didit.sefilm.services.BudordService @Component class BioBudordResolver(private val budordService: BudordService) : GraphQLQueryResolver { - fun allBiobudord() = budordService.getAll() - fun randomBudord() = budordService.getRandom() + fun allBiobudord() = budordService.getAll() + fun randomBudord() = budordService.getRandom() } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/CustomScalars.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/CustomScalars.kt index 4ba6dcf84..ab95d1916 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/CustomScalars.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/CustomScalars.kt @@ -12,155 +12,155 @@ import java.util.* @Component class CustomScalarUUID : GraphQLScalarType("UUID", "UUID", object : Coercing { - override fun parseLiteral(input: Any?): UUID { - return when (input) { - is StringValue -> UUID.fromString(input.value) - is String -> UUID.fromString(input) - else -> throw IllegalArgumentException("Unable to parse UUID from $input") + override fun parseLiteral(input: Any?): UUID { + return when (input) { + is StringValue -> UUID.fromString(input.value) + is String -> UUID.fromString(input) + else -> throw IllegalArgumentException("Unable to parse UUID from $input") + } } - } - override fun parseValue(input: Any?): UUID = parseLiteral(input) + override fun parseValue(input: Any?): UUID = parseLiteral(input) - override fun serialize(dataFetcherResult: Any?): String? = when (dataFetcherResult) { - is String -> dataFetcherResult - is UUID -> dataFetcherResult.toString() - else -> null - } + override fun serialize(dataFetcherResult: Any?): String? = when (dataFetcherResult) { + is String -> dataFetcherResult + is UUID -> dataFetcherResult.toString() + else -> null + } }) @Component class CustomScalarBase64ID : GraphQLScalarType("Base64ID", "Base64ID", object : Coercing { - override fun parseLiteral(input: Any?): Base64ID { - return when (input) { - is StringValue -> Base64ID(input.value) - is String -> Base64ID(input) - else -> throw IllegalArgumentException("Unable to parse Base64ID from $input") + override fun parseLiteral(input: Any?): Base64ID { + return when (input) { + is StringValue -> Base64ID(input.value) + is String -> Base64ID(input) + else -> throw IllegalArgumentException("Unable to parse Base64ID from $input") + } } - } - override fun parseValue(input: Any?): Base64ID = parseLiteral(input) + override fun parseValue(input: Any?): Base64ID = parseLiteral(input) - override fun serialize(input: Any?): String? = when (input) { - is String -> input - is Base64ID -> input.id - else -> null - } + override fun serialize(input: Any?): String? = when (input) { + is String -> input + is Base64ID -> input.id + else -> null + } }) @Component class CustomScalarSEK : GraphQLScalarType("SEK", "Swedish Kronor", object : Coercing { - override fun parseLiteral(input: Any?): SEK? { - return when (input) { - is Long -> SEK(input) - is IntValue -> SEK(input.value.toLong()) - is Int -> SEK(input.toLong()) - else -> null + override fun parseLiteral(input: Any?): SEK? { + return when (input) { + is Long -> SEK(input) + is IntValue -> SEK(input.value.toLong()) + is Int -> SEK(input.toLong()) + else -> null + } } - } - override fun parseValue(input: Any?): SEK? = parseLiteral(input) + override fun parseValue(input: Any?): SEK? = parseLiteral(input) - override fun serialize(dataFetcherResult: Any?): Long? = when (dataFetcherResult) { - is Long -> dataFetcherResult - is SEK -> dataFetcherResult.toÖren() - else -> null - } + override fun serialize(dataFetcherResult: Any?): Long? = when (dataFetcherResult) { + is Long -> dataFetcherResult + is SEK -> dataFetcherResult.toÖren() + else -> null + } }) @Component class CustomScalarLocalDate : - GraphQLScalarType("LocalDate", "Simple LocalDate, i.e. 2017-12-01", object : Coercing { - override fun parseLiteral(input: Any?): LocalDate? { - return when (input) { - is StringValue -> LocalDate.parse(input.value) - is String -> LocalDate.parse(input) - else -> null - } - } - - override fun parseValue(input: Any?): LocalDate? = parseLiteral(input) - - override fun serialize(dataFetcherResult: Any?): String? = when (dataFetcherResult) { - is String -> dataFetcherResult - is LocalDate -> dataFetcherResult.toString() - else -> null - } - }) + GraphQLScalarType("LocalDate", "Simple LocalDate, i.e. 2017-12-01", object : Coercing { + override fun parseLiteral(input: Any?): LocalDate? { + return when (input) { + is StringValue -> LocalDate.parse(input.value) + is String -> LocalDate.parse(input) + else -> null + } + } + + override fun parseValue(input: Any?): LocalDate? = parseLiteral(input) + + override fun serialize(dataFetcherResult: Any?): String? = when (dataFetcherResult) { + is String -> dataFetcherResult + is LocalDate -> dataFetcherResult.toString() + else -> null + } + }) @Component class CustomScalarLocalTime : - GraphQLScalarType("LocalTime", "Simple LocalTime, i.e. 19:37:21", object : Coercing { - override fun parseLiteral(input: Any?): LocalTime? { - return when (input) { - is StringValue -> LocalTime.parse(input.value) - is String -> LocalTime.parse(input) - else -> null - } - } - - override fun parseValue(input: Any?): LocalTime? = parseLiteral(input) - - override fun serialize(dataFetcherResult: Any?): String? = when (dataFetcherResult) { - is String -> dataFetcherResult - is LocalTime -> dataFetcherResult.toString() - else -> null - } - }) + GraphQLScalarType("LocalTime", "Simple LocalTime, i.e. 19:37:21", object : Coercing { + override fun parseLiteral(input: Any?): LocalTime? { + return when (input) { + is StringValue -> LocalTime.parse(input.value) + is String -> LocalTime.parse(input) + else -> null + } + } + + override fun parseValue(input: Any?): LocalTime? = parseLiteral(input) + + override fun serialize(dataFetcherResult: Any?): String? = when (dataFetcherResult) { + is String -> dataFetcherResult + is LocalTime -> dataFetcherResult.toString() + else -> null + } + }) @Component class CustomScalarIMDbID : GraphQLScalarType("IMDbID", "IMDb ID, i.e. tt2527336", object : Coercing { - override fun parseLiteral(input: Any?): IMDbID { - return when (input) { - is StringValue -> IMDbID.valueOf(input.value) - is String -> IMDbID.valueOf(input) - else -> IMDbID.MISSING + override fun parseLiteral(input: Any?): IMDbID { + return when (input) { + is StringValue -> IMDbID.valueOf(input.value) + is String -> IMDbID.valueOf(input) + else -> IMDbID.MISSING + } } - } - override fun parseValue(input: Any?): IMDbID = parseLiteral(input) + override fun parseValue(input: Any?): IMDbID = parseLiteral(input) - override fun serialize(input: Any?): String? = when (input) { - is String -> input - is IMDbID -> input.value - else -> null - } + override fun serialize(input: Any?): String? = when (input) { + is String -> input + is IMDbID -> input.value + else -> null + } }) @Component class CustomScalarTMDbID : GraphQLScalarType("TMDbID", "TMDb ID", object : Coercing { - override fun parseLiteral(input: Any?): TMDbID { - return when (input) { - is IntValue -> TMDbID.valueOf(input.value.toLong()) - is Int -> TMDbID.valueOf(input.toLong()) - else -> TMDbID.MISSING + override fun parseLiteral(input: Any?): TMDbID { + return when (input) { + is IntValue -> TMDbID.valueOf(input.value.toLong()) + is Int -> TMDbID.valueOf(input.toLong()) + else -> TMDbID.MISSING + } } - } - override fun parseValue(input: Any?): TMDbID = parseLiteral(input) + override fun parseValue(input: Any?): TMDbID = parseLiteral(input) - override fun serialize(input: Any?): Long? = when (input) { - is Long -> input - is TMDbID -> input.value - else -> null - } + override fun serialize(input: Any?): Long? = when (input) { + is Long -> input + is TMDbID -> input.value + else -> null + } }) @Component class CustomScalarUserID : GraphQLScalarType("UserID", "UserID", object : Coercing { - override fun parseLiteral(input: Any?): UserID { - return when (input) { - is StringValue -> UserID(input.value) - is String -> UserID(input) - else -> throw IllegalArgumentException("Unablet to convert '$input' to a UserID") + override fun parseLiteral(input: Any?): UserID { + return when (input) { + is StringValue -> UserID(input.value) + is String -> UserID(input) + else -> throw IllegalArgumentException("Unablet to convert '$input' to a UserID") + } } - } - override fun parseValue(input: Any?): UserID = parseLiteral(input) + override fun parseValue(input: Any?): UserID = parseLiteral(input) - override fun serialize(input: Any?): String? = when (input) { - is String -> input - is UserID -> input.id - else -> null - } + override fun serialize(input: Any?): String? = when (input) { + is String -> input + is UserID -> input.id + else -> null + } }) diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/GraphqlExceptionHandler.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/GraphqlExceptionHandler.kt index 9788548e2..046a6d46c 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/GraphqlExceptionHandler.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/GraphqlExceptionHandler.kt @@ -14,35 +14,35 @@ import rocks.didit.sefilm.KnownException @Component class GraphqlExceptionHandler : DataFetcherExceptionHandler { - private val log: Logger = LoggerFactory.getLogger(GraphqlExceptionHandler::class.java) + private val log: Logger = LoggerFactory.getLogger(GraphqlExceptionHandler::class.java) - override fun accept(handlerParameters: DataFetcherExceptionHandlerParameters) { - val exception = handlerParameters.exception - val sourceLocation = handlerParameters.field.sourceLocation - val path = handlerParameters.path + override fun accept(handlerParameters: DataFetcherExceptionHandlerParameters) { + val exception = handlerParameters.exception + val sourceLocation = handlerParameters.field.sourceLocation + val path = handlerParameters.path - val graphqlError = when (exception) { - is KnownException -> createFetchingError( - exception.message ?: "", - mapOf("offendingUser" to exception.whichUser, "showing" to exception.whichShowing) - ) - is IllegalArgumentException -> createFetchingError(exception.message ?: "") - is AccessDeniedException -> createFetchingError(exception.message ?: "") - else -> { - log.warn("Exception during data fetching: ${exception.message}", exception) - ExceptionWhileDataFetching(path, exception, sourceLocation) - } - } - handlerParameters.executionContext.addError(graphqlError, path) + val graphqlError = when (exception) { + is KnownException -> createFetchingError( + exception.message ?: "", + mapOf("offendingUser" to exception.whichUser, "showing" to exception.whichShowing) + ) + is IllegalArgumentException -> createFetchingError(exception.message ?: "") + is AccessDeniedException -> createFetchingError(exception.message ?: "") + else -> { + log.warn("Exception during data fetching: ${exception.message}", exception) + ExceptionWhileDataFetching(path, exception, sourceLocation) + } + } + handlerParameters.executionContext.addError(graphqlError, path) - } + } - private fun createFetchingError(msg: String, extensions: Map = mapOf()): GraphQLError { - return object : GraphQLError { - override fun getMessage(): String = msg - override fun getErrorType(): ErrorType = ErrorType.DataFetchingException - override fun getLocations(): List = listOf() - override fun getExtensions(): Map = extensions + private fun createFetchingError(msg: String, extensions: Map = mapOf()): GraphQLError { + return object : GraphQLError { + override fun getMessage(): String = msg + override fun getErrorType(): ErrorType = ErrorType.DataFetchingException + override fun getLocations(): List = listOf() + override fun getExtensions(): Map = extensions + } } - } } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/LocationResolvers.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/LocationResolvers.kt index e67830ef1..60458f51b 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/LocationResolvers.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/LocationResolvers.kt @@ -8,7 +8,7 @@ import rocks.didit.sefilm.services.LocationService @Component class LocationResolver(private val locationService: LocationService) : GraphQLQueryResolver { - fun previousLocations() = locationService.allLocations() - fun location(id: String) = locationService.getLocation(id) - fun sfCities() = locationService.sfCities() + fun previousLocations() = locationService.allLocations() + fun location(id: String) = locationService.getLocation(id) + fun filmstadenCities() = locationService.filmstadenCities() } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/MovieMutationResolver.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/MovieMutationResolver.kt index a303ac82c..45489e94d 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/MovieMutationResolver.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/MovieMutationResolver.kt @@ -9,8 +9,8 @@ import rocks.didit.sefilm.services.MovieService @Component class MovieMutationResolver( - private val movieService: MovieService + private val movieService: MovieService ) : GraphQLMutationResolver { - fun fetchNewMoviesFromSf(): List = movieService.fetchNewMoviesFromSf() + fun fetchNewMoviesFromFilmstaden(): List = movieService.fetchNewMoviesFromFilmstaden() } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/MovieResolvers.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/MovieResolvers.kt index e019592a1..d773a55e5 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/MovieResolvers.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/MovieResolvers.kt @@ -9,36 +9,37 @@ import org.slf4j.LoggerFactory import org.springframework.stereotype.Component import rocks.didit.sefilm.Properties import rocks.didit.sefilm.database.entities.Movie -import rocks.didit.sefilm.domain.dto.SfShowingDTO +import rocks.didit.sefilm.domain.dto.FilmstadenShowingDTO import rocks.didit.sefilm.services.MovieService -import rocks.didit.sefilm.services.external.SFService +import rocks.didit.sefilm.services.external.FilmstadenService import java.time.LocalDate import java.time.ZoneOffset import java.util.* @Component class MovieQueryResolver(private val movieService: MovieService) : GraphQLQueryResolver { - fun getMovie(id: UUID) = movieService.getMovie(id) - fun allMovies() = movieService.allMovies() - fun archivedMovies() = movieService.archivedMovies() + fun getMovie(id: UUID) = movieService.getMovie(id) + fun allMovies() = movieService.allMovies() + fun archivedMovies() = movieService.archivedMovies() } @Component -class SfShowingResolver( - private val sfService: SFService, - private val properties: Properties +class FilmstadenShowingResolver( + private val filmstadenService: FilmstadenService, + private val properties: Properties ) : GraphQLResolver { - private val log: Logger = LoggerFactory.getLogger(SfShowingResolver::class.java) - fun sfShowings(movie: Movie, city: String?, afterDate: LocalDate?): List { - log.info("Fetching SF showings after ${afterDate ?: "EPOCH"} in city=$city for '${movie.title}' (${movie.id})") - if (movie.sfId == null) return listOf() - return sfService.getShowingDates(movie.sfId, city ?: properties.defaultCity) - .filter { - val after = (afterDate ?: LocalDate.MIN).atStartOfDay().toInstant(ZoneOffset.UTC) - it.timeUtc.isAfter(after) - } - } + private val log: Logger = LoggerFactory.getLogger(FilmstadenShowingResolver::class.java) + fun filmstadenShowings(movie: Movie, city: String?, afterDate: LocalDate?): List { + log.info("Fetching Filmstaden showings after ${afterDate + ?: "EPOCH"} in city=$city for '${movie.title}' (${movie.id})") + if (movie.filmstadenId == null) return listOf() + return filmstadenService.getShowingDates(movie.filmstadenId, city ?: properties.defaultCity) + .filter { + val after = (afterDate ?: LocalDate.MIN).atStartOfDay().toInstant(ZoneOffset.UTC) + it.timeUtc.isAfter(after) + } + } } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/NotificationResolvers.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/NotificationResolvers.kt index c1c118f4d..a4eaf3e7b 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/NotificationResolvers.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/NotificationResolvers.kt @@ -10,10 +10,9 @@ import rocks.didit.sefilm.notification.providers.NotificationProviderDTO @Component class NotificationResolvers(private val notificationProviders: List>) : GraphQLQueryResolver { - fun allNotificationProviders(): List - = notificationProviders - .filter { it.isSubscribable } - .map { NotificationProviderDTO.from(it) } + fun allNotificationProviders(): List = notificationProviders + .filter { it.isSubscribable } + .map { NotificationProviderDTO.from(it) } - fun allNotificationTypes() = NotificationType.values().toList() + fun allNotificationTypes() = NotificationType.values().toList() } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ParticipantPaymentInfoResolver.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ParticipantPaymentInfoResolver.kt index b3fe6ad24..916a30ef0 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ParticipantPaymentInfoResolver.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ParticipantPaymentInfoResolver.kt @@ -13,23 +13,23 @@ import rocks.didit.sefilm.services.UserService @Component class ParticipantPaymentInfoResolver( - private val userService: UserService, - private val showingService: ShowingService + private val userService: UserService, + private val showingService: ShowingService ) : GraphQLResolver { - fun user(participantPaymentInfo: ParticipantPaymentInfo): LimitedUserDTO = - userService.getUserOrThrow(participantPaymentInfo.userId) + fun user(participantPaymentInfo: ParticipantPaymentInfo): LimitedUserDTO = + userService.getUserOrThrow(participantPaymentInfo.userId) - fun showing(participantPaymentInfo: ParticipantPaymentInfo): ShowingDTO = - showingService.getShowingOrThrow(participantPaymentInfo.showingId) + fun showing(participantPaymentInfo: ParticipantPaymentInfo): ShowingDTO = + showingService.getShowingOrThrow(participantPaymentInfo.showingId) } @Component class PaymentInfoResolver( - private val userService: UserService + private val userService: UserService ) : GraphQLResolver { - fun payTo(paymentDTO: AttendeePaymentDetailsDTO): LimitedUserDTO = userService.getUserOrThrow(paymentDTO.payTo) + fun payTo(paymentDTO: AttendeePaymentDetailsDTO): LimitedUserDTO = userService.getUserOrThrow(paymentDTO.payTo) - fun payer(paymentDTO: AttendeePaymentDetailsDTO): LimitedUserDTO = userService.getUserOrThrow(paymentDTO.payerUserID) + fun payer(paymentDTO: AttendeePaymentDetailsDTO): LimitedUserDTO = userService.getUserOrThrow(paymentDTO.payerUserID) } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ShowingMutationResolver.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ShowingMutationResolver.kt index bb90b7ebd..5bd8f5a73 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ShowingMutationResolver.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ShowingMutationResolver.kt @@ -5,6 +5,7 @@ package rocks.didit.sefilm.graphql import com.coxautodev.graphql.tools.GraphQLMutationResolver import org.springframework.stereotype.Component import rocks.didit.sefilm.domain.PaymentOption +import rocks.didit.sefilm.domain.SEK import rocks.didit.sefilm.domain.UserID import rocks.didit.sefilm.domain.dto.CreateShowingDTO import rocks.didit.sefilm.domain.dto.ShowingDTO @@ -16,29 +17,29 @@ import java.util.* @Component class ShowingMutationResolver( - private val showingService: ShowingService, - private val adminService: AdminService, - private val ticketService: TicketService + private val showingService: ShowingService, + private val adminService: AdminService, + private val ticketService: TicketService ) : GraphQLMutationResolver { - fun attendShowing(showingId: UUID, paymentOption: PaymentOption): ShowingDTO = - showingService.attendShowing(showingId, paymentOption) + fun attendShowing(showingId: UUID, paymentOption: PaymentOption): ShowingDTO = + showingService.attendShowing(showingId, paymentOption) - fun unattendShowing(showingId: UUID): ShowingDTO = showingService.unattendShowing(showingId) + fun unattendShowing(showingId: UUID): ShowingDTO = showingService.unattendShowing(showingId) - fun createShowing(showing: CreateShowingDTO): ShowingDTO = showingService.createShowing(showing) + fun createShowing(showing: CreateShowingDTO): ShowingDTO = showingService.createShowing(showing) - fun deleteShowing(showingId: UUID): List = showingService.deleteShowing(showingId) + fun deleteShowing(showingId: UUID): List = showingService.deleteShowing(showingId) - fun markAsBought(showingId: UUID): ShowingDTO = showingService.markAsBought(showingId) + fun markAsBought(showingId: UUID, price: SEK): ShowingDTO = showingService.markAsBought(showingId, price) - fun processTicketUrls(showingId: UUID, ticketUrls: List): ShowingDTO { - ticketService.processTickets(ticketUrls, showingId) - return showingService.getShowingOrThrow(showingId) - } + fun processTicketUrls(showingId: UUID, ticketUrls: List): ShowingDTO { + ticketService.processTickets(ticketUrls, showingId) + return showingService.getShowingOrThrow(showingId) + } - fun updateShowing(showingId: UUID, newValues: UpdateShowingDTO): ShowingDTO = - showingService.updateShowing(showingId, newValues) + fun updateShowing(showingId: UUID, newValues: UpdateShowingDTO): ShowingDTO = + showingService.updateShowing(showingId, newValues) - fun promoteToAdmin(showingId: UUID, userToPromote: UserID): ShowingDTO = - adminService.promoteToAdmin(showingId, userToPromote) + fun promoteToAdmin(showingId: UUID, userToPromote: UserID): ShowingDTO = + adminService.promoteToAdmin(showingId, userToPromote) } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ShowingResolvers.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ShowingResolvers.kt index aaf1fd016..141d9d3bf 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ShowingResolvers.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/ShowingResolvers.kt @@ -21,70 +21,70 @@ import java.util.* @Component class ShowingQueryResolver(private val showingService: ShowingService) : GraphQLQueryResolver { - fun publicShowings(afterDate: LocalDate?) = showingService.getAllPublicShowings(afterDate ?: LocalDate.MIN) - fun privateShowingsForCurrentUser(afterDate: LocalDate?) = - showingService.getPrivateShowingsForCurrentUser(afterDate ?: LocalDate.MIN) - - fun showing(id: UUID?, webId: Base64ID?): ShowingDTO? { - return when { - id != null -> showingService.getShowing(id) - webId != null -> showingService.getShowing(webId) - else -> null + fun publicShowings(afterDate: LocalDate?) = showingService.getAllPublicShowings(afterDate ?: LocalDate.MIN) + fun privateShowingsForCurrentUser(afterDate: LocalDate?) = + showingService.getPrivateShowingsForCurrentUser(afterDate ?: LocalDate.MIN) + + fun showing(id: UUID?, webId: Base64ID?): ShowingDTO? { + return when { + id != null -> showingService.getShowing(id) + webId != null -> showingService.getShowing(webId) + else -> null + } } - } - fun showing(webId: Base64ID): ShowingDTO? = showingService.getShowing(webId) - fun showingForMovie(movieId: UUID) = showingService.getShowingByMovie(movieId) + fun showing(webId: Base64ID): ShowingDTO? = showingService.getShowing(webId) + fun showingForMovie(movieId: UUID) = showingService.getShowingByMovie(movieId) } @Component class ShowingResolver( - private val showingService: ShowingService, - private val userService: UserService, - private val movieService: MovieService, - private val ticketService: TicketService + private val showingService: ShowingService, + private val userService: UserService, + private val movieService: MovieService, + private val ticketService: TicketService ) : GraphQLResolver { - fun admin(showing: ShowingDTO): LimitedUserDTO = userService - .getUser(showing.admin) - .orElseThrow { NotFoundException("admin user", showing.admin, showing.id) } - - fun payToUser(showing: ShowingDTO): LimitedUserDTO = userService - .getUser(showing.payToUser) - .orElseThrow { NotFoundException("payment receiver user", showing.payToUser, showing.id) } - - fun movie(showing: ShowingDTO): Movie { - val id = showing.movieId - return movieService.getMovie(id) - .orElseThrow { NotFoundException("movie with id: ${showing.movieId}") } - } + fun admin(showing: ShowingDTO): LimitedUserDTO = userService + .getUser(showing.admin) + .orElseThrow { NotFoundException("admin user", showing.admin, showing.id) } + + fun payToUser(showing: ShowingDTO): LimitedUserDTO = userService + .getUser(showing.payToUser) + .orElseThrow { NotFoundException("payment receiver user", showing.payToUser, showing.id) } + + fun movie(showing: ShowingDTO): Movie { + val id = showing.movieId + return movieService.getMovie(id) + .orElseThrow { NotFoundException("movie with id: ${showing.movieId}") } + } - fun myTickets(showing: ShowingDTO): List = ticketService.getTicketsForCurrentUserAndShowing(showing.id) + fun myTickets(showing: ShowingDTO): List = ticketService.getTicketsForCurrentUserAndShowing(showing.id) - fun ticketRange(showing: ShowingDTO): TicketRange? = ticketService.getTicketRange(showing.id) + fun ticketRange(showing: ShowingDTO): TicketRange? = ticketService.getTicketRange(showing.id) - fun adminPaymentDetails(showing: ShowingDTO): AdminPaymentDetailsDTO? = - showingService.getAdminPaymentDetails(showing.id) + fun adminPaymentDetails(showing: ShowingDTO): AdminPaymentDetailsDTO? = + showingService.getAdminPaymentDetails(showing.id) - fun attendeePaymentDetails(showing: ShowingDTO): AttendeePaymentDetailsDTO? = - showingService.getAttendeePaymentDetails(showing.id) + fun attendeePaymentDetails(showing: ShowingDTO): AttendeePaymentDetailsDTO? = + showingService.getAttendeePaymentDetails(showing.id) - fun sfSeatMap(showing: ShowingDTO): List = showingService.fetchSeatMap(showing.id) + fun filmstadenSeatMap(showing: ShowingDTO): List = showingService.fetchSeatMap(showing.id) } @Component class ParticipantUserResolver(private val userService: UserService) : GraphQLResolver { - fun user(participant: ParticipantDTO): LimitedUserDTO = userService - .getUserOrThrow(participant.userId) + fun user(participant: ParticipantDTO): LimitedUserDTO = userService + .getUserOrThrow(participant.userId) } @Component class TicketUserResolver(private val userService: UserService) : GraphQLResolver { - fun assignedToUser(ticket: Ticket): LimitedUserDTO = userService - .getUserOrThrow(ticket.assignedToUser) + fun assignedToUser(ticket: Ticket): LimitedUserDTO = userService + .getUserOrThrow(ticket.assignedToUser) } @Component -class SfDataUserResolver(private val userService: UserService) : GraphQLResolver { - fun user(data: UserAndSfData): LimitedUserDTO = userService - .getUserOrThrow(data.userId) +class FilmstadenDataUserResolver(private val userService: UserService) : GraphQLResolver { + fun user(data: UserAndFilmstadenData): LimitedUserDTO = userService + .getUserOrThrow(data.userId) } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/UserMutationResolver.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/UserMutationResolver.kt index 80fbd15e9..3072da2d7 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/UserMutationResolver.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/UserMutationResolver.kt @@ -12,29 +12,28 @@ import rocks.didit.sefilm.services.UserService @Component class UserMutationResolver( - private val userService: UserService, - private val participantPaymentInfoService: ParticipantPaymentInfoService, - private val foretagsbiljettService: ForetagsbiljettService + private val userService: UserService, + private val participantPaymentInfoService: ParticipantPaymentInfoService, + private val foretagsbiljettService: ForetagsbiljettService ) : GraphQLMutationResolver { - fun updateUser(newInfo: UserDetailsDTO): UserDTO = userService.updateUser(newInfo) + fun updateUser(newInfo: UserDetailsDTO): UserDTO = userService.updateUser(newInfo) - fun updateParticipantPaymentInfo(paymentInfo: ParticipantPaymentInfoDTO): ParticipantPaymentInfo = - participantPaymentInfoService.updatePaymentInfo(paymentInfo) + fun updateParticipantPaymentInfo(paymentInfo: ParticipantPaymentInfoDTO): ParticipantPaymentInfo = + participantPaymentInfoService.updatePaymentInfo(paymentInfo) - fun addForetagsBiljetter(biljetter: List): UserDTO { - foretagsbiljettService.addForetagsbiljetterToCurrentUser(biljetter) - return userService.getCurrentUser() - } + fun addForetagsBiljetter(biljetter: List): UserDTO { + foretagsbiljettService.addForetagsbiljetterToCurrentUser(biljetter) + return userService.getCurrentUser() + } - fun deleteForetagsBiljett(biljett: ForetagsbiljettDTO): UserDTO { - foretagsbiljettService.deleteTicketFromUser(biljett) - return userService.getCurrentUser() - } + fun deleteForetagsBiljett(biljett: ForetagsbiljettDTO): UserDTO { + foretagsbiljettService.deleteTicketFromUser(biljett) + return userService.getCurrentUser() + } - fun invalidateCalendarFeed() = userService.invalidateCalendarFeedId() - fun disableCalendarFeed() = userService.disableCalendarFeed() + fun invalidateCalendarFeed() = userService.invalidateCalendarFeedId() + fun disableCalendarFeed() = userService.disableCalendarFeed() - fun updateNotificationSettings(notificationInput: NotificationSettingsInputDTO) - = userService.updateNotificationSettings(notificationInput) + fun updateNotificationSettings(notificationInput: NotificationSettingsInputDTO) = userService.updateNotificationSettings(notificationInput) } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/UserResolver.kt b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/UserResolver.kt index 39006ebb5..bd240e69d 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/graphql/UserResolver.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/graphql/UserResolver.kt @@ -12,17 +12,17 @@ import rocks.didit.sefilm.web.controllers.CalendarController @Component class UserResolver(private val userService: UserService) : GraphQLQueryResolver { - fun allUsers() = userService.allUsers() - /** The currently logged in user */ - fun currentUser() = userService.getCurrentUser() + fun allUsers() = userService.allUsers() + /** The currently logged in user */ + fun currentUser() = userService.getCurrentUser() } @Component class CalendarFeedResolver(private val properties: Properties) : GraphQLResolver { - fun calendarFeedUrl(user: UserDTO): String? { - if (user.calendarFeedId == null) { - return null + fun calendarFeedUrl(user: UserDTO): String? { + if (user.calendarFeedId == null) { + return null + } + return "${properties.baseUrl.api}${CalendarController.PATH}/${user.calendarFeedId}" } - return "${properties.baseUrl.api}${CalendarController.PATH}/${user.calendarFeedId}" - } } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/notification/NotificationSettings.kt b/backend/src/main/kotlin/rocks/didit/sefilm/notification/NotificationSettings.kt index 340bb3594..ebf2ae03e 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/notification/NotificationSettings.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/notification/NotificationSettings.kt @@ -1,7 +1,7 @@ package rocks.didit.sefilm.notification data class NotificationSettings( - val notificationsEnabled: Boolean, - val enabledTypes: List, - val providerSettings: List + val notificationsEnabled: Boolean, + val enabledTypes: List, + val providerSettings: List ) \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/notification/NotificationType.kt b/backend/src/main/kotlin/rocks/didit/sefilm/notification/NotificationType.kt index 6812df46f..a52c9ce61 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/notification/NotificationType.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/notification/NotificationType.kt @@ -2,10 +2,10 @@ package rocks.didit.sefilm.notification /** Don't forget to update the corresponding GraphQL enum */ enum class NotificationType { - UpdateShowing, - NewShowing, - DeletedShowing, - TicketsBought, - UserAttended, - UserUnattended, + UpdateShowing, + NewShowing, + DeletedShowing, + TicketsBought, + UserAttended, + UserUnattended, } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/notification/ProviderHelper.kt b/backend/src/main/kotlin/rocks/didit/sefilm/notification/ProviderHelper.kt index 566112cae..3a96848da 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/notification/ProviderHelper.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/notification/ProviderHelper.kt @@ -11,75 +11,74 @@ import kotlin.reflect.full.cast @Service class ProviderHelper( - private val userService: UserService, - private val properties: Properties) { + private val userService: UserService, + private val properties: Properties) { - fun getNotifiableUsers(knownRecipients: List, wantedSettingsType: KClass): List> { - return userService.getUsersThatWantToBeNotified(knownRecipients).mapNotNull { - val notificationSettings - = it.notificationSettings.providerSettings - .filter { it.enabled && wantedSettingsType.isInstance(it) } - .map { wantedSettingsType.cast(it) } + fun getNotifiableUsers(knownRecipients: List, wantedSettingsType: KClass): List> { + return userService.getUsersThatWantToBeNotified(knownRecipients).mapNotNull { + val notificationSettings = it.notificationSettings.providerSettings + .filter { it.enabled && wantedSettingsType.isInstance(it) } + .map { wantedSettingsType.cast(it) } - when { - notificationSettings.size > 1 -> throw IllegalStateException("Found ${notificationSettings.size} enabled notification settings of the same type. Expected 1, got the following '$notificationSettings'") - notificationSettings.isNotEmpty() -> NotifiableUser(it.toLimitedUserDTO(), notificationSettings.first(), it.notificationSettings.enabledTypes) - else -> null - } - }.filter { it.enabledTypes.isNotEmpty() } - } + when { + notificationSettings.size > 1 -> throw IllegalStateException("Found ${notificationSettings.size} enabled notification settings of the same type. Expected 1, got the following '$notificationSettings'") + notificationSettings.isNotEmpty() -> NotifiableUser(it.toLimitedUserDTO(), notificationSettings.first(), it.notificationSettings.enabledTypes) + else -> null + } + }.filter { it.enabledTypes.isNotEmpty() } + } - // TODO: i18n - fun constructMessageBasedOnEvent(event: NotificationEvent): NotificationMessage { - return when (event) { - is NewShowingEvent -> newShowingMessage(event) - is UpdatedShowingEvent -> updatedShowingMessage(event) - is DeletedShowingEvent -> deletedShowingMessage(event) - is TicketsBoughtEvent -> ticketsBoughtMessage(event) - is UserAttendedEvent -> userAttendedMessage(event) - is UserUnattendedEvent -> userUnattendedMessage(event) + // TODO: i18n + fun constructMessageBasedOnEvent(event: NotificationEvent): NotificationMessage { + return when (event) { + is NewShowingEvent -> newShowingMessage(event) + is UpdatedShowingEvent -> updatedShowingMessage(event) + is DeletedShowingEvent -> deletedShowingMessage(event) + is TicketsBoughtEvent -> ticketsBoughtMessage(event) + is UserAttendedEvent -> userAttendedMessage(event) + is UserUnattendedEvent -> userUnattendedMessage(event) + } } - } - private fun newShowingMessage(event: NewShowingEvent): NotificationMessage { - return formatMsg(title = "A showing for ${event.showing.movie.title} has been created", - msg = "Time: ${event.showing.date} ${event.showing.time}.\nLocation: ${event.showing.location?.name}", event = event) - } + private fun newShowingMessage(event: NewShowingEvent): NotificationMessage { + return formatMsg(title = "A showing for ${event.showing.movie.title} has been created", + msg = "Time: ${event.showing.date} ${event.showing.time}.\nLocation: ${event.showing.location?.name}", event = event) + } - private fun updatedShowingMessage(event: UpdatedShowingEvent): NotificationMessage { - return formatMsg(title = "The showing for ${event.showing.movie.title} has been updated", - msg = "Time: ${event.showing.time}.\nLocation: ${event.showing.location?.name}", event = event) - } + private fun updatedShowingMessage(event: UpdatedShowingEvent): NotificationMessage { + return formatMsg(title = "The showing for ${event.showing.movie.title} has been updated", + msg = "Time: ${event.showing.time}.\nLocation: ${event.showing.location?.name}", event = event) + } - private fun deletedShowingMessage(event: DeletedShowingEvent): NotificationMessage { - return formatMsg(title = "The showing for ${event.showing.movie.title} has been removed", - msg = "Sad panda :(", event = event) - } + private fun deletedShowingMessage(event: DeletedShowingEvent): NotificationMessage { + return formatMsg(title = "The showing for ${event.showing.movie.title} has been removed", + msg = "Sad panda :(", event = event) + } - private fun ticketsBoughtMessage(event: TicketsBoughtEvent): NotificationMessage { - return formatMsg(title = "Tickets bought for ${event.showing.movie.title}", - msg = "Price ${event.showing.price?.toKronor()} SEK. Pay to ${event.showing.payToUser.phone}", event = event) - } + private fun ticketsBoughtMessage(event: TicketsBoughtEvent): NotificationMessage { + return formatMsg(title = "Tickets bought for ${event.showing.movie.title}", + msg = "Price ${event.showing.price?.toKronor()} SEK. Pay to ${event.showing.payToUser.phone}", event = event) + } - private fun userAttendedMessage(event: UserAttendedEvent): NotificationMessage { - return formatMsg(title = "${event.triggeredBy.nick} attended showing", - msg = "${event.triggeredBy.nick} has attended your showing. Chosen payment type: ${event.paymentType}", event = event) - } + private fun userAttendedMessage(event: UserAttendedEvent): NotificationMessage { + return formatMsg(title = "${event.triggeredBy.nick} attended showing", + msg = "${event.triggeredBy.nick} has attended your showing. Chosen payment type: ${event.paymentType}", event = event) + } - private fun userUnattendedMessage(event: UserUnattendedEvent): NotificationMessage { - return formatMsg(title = "User unattended showing", - msg = "${event.triggeredBy.nick} has chosen to unattended your showing", event = event) - } + private fun userUnattendedMessage(event: UserUnattendedEvent): NotificationMessage { + return formatMsg(title = "User unattended showing", + msg = "${event.triggeredBy.nick} has chosen to unattended your showing", event = event) + } - private fun formatMsg(title: String, msg: String, event: ShowingEvent): NotificationMessage { - return NotificationMessage(title = title, - message = msg, - url = formatShowingUrl(event), - urlTitle = "View showing") - } + private fun formatMsg(title: String, msg: String, event: ShowingEvent): NotificationMessage { + return NotificationMessage(title = title, + message = msg, + url = formatShowingUrl(event), + urlTitle = "View showing") + } - private fun formatShowingUrl(event: ShowingEvent) = - "${properties.baseUrl.frontend}/showings/${event.showing.id}" + private fun formatShowingUrl(event: ShowingEvent) = + "${properties.baseUrl.frontend}/showings/${event.showing.id}" - data class NotificationMessage(val title: String, val message: String, val url: String, val urlTitle: String) + data class NotificationMessage(val title: String, val message: String, val url: String, val urlTitle: String) } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/notification/ProviderSettings.kt b/backend/src/main/kotlin/rocks/didit/sefilm/notification/ProviderSettings.kt index b01be192c..ce82fced0 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/notification/ProviderSettings.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/notification/ProviderSettings.kt @@ -4,16 +4,16 @@ import rocks.didit.sefilm.services.external.PushoverValidationStatus /** This represents user and provider specific settings */ interface ProviderSettings { - val enabled: Boolean + val enabled: Boolean } data class MailSettings( - override val enabled: Boolean = false, - val mailAddress: String? = null) : ProviderSettings + override val enabled: Boolean = false, + val mailAddress: String? = null) : ProviderSettings data class PushoverSettings( - override val enabled: Boolean = false, - val userKey: String = "", - val device: String? = null, - val userKeyStatus: PushoverValidationStatus = PushoverValidationStatus.UNKNOWN) : ProviderSettings + override val enabled: Boolean = false, + val userKey: String = "", + val device: String? = null, + val userKeyStatus: PushoverValidationStatus = PushoverValidationStatus.UNKNOWN) : ProviderSettings diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/MailNotificationProvider.kt b/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/MailNotificationProvider.kt index cd3061b2d..b10c6e215 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/MailNotificationProvider.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/MailNotificationProvider.kt @@ -14,22 +14,21 @@ import rocks.didit.sefilm.notification.ProviderHelper @ConditionalOnProperty(prefix = "sefilm.notification.provider.MailProvider", name = ["enabled"], matchIfMissing = true, havingValue = "true") class MailNotificationProvider(private val providerHelper: ProviderHelper) : NotificationProvider, ApplicationListener { - private val log = LoggerFactory.getLogger(MailNotificationProvider::class.java) + private val log = LoggerFactory.getLogger(MailNotificationProvider::class.java) - override fun getNotifiableUsers(knownRecipients: List): List> - = providerHelper.getNotifiableUsers(knownRecipients, MailSettings::class) - .filter { it.notificationSettings.mailAddress != null } + override fun getNotifiableUsers(knownRecipients: List): List> = providerHelper.getNotifiableUsers(knownRecipients, MailSettings::class) + .filter { it.notificationSettings.mailAddress != null } - @Async - override fun onApplicationEvent(event: NotificationEvent) { - getNotifiableUsers(event.potentialRecipients).forEach { user -> - if (user.enabledTypes.contains(event.type)) { - // TODO: actually send mail - log.debug("(Not-actually) Sending mail notification to ${user.notificationSettings.mailAddress}") - } + @Async + override fun onApplicationEvent(event: NotificationEvent) { + getNotifiableUsers(event.potentialRecipients).forEach { user -> + if (user.enabledTypes.contains(event.type)) { + // TODO: actually send mail + log.debug("(Not-actually) Sending mail notification to ${user.notificationSettings.mailAddress}") + } + } } - } - override val name = "E-Mail" - override val isSubscribable = true + override val name = "E-Mail" + override val isSubscribable = true } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/NotificationProvider.kt b/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/NotificationProvider.kt index 79eeaa451..f67573d1b 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/NotificationProvider.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/NotificationProvider.kt @@ -7,29 +7,29 @@ import rocks.didit.sefilm.notification.ProviderSettings interface NotificationProvider { - /** Get the user settings related to this provider. I.e. the E-mail addresses we are going to notify */ - fun getNotifiableUsers(knownRecipients: List): List> + /** Get the user settings related to this provider. I.e. the E-mail addresses we are going to notify */ + fun getNotifiableUsers(knownRecipients: List): List> - /** Nice name of the service, i.e. Pushbullet, or E-Mail */ - val name: String + /** Nice name of the service, i.e. Pushbullet, or E-Mail */ + val name: String - /** Should the service be subscribable, i.e. visible to end users in the frontend */ - val isSubscribable: Boolean + /** Should the service be subscribable, i.e. visible to end users in the frontend */ + val isSubscribable: Boolean } data class NotificationProviderDTO( - val name: String, - val isSubscribable: Boolean + val name: String, + val isSubscribable: Boolean ) { - companion object { - fun from(provider: NotificationProvider<*>): NotificationProviderDTO { - return NotificationProviderDTO(provider.name, provider.isSubscribable) + companion object { + fun from(provider: NotificationProvider<*>): NotificationProviderDTO { + return NotificationProviderDTO(provider.name, provider.isSubscribable) + } } - } } data class NotifiableUser( - val user: LimitedUserDTO, - val notificationSettings: T, - val enabledTypes: List) { + val user: LimitedUserDTO, + val notificationSettings: T, + val enabledTypes: List) { } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/PushoverNotificationProvider.kt b/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/PushoverNotificationProvider.kt index 01c7acb5c..15756a1c5 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/PushoverNotificationProvider.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/notification/providers/PushoverNotificationProvider.kt @@ -16,35 +16,35 @@ import rocks.didit.sefilm.services.external.PushoverValidationStatus @Component @ConditionalOnProperty(prefix = "sefilm.notification.provider.Pushover", name = ["enabled"], matchIfMissing = true, havingValue = "true") class PushoverNotificationProvider( - private val pushoverService: PushoverService, - private val providerHelper: ProviderHelper) : NotificationProvider, ApplicationListener { + private val pushoverService: PushoverService, + private val providerHelper: ProviderHelper) : NotificationProvider, ApplicationListener { - private val log: Logger = LoggerFactory.getLogger(this.javaClass) + private val log: Logger = LoggerFactory.getLogger(this.javaClass) - @Async - override fun onApplicationEvent(event: NotificationEvent) { - if (pushoverService.isUsable()) { - sendNotification(event) + @Async + override fun onApplicationEvent(event: NotificationEvent) { + if (pushoverService.isUsable()) { + sendNotification(event) + } } - } - fun sendNotification(event: NotificationEvent) { - getNotifiableUsers(event.potentialRecipients).forEach { user -> - if (user.enabledTypes.contains(event.type)) { - val msg = providerHelper.constructMessageBasedOnEvent(event) + fun sendNotification(event: NotificationEvent) { + getNotifiableUsers(event.potentialRecipients).forEach { user -> + if (user.enabledTypes.contains(event.type)) { + val msg = providerHelper.constructMessageBasedOnEvent(event) - log.trace("About to send {} to {}", msg, user) - pushoverService.send(msg, user.notificationSettings.userKey, user.notificationSettings.device) - } + log.trace("About to send {} to {}", msg, user) + pushoverService.send(msg, user.notificationSettings.userKey, user.notificationSettings.device) + } + } } - } - override fun getNotifiableUsers(knownRecipients: List): List> { - return providerHelper - .getNotifiableUsers(knownRecipients, PushoverSettings::class) - .filter { it.notificationSettings.userKeyStatus == PushoverValidationStatus.USER_AND_DEVICE_VALID } - } + override fun getNotifiableUsers(knownRecipients: List): List> { + return providerHelper + .getNotifiableUsers(knownRecipients, PushoverSettings::class) + .filter { it.notificationSettings.userKeyStatus == PushoverValidationStatus.USER_AND_DEVICE_VALID } + } - override val name = "Pushover" - override val isSubscribable = true + override val name = "Pushover" + override val isSubscribable = true } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/AsyncMovieUpdater.kt b/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/AsyncMovieUpdater.kt index 8891ad756..6333890a7 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/AsyncMovieUpdater.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/AsyncMovieUpdater.kt @@ -13,208 +13,208 @@ import rocks.didit.sefilm.domain.IMDbID import rocks.didit.sefilm.domain.TMDbID import rocks.didit.sefilm.domain.dto.ImdbResult import rocks.didit.sefilm.domain.dto.TmdbMovieDetails -import rocks.didit.sefilm.services.external.SFService +import rocks.didit.sefilm.services.external.FilmstadenService import java.time.Duration import java.time.Instant import java.util.* @Component @ConditionalOnProperty( - prefix = "sefilm.schedulers.enabled", name = ["movieUpdater"], - matchIfMissing = true, - havingValue = "true" + prefix = "sefilm.schedulers.enabled", name = ["movieUpdater"], + matchIfMissing = true, + havingValue = "true" ) class AsyncMovieUpdater( - private val movieRepository: MovieRepository, - private val sfClient: SFService, - private val imdbClient: ImdbClient + private val movieRepository: MovieRepository, + private val filmstadenClient: FilmstadenService, + private val imdbClient: ImdbClient ) { - companion object { - private const val INITIAL_UPDATE_DELAY = 5 * 60 * 1000L - private const val UPDATE_INTERVAL = 43200000L // 12 hours - } - - private val log = LoggerFactory.getLogger(AsyncMovieUpdater::class.java) - - @Scheduled(initialDelay = INITIAL_UPDATE_DELAY, fixedDelay = UPDATE_INTERVAL) - fun scheduledMovieUpdates() { - val moviesThatRequiresUpdate = movieRepository - .findByArchivedOrderByPopularityDesc() - .filter(this::isUpdateRequired) - if (moviesThatRequiresUpdate.isNotEmpty()) { - log.info("Commencing scheduled update for ${moviesThatRequiresUpdate.count()} movies") - synchronousExtendMovieInfo(moviesThatRequiresUpdate) - } - } - - @Async - fun extendMovieInfo(movies: Iterable) { - synchronousExtendMovieInfo(movies) - } - - fun synchronousExtendMovieInfo(movies: Iterable) { - movies.forEach { - log.info("[MovieUpdater] Fetching extended info for ${it.log()}") - try { - updateInfo(it) - } catch (e: Exception) { - log.warn("[MovieUpdater] An error occurred when updating '${it.title}' ID=${it.id}, SF id=${it.sfId}", e) - } - randomBackoff() - } - } - - private fun randomBackoff() { - val waitTime = 3000L + Random().nextInt(7000) - try { - Thread.sleep(waitTime) - } catch (e: InterruptedException) { - log.info("randomBackoff were interrupted") - Thread.currentThread().interrupt() - } - } - - private fun isUpdateRequired(movie: Movie) = !movie.archived && (movie.needsMoreInfo() || movie.imdbId.isMissing()) - - private fun updateInfo(movie: Movie) { - if (movie.needsMoreInfo()) { - fetchExtendedInfoForMovie(movie) - } - if (movie.imdbId.isMissing()) { - updateImdbIdBasedOnTitle(movie) - } - } - - private fun fetchExtendedInfoForMovie(movie: Movie) { - when { - movie.sfId != null -> updateFromSf(movie) - movie.tmdbId.isSupplied() -> updateFromTmdbById(movie) - movie.imdbId.isSupplied() -> updateFromTmdbByImdbId(movie) - else -> updateFromImdbByTitle(movie) - } - } - - private fun updateFromSf(movie: Movie): Movie { - log.info("[SF] Fetching extended info from SF for ${movie.log()} and sfId: ${movie.sfId}") - val updatedMovie = sfClient.fetchExtendedInfo(movie.sfId!!) - if (updatedMovie == null) { - log.info("[SF] ${movie.log()} not found. Removing that SF id...") - return movieRepository.save(movie.copy(sfId = null)) - } - - val copy = movie.copy(synopsis = updatedMovie.shortDescription, - sfSlug = updatedMovie.slug, - originalTitle = updatedMovie.originalTitle, - releaseDate = updatedMovie.releaseDate ?: movie.releaseDate, - productionYear = updatedMovie.productionYear, - runtime = Duration.ofMinutes(updatedMovie.length ?: 0L), - poster = setWidthTo240(updatedMovie.posterUrl), - genres = updatedMovie.genres?.map { it.name } ?: listOf()) - - val saved = movieRepository.save(copy) - if (saved.needsMoreInfo()) { - log.info("[SF] Updated ${movie.log()} with SF data, but not all info were available") - } else { - log.info("[SF] Successfully updated ${movie.log()} with SF data") - } - return saved - } - - /** The width query parameter on the poster url sometimes causes 403 errors if too large. */ - private fun setWidthTo240(url: String?): String? { - if (url == null) return null - val toUriString = UriComponentsBuilder.fromUriString(url) - .replaceQueryParam("width", 240) - .toUriString() - return toUriString - } - - private fun updateFromTmdbById(movie: Movie) { - if (movie.tmdbId.isNotSupplied()) { - log.info("[TMDb] ${movie.log()} is missing an TMDb ID. TMDb ID state: ${movie.tmdbId.state}") - return - } - - log.info("[TMDb] Fetching movie details by TMDb ID=${movie.tmdbId}") - val movieDetails = imdbClient.movieDetailsExact(movie.tmdbId) - movieRepository.save(movie.copyDetails(movieDetails)) - } - - private fun updateFromTmdbByImdbId(movie: Movie) { - if (movie.imdbId.isNotSupplied()) { - log.info("[TMDb] The IMDb ID for ${movie.log()} is not supplied") - return - } - - log.info("[TMDb] Fetching movie details by IMDb ID=${movie.imdbId}") - val movieDetails = imdbClient.movieDetails(movie.imdbId) - val updatedMovie = movie.copyDetails(movieDetails) - movieRepository.save(updatedMovie) - } - - private fun updateFromImdbByTitle(movie: Movie) { - log.info("[IMDb] Update movie defaults for title=${movie.title}") - val imdbId = getFirstImdbIdMatchingTitle(movie.title, movie.productionYear) - - if (imdbId.isUnknown()) { - log.warn("[IMDb] Didn't find an IMDb matching title '${movie.title}'") - movieRepository.save(movie.copy(imdbId = imdbId)) - return - } - log.info("[IMDb] Using IMDb ID=$imdbId") - updateFromImdbByTitle(movie.copy(imdbId = imdbId)) - } - - /** Returns a Supplied IMDbID from the best match or IMDbID.UNKNOWN if nothing were found */ - private fun getFirstImdbIdMatchingTitle(title: String, year: Int?): IMDbID { - log.info("[IMDb] Fetching movie details by title='$title'") - val searchResults = imdbClient.search(title) - if (searchResults.isEmpty()) { - log.warn("[IMDb] No movies found matching '$title'. Nothing to update") - return IMDbID.UNKNOWN - } - var firstResult = searchResults.firstOrNull { - it.matchesTitleAndYear(title, year) - } - - if (firstResult == null) { - log.warn("[IMDb] No search result exactly matching '$title' and year=$year") - firstResult = searchResults[0] - } - log.info("[IMDb] Found ${searchResults.size} results matching $title. Choosing ${firstResult.l} (${firstResult.id})") - return IMDbID.valueOf(firstResult.id) - } - - private fun ImdbResult.matchesTitleAndYear(title: String, year: Int?): Boolean { - if (year == null) { - return this.l == title - } - return this.y == year && this.l == title - } - - private fun updateImdbIdBasedOnTitle(movie: Movie) { - log.info("[IMDb] Update IMDb ID based on title=${movie.title}") - val imdbId = getFirstImdbIdMatchingTitle(movie.title, movie.productionYear) - log.info("[IMDb] ${movie.title} ⟶ $imdbId") - val updatedMovie = movie.copy(imdbId = imdbId) - movieRepository.save(updatedMovie) - } - - private fun Movie.copyDetails(movieDetails: TmdbMovieDetails): Movie { - return this.copy( - title = movieDetails.title, - originalTitle = movieDetails.original_title, - synopsis = movieDetails.overview, - productionYear = movieDetails.release_date?.year, - genres = movieDetails.genres.map { it.name }, - poster = movieDetails.fullPosterPath(), - tmdbId = TMDbID.valueOf(movieDetails.id), - popularity = movieDetails.popularity, - popularityLastUpdated = Instant.now(), - imdbId = IMDbID.valueOf(movieDetails.imdb_id) - ) - } - - private fun Movie.log() = "'${this.title}' (${this.id})" + companion object { + private const val INITIAL_UPDATE_DELAY = 5 * 60 * 1000L + private const val UPDATE_INTERVAL = 43200000L // 12 hours + } + + private val log = LoggerFactory.getLogger(AsyncMovieUpdater::class.java) + + @Scheduled(initialDelay = INITIAL_UPDATE_DELAY, fixedDelay = UPDATE_INTERVAL) + fun scheduledMovieUpdates() { + val moviesThatRequiresUpdate = movieRepository + .findByArchivedOrderByPopularityDesc() + .filter(this::isUpdateRequired) + if (moviesThatRequiresUpdate.isNotEmpty()) { + log.info("Commencing scheduled update for ${moviesThatRequiresUpdate.count()} movies") + synchronousExtendMovieInfo(moviesThatRequiresUpdate) + } + } + + @Async + fun extendMovieInfo(movies: Iterable) { + synchronousExtendMovieInfo(movies) + } + + fun synchronousExtendMovieInfo(movies: Iterable) { + movies.forEach { + log.info("[MovieUpdater] Fetching extended info for ${it.log()}") + try { + updateInfo(it) + } catch (e: Exception) { + log.warn("[MovieUpdater] An error occurred when updating '${it.title}' ID=${it.id}, Filmstaden id=${it.filmstadenId}", e) + } + randomBackoff() + } + } + + private fun randomBackoff() { + val waitTime = 3000L + Random().nextInt(7000) + try { + Thread.sleep(waitTime) + } catch (e: InterruptedException) { + log.info("randomBackoff were interrupted") + Thread.currentThread().interrupt() + } + } + + private fun isUpdateRequired(movie: Movie) = !movie.archived && (movie.needsMoreInfo() || movie.imdbId.isMissing()) + + private fun updateInfo(movie: Movie) { + if (movie.needsMoreInfo()) { + fetchExtendedInfoForMovie(movie) + } + if (movie.imdbId.isMissing()) { + updateImdbIdBasedOnTitle(movie) + } + } + + private fun fetchExtendedInfoForMovie(movie: Movie) { + when { + movie.filmstadenId != null -> updateFromFilmstaden(movie) + movie.tmdbId.isSupplied() -> updateFromTmdbById(movie) + movie.imdbId.isSupplied() -> updateFromTmdbByImdbId(movie) + else -> updateFromImdbByTitle(movie) + } + } + + private fun updateFromFilmstaden(movie: Movie): Movie { + log.info("[Filmstaden] Fetching extended info from Filmstaden for ${movie.log()} and filmstadenId: ${movie.filmstadenId}") + val updatedMovie = filmstadenClient.fetchExtendedInfo(movie.filmstadenId!!) + if (updatedMovie == null) { + log.info("[Filmstaden] ${movie.log()} not found. Removing that Filmstaden id...") + return movieRepository.save(movie.copy(filmstadenId = null)) + } + + val copy = movie.copy(synopsis = updatedMovie.shortDescription, + filmstadenSlug = updatedMovie.slug, + originalTitle = updatedMovie.originalTitle, + releaseDate = updatedMovie.releaseDate ?: movie.releaseDate, + productionYear = updatedMovie.productionYear, + runtime = Duration.ofMinutes(updatedMovie.length ?: 0L), + poster = setWidthTo240(updatedMovie.posterUrl), + genres = updatedMovie.genres?.map { it.name } ?: listOf()) + + val saved = movieRepository.save(copy) + if (saved.needsMoreInfo()) { + log.info("[Filmstaden] Updated ${movie.log()} with Filmstaden data, but not all info were available") + } else { + log.info("[Filmstaden] Successfully updated ${movie.log()} with Filmstaden data") + } + return saved + } + + /** The width query parameter on the poster url sometimes causes 403 errors if too large. */ + private fun setWidthTo240(url: String?): String? { + if (url == null) return null + val toUriString = UriComponentsBuilder.fromUriString(url) + .replaceQueryParam("width", 240) + .toUriString() + return toUriString + } + + private fun updateFromTmdbById(movie: Movie) { + if (movie.tmdbId.isNotSupplied()) { + log.info("[TMDb] ${movie.log()} is missing an TMDb ID. TMDb ID state: ${movie.tmdbId.state}") + return + } + + log.info("[TMDb] Fetching movie details by TMDb ID=${movie.tmdbId}") + val movieDetails = imdbClient.movieDetailsExact(movie.tmdbId) + movieRepository.save(movie.copyDetails(movieDetails)) + } + + private fun updateFromTmdbByImdbId(movie: Movie) { + if (movie.imdbId.isNotSupplied()) { + log.info("[TMDb] The IMDb ID for ${movie.log()} is not supplied") + return + } + + log.info("[TMDb] Fetching movie details by IMDb ID=${movie.imdbId}") + val movieDetails = imdbClient.movieDetails(movie.imdbId) + val updatedMovie = movie.copyDetails(movieDetails) + movieRepository.save(updatedMovie) + } + + private fun updateFromImdbByTitle(movie: Movie) { + log.info("[IMDb] Update movie defaults for title=${movie.title}") + val imdbId = getFirstImdbIdMatchingTitle(movie.title, movie.productionYear) + + if (imdbId.isUnknown()) { + log.warn("[IMDb] Didn't find an IMDb matching title '${movie.title}'") + movieRepository.save(movie.copy(imdbId = imdbId)) + return + } + log.info("[IMDb] Using IMDb ID=$imdbId") + updateFromImdbByTitle(movie.copy(imdbId = imdbId)) + } + + /** Returns a Supplied IMDbID from the best match or IMDbID.UNKNOWN if nothing were found */ + private fun getFirstImdbIdMatchingTitle(title: String, year: Int?): IMDbID { + log.info("[IMDb] Fetching movie details by title='$title'") + val searchResults = imdbClient.search(title) + if (searchResults.isEmpty()) { + log.warn("[IMDb] No movies found matching '$title'. Nothing to update") + return IMDbID.UNKNOWN + } + var firstResult = searchResults.firstOrNull { + it.matchesTitleAndYear(title, year) + } + + if (firstResult == null) { + log.warn("[IMDb] No search result exactly matching '$title' and year=$year") + firstResult = searchResults[0] + } + log.info("[IMDb] Found ${searchResults.size} results matching $title. Choosing ${firstResult.l} (${firstResult.id})") + return IMDbID.valueOf(firstResult.id) + } + + private fun ImdbResult.matchesTitleAndYear(title: String, year: Int?): Boolean { + if (year == null) { + return this.l == title + } + return this.y == year && this.l == title + } + + private fun updateImdbIdBasedOnTitle(movie: Movie) { + log.info("[IMDb] Update IMDb ID based on title=${movie.title}") + val imdbId = getFirstImdbIdMatchingTitle(movie.title, movie.productionYear) + log.info("[IMDb] ${movie.title} ⟶ $imdbId") + val updatedMovie = movie.copy(imdbId = imdbId) + movieRepository.save(updatedMovie) + } + + private fun Movie.copyDetails(movieDetails: TmdbMovieDetails): Movie { + return this.copy( + title = movieDetails.title, + originalTitle = movieDetails.original_title, + synopsis = movieDetails.overview, + productionYear = movieDetails.release_date?.year, + genres = movieDetails.genres.map { it.name }, + poster = movieDetails.fullPosterPath(), + tmdbId = TMDbID.valueOf(movieDetails.id), + popularity = movieDetails.popularity, + popularityLastUpdated = Instant.now(), + imdbId = IMDbID.valueOf(movieDetails.imdb_id) + ) + } + + private fun Movie.log() = "'${this.title}' (${this.id})" } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/ScheduledArchiver.kt b/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/ScheduledArchiver.kt index b8469ab99..7d8430d50 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/ScheduledArchiver.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/ScheduledArchiver.kt @@ -7,92 +7,92 @@ import org.springframework.stereotype.Component import rocks.didit.sefilm.database.entities.Movie import rocks.didit.sefilm.database.repositories.MovieRepository import rocks.didit.sefilm.database.repositories.ShowingRepository -import rocks.didit.sefilm.services.external.SFService +import rocks.didit.sefilm.services.external.FilmstadenService import java.time.Duration import java.time.LocalDate import java.time.LocalDateTime @Component @ConditionalOnProperty( - prefix = "sefilm.schedulers.enabled", name = ["archiver"], - matchIfMissing = true, - havingValue = "true" + prefix = "sefilm.schedulers.enabled", name = ["archiver"], + matchIfMissing = true, + havingValue = "true" ) class ScheduledArchiver( - private val movieRepository: MovieRepository, - showingRepository: ShowingRepository, - sfClient: SFService + private val movieRepository: MovieRepository, + showingRepository: ShowingRepository, + filmstadenClient: FilmstadenService ) { - companion object { - private const val INITIAL_UPDATE_DELAY = 60 * 60 * 1000L // 1 hour - private const val UPDATE_INTERVAL = 604800000L // 1 week - } + companion object { + private const val INITIAL_UPDATE_DELAY = 60 * 60 * 1000L // 1 hour + private const val UPDATE_INTERVAL = 604800000L // 1 week + } + + private val log = LoggerFactory.getLogger(ScheduledArchiver::class.java) + private val archiveRules: List = listOf(ReleaseDateAndShowingsRule(showingRepository, filmstadenClient)) - private val log = LoggerFactory.getLogger(ScheduledArchiver::class.java) - private val archiveRules: List = listOf(ReleaseDateAndShowingsRule(showingRepository, sfClient)) + @Scheduled(initialDelay = INITIAL_UPDATE_DELAY, fixedDelay = UPDATE_INTERVAL) + fun scheduledArchivations() { + val archivableMovies = movieRepository.findAll() + .filter { it.isScheduledForArchivation() } + log.info("[Archiver] Found ${archivableMovies.size} movies scheduled for archivation") - @Scheduled(initialDelay = INITIAL_UPDATE_DELAY, fixedDelay = UPDATE_INTERVAL) - fun scheduledArchivations() { - val archivableMovies = movieRepository.findAll() - .filter { it.isScheduledForArchivation() } - log.info("[Archiver] Found ${archivableMovies.size} movies scheduled for archivation") + archiveMovies(archivableMovies) + } - archiveMovies(archivableMovies) - } + private fun archiveMovies(movies: List) { + val prettyPrintMovies = movies.map { "{title: ${it.title}, id: ${it.id}}" } + log.debug("[Archiver] Will archive the following movies: $prettyPrintMovies") - private fun archiveMovies(movies: List) { - val prettyPrintMovies = movies.map { "{title: ${it.title}, id: ${it.id}}" } - log.debug("[Archiver] Will archive the following movies: $prettyPrintMovies") + val archivedMovies = movies.map { + it.copy(archived = true) + } + movieRepository.saveAll(archivedMovies) + } - val archivedMovies = movies.map { - it.copy(archived = true) + private fun Movie.isScheduledForArchivation(): Boolean { + return archiveRules + .map { it.isEligibleForArchivation(this) } + .all { it } } - movieRepository.saveAll(archivedMovies) - } - - private fun Movie.isScheduledForArchivation(): Boolean { - return archiveRules - .map { it.isEligibleForArchivation(this) } - .all { it } - } } private class ReleaseDateAndShowingsRule( - private val showingRepository: ShowingRepository, - private val sfClient: SFService + private val showingRepository: ShowingRepository, + private val filmstadenClient: FilmstadenService ) : ArchiveRule { - override fun isEligibleForArchivation(movie: Movie): Boolean { - if (movie.isOlderThan(Duration.ofDays(65))) { - val hasActiveShowings = movie.hasActiveShowings() - if (!hasActiveShowings) { - return !movie.hasActiveShowingsOnSF() - } - return !hasActiveShowings + override fun isEligibleForArchivation(movie: Movie): Boolean { + if (movie.isOlderThan(Duration.ofDays(65))) { + val hasActiveShowings = movie.hasActiveShowings() + if (!hasActiveShowings) { + return !movie.hasActiveShowingsOnFilmstaden() + } + return !hasActiveShowings + } + return false } - return false - } - private fun Movie.hasActiveShowings(): Boolean { - val showingsForMovie = showingRepository.findByMovieIdOrderByDateDesc(this.id) - return showingsForMovie.any { - it.date?.isAfter(LocalDate.now()) ?: false + private fun Movie.hasActiveShowings(): Boolean { + val showingsForMovie = showingRepository.findByMovieIdOrderByDateDesc(this.id) + return showingsForMovie.any { + it.date?.isAfter(LocalDate.now()) ?: false + } } - } - private fun Movie.isOlderThan(maxAge: Duration) = - Duration.between(this.releaseDate.atTime(0, 0), LocalDateTime.now()) > maxAge + private fun Movie.isOlderThan(maxAge: Duration) = + Duration.between(this.releaseDate.atTime(0, 0), LocalDateTime.now()) > maxAge - private fun Movie.hasActiveShowingsOnSF(): Boolean { - if (this.sfId == null) { - return false + private fun Movie.hasActiveShowingsOnFilmstaden(): Boolean { + if (this.filmstadenId == null) { + return false + } + return filmstadenClient.getShowingDates(this.filmstadenId).isNotEmpty() } - return sfClient.getShowingDates(this.sfId).isNotEmpty() - } } private interface ArchiveRule { - fun isEligibleForArchivation(movie: Movie): Boolean + fun isEligibleForArchivation(movie: Movie): Boolean } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/ScheduledPopularityUpdater.kt b/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/ScheduledPopularityUpdater.kt index 95b11e277..8809f1ed6 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/ScheduledPopularityUpdater.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/schedulers/ScheduledPopularityUpdater.kt @@ -11,7 +11,7 @@ import rocks.didit.sefilm.database.repositories.MovieRepository import rocks.didit.sefilm.domain.IMDbID import rocks.didit.sefilm.domain.TMDbID import rocks.didit.sefilm.domain.dto.TmdbMovieDetails -import rocks.didit.sefilm.services.external.SFService +import rocks.didit.sefilm.services.external.FilmstadenService import rocks.didit.sefilm.toImdbId import rocks.didit.sefilm.toTmdbId import java.time.Instant @@ -21,138 +21,138 @@ import java.util.* @Component @ConditionalOnProperty( - prefix = "sefilm.schedulers.enabled", name = ["popularityUpdater"], - matchIfMissing = true, - havingValue = "true" + prefix = "sefilm.schedulers.enabled", name = ["popularityUpdater"], + matchIfMissing = true, + havingValue = "true" ) class ScheduledPopularityUpdater( - private val movieRepository: MovieRepository, - private val sfService: SFService, - private val imdbClient: ImdbClient + private val movieRepository: MovieRepository, + private val filmstadenService: FilmstadenService, + private val imdbClient: ImdbClient ) { - companion object { - private const val INITIAL_UPDATE_DELAY = 10L * 60 * 1000L // 10min - private const val UPDATE_INTERVAL = 4 * 60 * 60 * 1000L // 4 hours + companion object { + private const val INITIAL_UPDATE_DELAY = 10L * 60 * 1000L // 10min + private const val UPDATE_INTERVAL = 4 * 60 * 60 * 1000L // 4 hours - private const val HAS_SF_SHOWINGS_POPULARITY = 500.0 - } - - private val log = LoggerFactory.getLogger(ScheduledPopularityUpdater::class.java) - - @Scheduled(initialDelay = INITIAL_UPDATE_DELAY, fixedDelay = UPDATE_INTERVAL) - fun scheduledMovieUpdates() { - val moviesWithOldPopularity = movieRepository - .findByArchivedOrderByPopularityDesc() - .filter(Movie::isPopularityOutdated) - if (moviesWithOldPopularity.isNotEmpty()) { - log.info("[Schedule] Updating popularity for ${moviesWithOldPopularity.count()} movies") - updatePopularitys(moviesWithOldPopularity) + private const val HAS_FILMSTADEN_SHOWINGS_POPULARITY = 500.0 } - } - - fun updatePopularitys(movies: Iterable) { - movies.forEach { - log.info("[Popularity] Updating popularity for '${it.title}' with id=${it.id}") - try { - updatePopularity(it) - } catch (e: ExternalProviderException) { - log.warn("[Popularity] Provider error: " + e.message) - rescheduleNextPopularityUpdate(movie = it) - } catch (e: Exception) { - log.warn("[Popularity] An error occurred when updating popularity for '${it.title}' ID=${it.id}", e) - } - randomBackoff() + + private val log = LoggerFactory.getLogger(ScheduledPopularityUpdater::class.java) + + @Scheduled(initialDelay = INITIAL_UPDATE_DELAY, fixedDelay = UPDATE_INTERVAL) + fun scheduledMovieUpdates() { + val moviesWithOldPopularity = movieRepository + .findByArchivedOrderByPopularityDesc() + .filter(Movie::isPopularityOutdated) + if (moviesWithOldPopularity.isNotEmpty()) { + log.info("[Schedule] Updating popularity for ${moviesWithOldPopularity.count()} movies") + updatePopularitys(moviesWithOldPopularity) + } } - } - - private fun randomBackoff() { - val waitTime = 3000L + Random().nextInt(10000) - try { - Thread.sleep(waitTime) - } catch (e: InterruptedException) { - log.info("[Popularity] randomBackoff were interrupted") - Thread.currentThread().interrupt() + + fun updatePopularitys(movies: Iterable) { + movies.forEach { + log.info("[Popularity] Updating popularity for '${it.title}' with id=${it.id}") + try { + updatePopularity(it) + } catch (e: ExternalProviderException) { + log.warn("[Popularity] Provider error: " + e.message) + rescheduleNextPopularityUpdate(movie = it) + } catch (e: Exception) { + log.warn("[Popularity] An error occurred when updating popularity for '${it.title}' ID=${it.id}", e) + } + randomBackoff() + } } - } - private fun updatePopularity(movie: Movie) { - val popularityAndId = when { - movie.tmdbId.isSupplied() -> fetchPopularityByTmdbId(movie) - movie.imdbId.isSupplied() -> fetchPopularityByImdbId(movie) - else -> fetchPopularityByTitle(movie) + private fun randomBackoff() { + val waitTime = 3000L + Random().nextInt(10000) + try { + Thread.sleep(waitTime) + } catch (e: InterruptedException) { + log.info("[Popularity] randomBackoff were interrupted") + Thread.currentThread().interrupt() + } } - if (popularityAndId == null) { - rescheduleNextPopularityUpdate(movie = movie) - return + private fun updatePopularity(movie: Movie) { + val popularityAndId = when { + movie.tmdbId.isSupplied() -> fetchPopularityByTmdbId(movie) + movie.imdbId.isSupplied() -> fetchPopularityByImdbId(movie) + else -> fetchPopularityByTitle(movie) + } + + if (popularityAndId == null) { + rescheduleNextPopularityUpdate(movie = movie) + return + } + + val newPopularity = when (movie.hasFutureFilmstadenShowings()) { + true -> { + log.info("[Popularity] ${movie.title} has future showings at Filmstaden, increasing popularity by $HAS_FILMSTADEN_SHOWINGS_POPULARITY") + popularityAndId.popularity + HAS_FILMSTADEN_SHOWINGS_POPULARITY + } + false -> popularityAndId.popularity + } + + val updatedMovie = movie.copy( + popularity = newPopularity, + popularityLastUpdated = Instant.now(), + tmdbId = popularityAndId.tmdbId, + imdbId = popularityAndId.imdbId + ) + log.info("[Popularity] Popularity updated from ${movie.popularity} → ${updatedMovie.popularity} for '${movie.title}'") + movieRepository.save(updatedMovie) } - val newPopularity = when (movie.hasFutureSfShowings()) { - true -> { - log.info("[Popularity] ${movie.title} has future showings at SF, increasing popularity by $HAS_SF_SHOWINGS_POPULARITY") - popularityAndId.popularity + HAS_SF_SHOWINGS_POPULARITY - } - false -> popularityAndId.popularity + private fun Movie.hasFutureFilmstadenShowings(): Boolean { + if (this.filmstadenId == null || this.filmstadenId.isBlank()) return false + + return filmstadenService.getShowingDates(this.filmstadenId).isNotEmpty() } - val updatedMovie = movie.copy( - popularity = newPopularity, - popularityLastUpdated = Instant.now(), - tmdbId = popularityAndId.tmdbId, - imdbId = popularityAndId.imdbId - ) - log.info("[Popularity] Popularity updated from ${movie.popularity} → ${updatedMovie.popularity} for '${movie.title}'") - movieRepository.save(updatedMovie) - } - - private fun Movie.hasFutureSfShowings(): Boolean { - if (this.sfId == null || this.sfId.isBlank()) return false - - return sfService.getShowingDates(this.sfId).isNotEmpty() - } - - private fun rescheduleNextPopularityUpdate(weeks: Long = 4, movie: Movie) { - log.warn("[Popularity] No info found for movie with ${movie.title} (${movie.id}). Next check in approximately $weeks weeks") - val updatedMovie = - movie.copy(popularityLastUpdated = LocalDateTime.now().plusWeeks(weeks).toInstant(ZoneOffset.UTC)) - movieRepository.save(updatedMovie) - } - - private fun fetchPopularityByTmdbId(movie: Movie): PopularityAndId? { - if (movie.tmdbId.isNotSupplied()) { - log.warn("[TMDb][Popularity] Movie[${movie.id} is missing an TMDb id") - return null + private fun rescheduleNextPopularityUpdate(weeks: Long = 4, movie: Movie) { + log.warn("[Popularity] No info found for movie with ${movie.title} (${movie.id}). Next check in approximately $weeks weeks") + val updatedMovie = + movie.copy(popularityLastUpdated = LocalDateTime.now().plusWeeks(weeks).toInstant(ZoneOffset.UTC)) + movieRepository.save(updatedMovie) } - val movieDetails = imdbClient.movieDetailsExact(movie.tmdbId) - return PopularityAndId(movieDetails.popularity, movieDetails.id.toTmdbId(), movieDetails.imdb_id.toImdbId()) - } + private fun fetchPopularityByTmdbId(movie: Movie): PopularityAndId? { + if (movie.tmdbId.isNotSupplied()) { + log.warn("[TMDb][Popularity] Movie[${movie.id} is missing an TMDb id") + return null + } - private fun fetchPopularityByImdbId(movie: Movie): PopularityAndId? { - if (movie.imdbId.isNotSupplied()) { - log.warn("[IMDb][Popularity] Movie[${movie.id} is missing an IMDb id") - return null + val movieDetails = imdbClient.movieDetailsExact(movie.tmdbId) + return PopularityAndId(movieDetails.popularity, movieDetails.id.toTmdbId(), movieDetails.imdb_id.toImdbId()) } - val movieDetails: TmdbMovieDetails - try { - movieDetails = imdbClient.movieDetails(movie.imdbId) - } catch (e: ExternalProviderException) { - return null + private fun fetchPopularityByImdbId(movie: Movie): PopularityAndId? { + if (movie.imdbId.isNotSupplied()) { + log.warn("[IMDb][Popularity] Movie[${movie.id} is missing an IMDb id") + return null + } + + val movieDetails: TmdbMovieDetails + try { + movieDetails = imdbClient.movieDetails(movie.imdbId) + } catch (e: ExternalProviderException) { + return null + } + return PopularityAndId(movieDetails.popularity, movieDetails.id.toTmdbId(), movieDetails.imdb_id.toImdbId()) } - return PopularityAndId(movieDetails.popularity, movieDetails.id.toTmdbId(), movieDetails.imdb_id.toImdbId()) - } - private fun fetchPopularityByTitle(movie: Movie): PopularityAndId? { - val title = movie.originalTitle ?: movie.title - val movieResults = imdbClient.search(title) + private fun fetchPopularityByTitle(movie: Movie): PopularityAndId? { + val title = movie.originalTitle ?: movie.title + val movieResults = imdbClient.search(title) - if (movieResults.isEmpty()) return null + if (movieResults.isEmpty()) return null - val movieDetails = imdbClient.movieDetails(movieResults[0].id.toImdbId()) - return PopularityAndId(movieDetails.popularity, movieDetails.id.toTmdbId(), movieDetails.imdb_id.toImdbId()) - } + val movieDetails = imdbClient.movieDetails(movieResults[0].id.toImdbId()) + return PopularityAndId(movieDetails.popularity, movieDetails.id.toTmdbId(), movieDetails.imdb_id.toImdbId()) + } - private data class PopularityAndId(val popularity: Double, val tmdbId: TMDbID, val imdbId: IMDbID = IMDbID.MISSING) + private data class PopularityAndId(val popularity: Double, val tmdbId: TMDbID, val imdbId: IMDbID = IMDbID.MISSING) } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/AdminService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/AdminService.kt index fd381ffbc..837e4670c 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/AdminService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/AdminService.kt @@ -9,20 +9,20 @@ import java.util.* @Service class AdminService( - private val showingRepository: ShowingRepository, - private val userService: UserService, - private val assertionService: AssertionService) { + private val showingRepository: ShowingRepository, + private val userService: UserService, + private val assertionService: AssertionService) { - fun promoteToAdmin(showingId: UUID, userIdToPromote: UserID): ShowingDTO { - val showing = showingRepository.findById(showingId) - .orElseThrow { NotFoundException(what = "showing", showingId = showingId) } - assertionService.assertLoggedInUserIsAdmin(showing.toDto()) - val userToPromote = userService.getCompleteUser(userIdToPromote) + fun promoteToAdmin(showingId: UUID, userIdToPromote: UserID): ShowingDTO { + val showing = showingRepository.findById(showingId) + .orElseThrow { NotFoundException(what = "showing", showingId = showingId) } + assertionService.assertLoggedInUserIsAdmin(showing.toDto()) + val userToPromote = userService.getCompleteUser(userIdToPromote) - val updatedShowing = showing.copy( - admin = userToPromote, - payToUser = userToPromote - ) - return showingRepository.save(updatedShowing).toDto() - } + val updatedShowing = showing.copy( + admin = userToPromote, + payToUser = userToPromote + ) + return showingRepository.save(updatedShowing).toDto() + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/AssertionService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/AssertionService.kt index 1d577214a..12e2aafaa 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/AssertionService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/AssertionService.kt @@ -11,54 +11,54 @@ import rocks.didit.sefilm.domain.dto.ShowingDTO @Service class AssertionService( - private val userService: UserService, - private val foretagsbiljettService: ForetagsbiljettService + private val userService: UserService, + private val foretagsbiljettService: ForetagsbiljettService ) { - fun assertTicketsNotBought(userID: UserID, showing: Showing) { - if (showing.ticketsBought) { - throw TicketsAlreadyBoughtException(userID, showing.id) + fun assertTicketsNotBought(userID: UserID, showing: Showing) { + if (showing.ticketsBought) { + throw TicketsAlreadyBoughtException(userID, showing.id) + } } - } - fun assertUserNotAlreadyAttended(userID: UserID, showing: Showing) { - if (showing.participants.any { it.userId == userID }) { - throw UserAlreadyAttendedException(userID) + fun assertUserNotAlreadyAttended(userID: UserID, showing: Showing) { + if (showing.participants.any { it.userId == userID }) { + throw UserAlreadyAttendedException(userID) + } } - } - fun assertLoggedInUserIsAdmin(showing: ShowingDTO) = assertLoggedInUserIsAdmin(showing.admin) - fun assertLoggedInUserIsAdmin(showingAdmin: UserID) { - if (currentLoggedInUser() != showingAdmin) { - throw AccessDeniedException("Only the showing admin is allowed to do that") + fun assertLoggedInUserIsAdmin(showing: ShowingDTO) = assertLoggedInUserIsAdmin(showing.admin) + fun assertLoggedInUserIsAdmin(showingAdmin: UserID) { + if (currentLoggedInUserId() != showingAdmin) { + throw AccessDeniedException("Only the showing admin is allowed to do that") + } } - } - fun assertUserHasPhoneNumber(userID: UserID) { - val user = userService.getUserOrThrow(userID) - if (user.phone == null || user.phone.isBlank()) { - throw MissingPhoneNumberException(userID) + fun assertUserHasPhoneNumber(userID: UserID) { + val user = userService.getUserOrThrow(userID) + if (user.phone == null || user.phone.isBlank()) { + throw MissingPhoneNumberException(userID) + } } - } - fun assertForetagsbiljettIsUsable(userId: UserID, suppliedTicket: TicketNumber, showing: Showing) { - val matchingTickets = foretagsbiljettService - .getForetagsbiljetterForUser(userId) - .filter { it.number == suppliedTicket } + fun assertForetagsbiljettIsUsable(userId: UserID, suppliedTicket: TicketNumber, showing: Showing) { + val matchingTickets = foretagsbiljettService + .getForetagsbiljetterForUser(userId) + .filter { it.number == suppliedTicket } - if (matchingTickets.isEmpty()) { - throw TicketNotFoundException(suppliedTicket) - } - if (matchingTickets.size > 1) { - throw DuplicateTicketException(": $suppliedTicket") - } + if (matchingTickets.isEmpty()) { + throw TicketNotFoundException(suppliedTicket) + } + if (matchingTickets.size > 1) { + throw DuplicateTicketException(": $suppliedTicket") + } - if (foretagsbiljettService.getStatusOfTicket(matchingTickets.first()) != Foretagsbiljett.Status.Available) { - throw TicketAlreadyUsedException(suppliedTicket) - } + if (foretagsbiljettService.getStatusOfTicket(matchingTickets.first()) != Foretagsbiljett.Status.Available) { + throw TicketAlreadyUsedException(suppliedTicket) + } - if (matchingTickets.first().expires.isBefore(showing.expectedBuyDate ?: showing.date)) { - throw TicketExpiredException(suppliedTicket) + if (matchingTickets.first().expires.isBefore(showing.expectedBuyDate ?: showing.date)) { + throw TicketExpiredException(suppliedTicket) + } } - } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/BudordService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/BudordService.kt index 722f9bf7a..a15848076 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/BudordService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/BudordService.kt @@ -8,16 +8,16 @@ import java.security.SecureRandom @Service class BudordService(private val budordRepo: BudordRepository) { - fun getAll() = budordRepo.findAll().toList() + fun getAll() = budordRepo.findAll().toList() - fun getRandom(): BioBudord { - val all = budordRepo.findAll() - val count = all.count() - 1 - if (count <= 0) { - throw NotFoundException("any budord :(") - } + fun getRandom(): BioBudord { + val all = budordRepo.findAll() + val count = all.count() - 1 + if (count <= 0) { + throw NotFoundException("any budord :(") + } - val randomIndex = SecureRandom().nextInt(count) - return all.elementAt(randomIndex) - } + val randomIndex = SecureRandom().nextInt(count) + return all.elementAt(randomIndex) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/CalendarService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/CalendarService.kt index da2e41529..8f9b79fe2 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/CalendarService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/CalendarService.kt @@ -22,115 +22,115 @@ import java.util.* @Service class CalendarService( - private val userService: UserService, - private val showingService: ShowingService, - private val movieService: MovieService, - private val properties: Properties + private val userService: UserService, + private val showingService: ShowingService, + private val movieService: MovieService, + private val properties: Properties ) { - private val calendarDescription: String = "Dina visningar på $CALENDAR_NAME (${properties.baseUrl.frontend})" - - companion object { - private const val CALENDAR_NAME = "ITBio" - private val stockholmZoneId = TimeZone.getTimeZone("Europe/Stockholm").toZoneId() - - private val log: Logger = LoggerFactory.getLogger(CalendarService::class.java) - } - - fun getCalendarFeed(userFeedId: UUID): ICalendar { - val user = userService.lookupUserFromCalendarFeedId(userFeedId) ?: return setupCalendar(userFeedId) - - val cal = setupCalendar(userFeedId) - showingService - .getShowingByUser(user.id) - .map { it.toVEvent(user) } - .forEach { cal.addEvent(it) } - - return cal - } - - private fun setupCalendar(id: UUID): ICalendar { - val calendar = ICalendar() - calendar.setUid(id.toString()) - calendar.setExperimentalProperty("X-WR-RECALID", id.toString()) - calendar.setMethod("PUBLISH") - calendar.productId = ProductId("-//cthdidIT//itbio//EN") - calendar.calendarScale = CalendarScale.gregorian() - calendar.addName(CALENDAR_NAME) - calendar.addExperimentalProperty("X-WR-CALNAME", CALENDAR_NAME) - calendar.addExperimentalProperty("X-WR-CALDESC", calendarDescription) - calendar.addDescription(calendarDescription) - - return calendar - } - - private fun ShowingDTO.toVEvent(user: UserDTO): VEvent { - val movie = movieService.getMovie(this.movieId) ?: return VEvent() - val showingUrl = "${properties.baseUrl.frontend}/showings/$id" - - val vEvent = VEvent() - vEvent.setSummary(movie.title).language = "en-us" - vEvent.setDateStart(Date.from(this.getStartDate())) - vEvent.setDateEnd(this.getEndDate(movie)) - vEvent.setUid(this.id.toString()) - vEvent.setLocation(this.location.formatAddress()) - vEvent.setDescription(formatDescription(this.id, user.id, movie) + "\n\n$showingUrl") - vEvent.setUrl(showingUrl) - vEvent.addCategories(Categories("bio")) - vEvent.addParticipants(this, user.email) - vEvent.setOrganizer(user.email) - vEvent.status = if (this.ticketsBought) Status.confirmed() else Status.tentative() - vEvent.created = Created(Date.from(this.createdDate)) - vEvent.lastModified = LastModified(Date.from(this.lastModifiedDate)) - vEvent.transparency = Transparency.opaque() - - return vEvent - } - - private fun formatDescription(showingId: UUID, userId: UserID, movie: Movie): String { - val paymentDetails = showingService.getAttendeePaymentDetailsForUser(userId, showingId) - - return if (paymentDetails == null || paymentDetails.hasPaid) { - "Kolla på bio!\n${if (movie.imdbId.isSupplied()) "http://www.imdb.com/title/${movie.imdbId.value}/" else ""}" - } else { - val phoneNumber = userService.getUser(paymentDetails.payTo)?.phone - "Betala ${paymentDetails.amountOwed.toKronor()} kr till $phoneNumber" + private val calendarDescription: String = "Dina visningar på $CALENDAR_NAME (${properties.baseUrl.frontend})" + + companion object { + private const val CALENDAR_NAME = "ITBio" + private val stockholmZoneId = TimeZone.getTimeZone("Europe/Stockholm").toZoneId() + + private val log: Logger = LoggerFactory.getLogger(CalendarService::class.java) + } + + fun getCalendarFeed(userFeedId: UUID): ICalendar { + val user = userService.lookupUserFromCalendarFeedId(userFeedId) ?: return setupCalendar(userFeedId) + + val cal = setupCalendar(userFeedId) + showingService + .getShowingByUser(user.id) + .map { it.toVEvent(user) } + .forEach { cal.addEvent(it) } + + return cal + } + + private fun setupCalendar(id: UUID): ICalendar { + val calendar = ICalendar() + calendar.setUid(id.toString()) + calendar.setExperimentalProperty("X-WR-RECALID", id.toString()) + calendar.setMethod("PUBLISH") + calendar.productId = ProductId("-//cthdidIT//itbio//EN") + calendar.calendarScale = CalendarScale.gregorian() + calendar.addName(CALENDAR_NAME) + calendar.addExperimentalProperty("X-WR-CALNAME", CALENDAR_NAME) + calendar.addExperimentalProperty("X-WR-CALDESC", calendarDescription) + calendar.addDescription(calendarDescription) + + return calendar + } + + private fun ShowingDTO.toVEvent(user: UserDTO): VEvent { + val movie = movieService.getMovie(this.movieId) ?: return VEvent() + val showingUrl = "${properties.baseUrl.frontend}/showings/$id" + + val vEvent = VEvent() + vEvent.setSummary(movie.title).language = "en-us" + vEvent.setDateStart(Date.from(this.getStartDate())) + vEvent.setDateEnd(this.getEndDate(movie)) + vEvent.setUid(this.id.toString()) + vEvent.setLocation(this.location.formatAddress()) + vEvent.setDescription(formatDescription(this.id, user.id, movie) + "\n\n$showingUrl") + vEvent.setUrl(showingUrl) + vEvent.addCategories(Categories("bio")) + vEvent.addParticipants(this, user.email) + vEvent.setOrganizer(user.email) + vEvent.status = if (this.ticketsBought) Status.confirmed() else Status.tentative() + vEvent.created = Created(Date.from(this.createdDate)) + vEvent.lastModified = LastModified(Date.from(this.lastModifiedDate)) + vEvent.transparency = Transparency.opaque() + + return vEvent } - } - - private fun VEvent.addParticipants(showingDTO: ShowingDTO, mail: String) { - showingDTO.participants.forEach { - val user = userService.getUser(it.userId) - if (user != null) { - val attendee = Attendee("${user.firstName} '${user.nick}' ${user.lastName}", mail) - attendee.calendarUserType = CalendarUserType.INDIVIDUAL - attendee.participationStatus = ParticipationStatus.ACCEPTED - attendee.role = Role.ATTENDEE - attendee.participationLevel = ParticipationLevel.REQUIRED - - this.addAttendee( - attendee - ) - } + + private fun formatDescription(showingId: UUID, userId: UserID, movie: Movie): String { + val paymentDetails = showingService.getAttendeePaymentDetailsForUser(userId, showingId) + + return if (paymentDetails == null || paymentDetails.hasPaid) { + "Kolla på bio!\n${if (movie.imdbId.isSupplied()) "http://www.imdb.com/title/${movie.imdbId.value}/" else ""}" + } else { + val phoneNumber = userService.getUser(paymentDetails.payTo)?.phone + "Betala ${paymentDetails.amountOwed.toKronor()} kr till $phoneNumber" + } + } + + private fun VEvent.addParticipants(showingDTO: ShowingDTO, mail: String) { + showingDTO.participants.forEach { + val user = userService.getUser(it.userId) + if (user != null) { + val attendee = Attendee("${user.firstName} '${user.nick}' ${user.lastName}", mail) + attendee.calendarUserType = CalendarUserType.INDIVIDUAL + attendee.participationStatus = ParticipationStatus.ACCEPTED + attendee.role = Role.ATTENDEE + attendee.participationLevel = ParticipationLevel.REQUIRED + + this.addAttendee( + attendee + ) + } + } + } + + private fun ShowingDTO.getStartDate(): Instant { + return LocalDateTime.of(this.date, this.time) + .atZone(stockholmZoneId) + .toInstant() + } + + private fun ShowingDTO.getEndDate(movie: Movie): Date { + val end = this.getStartDate() + .plusMillis(movie.getDurationOrDefault2hours().toMillis()) + return Date.from(end) + } + + private fun Movie.getDurationOrDefault2hours() = when { + this.runtime.isZero -> Duration.ofHours(2).plusMinutes(30) + else -> this.runtime } - } - - private fun ShowingDTO.getStartDate(): Instant { - return LocalDateTime.of(this.date, this.time) - .atZone(stockholmZoneId) - .toInstant() - } - - private fun ShowingDTO.getEndDate(movie: Movie): Date { - val end = this.getStartDate() - .plusMillis(movie.getDurationOrDefault2hours().toMillis()) - return Date.from(end) - } - - private fun Movie.getDurationOrDefault2hours() = when { - this.runtime.isZero -> Duration.ofHours(2).plusMinutes(30) - else -> this.runtime - } } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/ForetagsbiljettService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/ForetagsbiljettService.kt index b767be377..7b78f52b4 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/ForetagsbiljettService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/ForetagsbiljettService.kt @@ -4,7 +4,7 @@ import org.springframework.stereotype.Service import rocks.didit.sefilm.NotFoundException import rocks.didit.sefilm.TicketAlreadyInUserException import rocks.didit.sefilm.TicketInUseException -import rocks.didit.sefilm.currentLoggedInUser +import rocks.didit.sefilm.currentLoggedInUserId import rocks.didit.sefilm.database.repositories.ShowingRepository import rocks.didit.sefilm.database.repositories.UserRepository import rocks.didit.sefilm.domain.* @@ -13,93 +13,93 @@ import java.time.LocalDate @Service class ForetagsbiljettService( - private val showingRepository: ShowingRepository, - private val userRepository: UserRepository + private val showingRepository: ShowingRepository, + private val userRepository: UserRepository ) { - fun getStatusOfTicket(ticket: Foretagsbiljett): Foretagsbiljett.Status { - if (ticket.expires < LocalDate.now()) { - return Foretagsbiljett.Status.Expired - } + fun getStatusOfTicket(ticket: Foretagsbiljett): Foretagsbiljett.Status { + if (ticket.expires < LocalDate.now()) { + return Foretagsbiljett.Status.Expired + } + + val showings = showingRepository.findAll() + .filter { s -> s.participants.any { p -> hasTicket(p, ticket) } } + + if (showings.size > 1) { + throw AssertionError("More than one showing with Företagsbiljett: ${ticket.number}") + } - val showings = showingRepository.findAll() - .filter { s -> s.participants.any { p -> hasTicket(p, ticket) } } + if (showings.isEmpty()) { + return Foretagsbiljett.Status.Available + } - if (showings.size > 1) { - throw AssertionError("More than one showing with Företagsbiljett: ${ticket.number}") + return if (showings.first().ticketsBought) { + Foretagsbiljett.Status.Used + } else { + Foretagsbiljett.Status.Pending + } } - if (showings.isEmpty()) { - return Foretagsbiljett.Status.Available + fun getForetagsbiljetterForUser(userID: UserID): List = userRepository + .findById(userID) + .map { it.foretagsbiljetter } + .orElseGet { listOf() } + + fun addForetagsbiljetterToCurrentUser(biljetter: List) { + val currentUser = userRepository.findById(currentLoggedInUserId()) + .orElseThrow { NotFoundException("current user", currentLoggedInUserId()) } + val biljetterWithouthNew = currentUser + .foretagsbiljetter + .filterNot { (ticket) -> biljetter.any { ticket.number == it.number } } + + + assertForetagsbiljetterNotAlreadyInUse(biljetter, currentUser.foretagsbiljetter) + + val newTickets = biljetter.map { Foretagsbiljett.valueOf(it) } + val combinedTickets = newTickets + .plus(biljetterWithouthNew) + .sortedBy { it.expires } + + userRepository.save(currentUser.copy(foretagsbiljetter = combinedTickets)) } - return if (showings.first().ticketsBought) { - Foretagsbiljett.Status.Used - } else { - Foretagsbiljett.Status.Pending + fun deleteTicketFromUser(biljett: ForetagsbiljettDTO) { + val currentUser = userRepository.findById(currentLoggedInUserId()) + .orElseThrow { NotFoundException("current user", currentLoggedInUserId()) } + + val ticketNumber = TicketNumber(biljett.number) + val ticket = currentUser.foretagsbiljetter.find { it.number == ticketNumber } + ?: throw NotFoundException("företagsbiljett with number $ticketNumber") + assertTicketIsntPending(ticket) + + val ticketsWithoutDeleted = currentUser.foretagsbiljetter.filterNot { it.number == ticketNumber } + userRepository.save(currentUser.copy(foretagsbiljetter = ticketsWithoutDeleted)) } - } - - fun getForetagsbiljetterForUser(userID: UserID): List = userRepository - .findById(userID) - .map { it.foretagsbiljetter } - .orElseGet { listOf() } - - fun addForetagsbiljetterToCurrentUser(biljetter: List) { - val currentUser = userRepository.findById(currentLoggedInUser()) - .orElseThrow { NotFoundException("current user", currentLoggedInUser()) } - val biljetterWithouthNew = currentUser - .foretagsbiljetter - .filterNot { (ticket) -> biljetter.any { ticket.number == it.number } } - - - assertForetagsbiljetterNotAlreadyInUse(biljetter, currentUser.foretagsbiljetter) - - val newTickets = biljetter.map { Foretagsbiljett.valueOf(it) } - val combinedTickets = newTickets - .plus(biljetterWithouthNew) - .sortedBy { it.expires } - - userRepository.save(currentUser.copy(foretagsbiljetter = combinedTickets)) - } - - fun deleteTicketFromUser(biljett: ForetagsbiljettDTO) { - val currentUser = userRepository.findById(currentLoggedInUser()) - .orElseThrow { NotFoundException("current user", currentLoggedInUser()) } - - val ticketNumber = TicketNumber(biljett.number) - val ticket = currentUser.foretagsbiljetter.find { it.number == ticketNumber } - ?: throw NotFoundException("företagsbiljett with number $ticketNumber") - assertTicketIsntPending(ticket) - - val ticketsWithoutDeleted = currentUser.foretagsbiljetter.filterNot { it.number == ticketNumber } - userRepository.save(currentUser.copy(foretagsbiljetter = ticketsWithoutDeleted)) - } - - /** The tickets are allowed to be in use by the current user. */ - private fun assertForetagsbiljetterNotAlreadyInUse( - biljetter: List, - userBiljetter: List - ) { - biljetter.forEach { - val ticketNumber = TicketNumber(it.number) - if (!userBiljetter.any { it.number == ticketNumber } - && userRepository.existsByForetagsbiljetterNumber(TicketNumber(it.number))) { - throw TicketAlreadyInUserException(currentLoggedInUser()) - } + + /** The tickets are allowed to be in use by the current user. */ + private fun assertForetagsbiljetterNotAlreadyInUse( + biljetter: List, + userBiljetter: List + ) { + biljetter.forEach { + val ticketNumber = TicketNumber(it.number) + if (!userBiljetter.any { it.number == ticketNumber } + && userRepository.existsByForetagsbiljetterNumber(TicketNumber(it.number))) { + throw TicketAlreadyInUserException(currentLoggedInUserId()) + } + } } - } - private fun assertTicketIsntPending(ticket: Foretagsbiljett) { - if (getStatusOfTicket(ticket) == Foretagsbiljett.Status.Pending) { - throw TicketInUseException(ticket.number) + private fun assertTicketIsntPending(ticket: Foretagsbiljett) { + if (getStatusOfTicket(ticket) == Foretagsbiljett.Status.Pending) { + throw TicketInUseException(ticket.number) + } } - } - private fun hasTicket(p: Participant, ticket: Foretagsbiljett): Boolean { - return when (p) { - is FtgBiljettParticipant -> p.ticketNumber == ticket.number - else -> false + private fun hasTicket(p: Participant, ticket: Foretagsbiljett): Boolean { + return when (p) { + is FtgBiljettParticipant -> p.ticketNumber == ticket.number + else -> false + } } - } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/LocationService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/LocationService.kt index ca7f84088..7a73b8158 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/LocationService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/LocationService.kt @@ -7,24 +7,24 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder import org.springframework.stereotype.Service import rocks.didit.sefilm.database.entities.Location import rocks.didit.sefilm.database.repositories.LocationRepository -import rocks.didit.sefilm.domain.dto.SfCityAliasDTO +import rocks.didit.sefilm.domain.dto.FilmstadenCityAliasDTO import java.util.* @Service class LocationService(private val locationRepo: LocationRepository) { - fun allLocations() = locationRepo.findAll().toList() - fun getLocation(name: String): Optional = locationRepo.findByNameIgnoreCaseOrAliasIgnoreCase(name, name) + fun allLocations() = locationRepo.findAll().toList() + fun getLocation(name: String): Optional = locationRepo.findByNameIgnoreCaseOrAliasIgnoreCase(name, name) - fun sfCities(): List { - val objectMapper: ObjectMapper = Jackson2ObjectMapperBuilder.json().build() + fun filmstadenCities(): List { + val objectMapper: ObjectMapper = Jackson2ObjectMapperBuilder.json().build() - val cityResource = ClassPathResource("seeds/sf-cities.json") - return objectMapper.readValue(cityResource.inputStream) - } + val cityResource = ClassPathResource("seeds/filmstaden-cities.json") + return objectMapper.readValue(cityResource.inputStream) + } - fun getOrCreateNewLocation(name: String): Location { - return locationRepo - .findByNameIgnoreCaseOrAliasIgnoreCase(name, name) - .orElseGet { locationRepo.save(Location(name = name)) } - } + fun getOrCreateNewLocation(name: String): Location { + return locationRepo + .findByNameIgnoreCaseOrAliasIgnoreCase(name, name) + .orElseGet { locationRepo.save(Location(name = name)) } + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/MovieService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/MovieService.kt index 537001f34..495ccab37 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/MovieService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/MovieService.kt @@ -6,70 +6,70 @@ import org.springframework.stereotype.Service import rocks.didit.sefilm.NotFoundException import rocks.didit.sefilm.database.entities.Movie import rocks.didit.sefilm.database.repositories.MovieRepository -import rocks.didit.sefilm.domain.dto.SfMovieDTO +import rocks.didit.sefilm.domain.dto.FilmstadenMovieDTO import rocks.didit.sefilm.orElseThrow import rocks.didit.sefilm.schedulers.AsyncMovieUpdater -import rocks.didit.sefilm.services.external.SFService +import rocks.didit.sefilm.services.external.FilmstadenService import rocks.didit.sefilm.utils.MovieFilterUtil import java.time.Duration import java.util.* @Service class MovieService( - private val movieRepo: MovieRepository, - private val sfService: SFService, - private val filterUtil: MovieFilterUtil, - private val asyncMovieUpdater: AsyncMovieUpdater? + private val movieRepo: MovieRepository, + private val filmstadenService: FilmstadenService, + private val filterUtil: MovieFilterUtil, + private val asyncMovieUpdater: AsyncMovieUpdater? ) { - companion object { - private val log: Logger = LoggerFactory.getLogger(MovieService::class.java) - } + companion object { + private val log: Logger = LoggerFactory.getLogger(MovieService::class.java) + } - /** All movies that aren't archived */ - fun allMovies() = movieRepo.findByArchivedOrderByPopularityDesc(false) + /** All movies that aren't archived */ + fun allMovies() = movieRepo.findByArchivedOrderByPopularityDesc(false) - fun archivedMovies() = movieRepo.findByArchivedOrderByPopularityDesc(true) + fun archivedMovies() = movieRepo.findByArchivedOrderByPopularityDesc(true) - fun getMovie(movieId: UUID?): Movie? { - if (movieId == null) return null - return movieRepo.findById(movieId).orElse(null) - } + fun getMovie(movieId: UUID?): Movie? { + if (movieId == null) return null + return movieRepo.findById(movieId).orElse(null) + } - fun getMovieOrThrow(movieId: UUID?): Movie = - getMovie(movieId).orElseThrow { NotFoundException("movie with id: $movieId") } + fun getMovieOrThrow(movieId: UUID?): Movie = + getMovie(movieId).orElseThrow { NotFoundException("movie with id: $movieId") } - fun movieExists(movieId: UUID): Boolean = movieRepo.existsById(movieId) + fun movieExists(movieId: UUID): Boolean = movieRepo.existsById(movieId) - /** Fetch new movies from SF, and trigger an async background update of the movies when done */ - fun fetchNewMoviesFromSf(): List { - val sfMovies = sfService.allMovies() + /** Fetch new movies from Filmstaden, and trigger an async background update of the movies when done */ + fun fetchNewMoviesFromFilmstaden(): List { + val FilmstadenMovies = filmstadenService.allMovies() - val ourMovies = movieRepo.findAll() - val newMoviesWeHaventPreviouslySeen = sfMovies - .filter { - filterUtil.isNewerThan(it) - && !filterUtil.isMovieUnwantedBasedOnGenre(it.genres.map { it.name }) - && !filterUtil.isTitleUnwanted(it.title) - && ourMovies.isOtherMovieAlreadyKnown(it) - } - .map { - Movie(title = filterUtil.trimTitle(it.title), - sfId = it.ncgId, - releaseDate = it.releaseDate, - poster = it.posterUrl, - sfSlug = it.slug, - runtime = Duration.ofMinutes(it.length?.toLong() ?: 0L), - genres = it.genres.map { g -> g.name }) - } + val ourMovies = movieRepo.findAll() + val newMoviesWeHaventPreviouslySeen = FilmstadenMovies + .filter { + filterUtil.isNewerThan(it) + && !filterUtil.isMovieUnwantedBasedOnGenre(it.genres.map { it.name }) + && !filterUtil.isTitleUnwanted(it.title) + && ourMovies.isOtherMovieAlreadyKnown(it) + } + .map { + Movie(title = filterUtil.trimTitle(it.title), + filmstadenId = it.ncgId, + releaseDate = it.releaseDate, + poster = it.posterUrl, + filmstadenSlug = it.slug, + runtime = Duration.ofMinutes(it.length?.toLong() ?: 0L), + genres = it.genres.map { g -> g.name }) + } - val savedEntities = movieRepo.saveAll(newMoviesWeHaventPreviouslySeen) - log.info("Fetched ${savedEntities.count()} new movies from SF") + val savedEntities = movieRepo.saveAll(newMoviesWeHaventPreviouslySeen) + log.info("Fetched ${savedEntities.count()} new movies from Filmstaden") - asyncMovieUpdater?.extendMovieInfo(savedEntities) - return savedEntities.sortedBy { it.releaseDate } - } + asyncMovieUpdater?.extendMovieInfo(savedEntities) + return savedEntities.sortedBy { it.releaseDate } + } - private fun Iterable.isOtherMovieAlreadyKnown(other: SfMovieDTO) = - this.firstOrNull { our -> our.sfId == other.ncgId } == null + private fun Iterable.isOtherMovieAlreadyKnown(other: FilmstadenMovieDTO) = + this.firstOrNull { our -> our.filmstadenId == other.ncgId } == null } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/ParticipantPaymentInfoService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/ParticipantPaymentInfoService.kt index f1b7eac00..6ed46ec2a 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/ParticipantPaymentInfoService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/ParticipantPaymentInfoService.kt @@ -10,31 +10,31 @@ import rocks.didit.sefilm.domain.dto.ParticipantPaymentInfoDTO @Service class ParticipantPaymentInfoService( - private val participantInfoRepo: ParticipantPaymentInfoRepository, - private val showingService: ShowingService, - private val assertionService: AssertionService + private val participantInfoRepo: ParticipantPaymentInfoRepository, + private val showingService: ShowingService, + private val assertionService: AssertionService ) { - fun updatePaymentInfo(participantInfo: ParticipantPaymentInfoDTO): ParticipantPaymentInfo { - if (participantInfo.showingId == null || participantInfo.userId == null) { - throw IllegalArgumentException("Missing showing id and/or user id") - } - assertionService.assertLoggedInUserIsAdmin(showingService.getShowingOrThrow(participantInfo.showingId)) + fun updatePaymentInfo(participantInfo: ParticipantPaymentInfoDTO): ParticipantPaymentInfo { + if (participantInfo.showingId == null || participantInfo.userId == null) { + throw IllegalArgumentException("Missing showing id and/or user id") + } + assertionService.assertLoggedInUserIsAdmin(showingService.getShowingOrThrow(participantInfo.showingId)) - val paymentInfo = participantInfoRepo - .findById(participantInfo.id) - .orElseThrow { - NotFoundException( - "participant info '${participantInfo.id}'", - showingId = participantInfo.showingId - ) - } + val paymentInfo = participantInfoRepo + .findById(participantInfo.id) + .orElseThrow { + NotFoundException( + "participant info '${participantInfo.id}'", + showingId = participantInfo.showingId + ) + } - if (paymentInfo.showingId != participantInfo.showingId) { - throw AccessDeniedException("Oh no you didn't!") - } + if (paymentInfo.showingId != participantInfo.showingId) { + throw AccessDeniedException("Oh no you didn't!") + } - val newInfo = paymentInfo.copy(hasPaid = participantInfo.hasPaid, amountOwed = SEK(participantInfo.amountOwed)) - return participantInfoRepo.save(newInfo) - } + val newInfo = paymentInfo.copy(hasPaid = participantInfo.hasPaid, amountOwed = SEK(participantInfo.amountOwed)) + return participantInfoRepo.save(newInfo) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/ShowingService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/ShowingService.kt index 075a3c318..4431e566c 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/ShowingService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/ShowingService.kt @@ -13,297 +13,302 @@ import rocks.didit.sefilm.database.repositories.ShowingRepository import rocks.didit.sefilm.domain.* import rocks.didit.sefilm.domain.dto.* import rocks.didit.sefilm.events.* -import rocks.didit.sefilm.services.external.SFService +import rocks.didit.sefilm.services.external.FilmstadenService import rocks.didit.sefilm.utils.SwishUtil.Companion.constructSwishUri import java.time.LocalDate import java.util.* @Service class ShowingService( - private val showingRepo: ShowingRepository, - private val paymentInfoRepo: ParticipantPaymentInfoRepository, - private val movieService: MovieService, - private val userService: UserService, - private val ticketService: TicketService, - private val slugService: SlugService, - private val sfService: SFService, - private val locationService: LocationService, - private val eventPublisher: EventPublisher, - private val assertionService: AssertionService + private val showingRepo: ShowingRepository, + private val paymentInfoRepo: ParticipantPaymentInfoRepository, + private val movieService: MovieService, + private val userService: UserService, + private val ticketService: TicketService, + private val slugService: SlugService, + private val filmstadenService: FilmstadenService, + private val locationService: LocationService, + private val eventPublisher: EventPublisher, + private val assertionService: AssertionService ) { - companion object { - private val log: Logger = LoggerFactory.getLogger(ShowingService::class.java) - } - - private fun getShowingEntity(id: UUID): Showing = - showingRepo.findById(id).orElseThrow { NotFoundException("showing", showingId = id) } - - fun getShowing(id: UUID): ShowingDTO? = showingRepo - .findById(id) - .map { it.toDto() } - .orElse(null) - - fun getShowing(webId: Base64ID): ShowingDTO? = showingRepo - .findByWebId(webId) - .map { it.toDto() } - .orElse(null) - - fun getShowingOrThrow(id: UUID): ShowingDTO = getShowing(id) - ?: throw NotFoundException(what = "showing", showingId = id) - - fun getShowingByMovie(movieId: UUID): List = showingRepo - .findByMovieIdOrderByDateDesc(movieId) - .map { it.toDto() } - - fun getShowingByUser(user: UserID): List = showingRepo - .findAll() - .filter { it.userIsInvolvedInThisShowing(user) } - .map { it.toDto() } - - fun getAllPublicShowings(afterDate: LocalDate = LocalDate.MIN): List = - showingRepo.findByPrivateOrderByDateDesc(false).toList() - .filter { it.date?.isAfter(afterDate) ?: false } - .map { it.toDto() } - - fun getPrivateShowingsForCurrentUser(afterDate: LocalDate = LocalDate.MIN): List { - val currentLoggedInUser = currentLoggedInUser() - return showingRepo.findByPrivateOrderByDateDesc(true) - .filter { it.userIsInvolvedInThisShowing(currentLoggedInUser) } - .map { it.toDto() } - } - - /** Info that is needed before you buy the tickets at SF */ - fun getAdminPaymentDetails(showingId: UUID): AdminPaymentDetailsDTO? { - val showing = getShowingEntity(showingId) - if (showing.admin.id != currentLoggedInUser()) { - return null + companion object { + private val log: Logger = LoggerFactory.getLogger(ShowingService::class.java) } - // Note that this list is empty before the showing has been marked as bought - val paymentInfos = paymentInfoRepo.findByShowingId(showingId) - val ticketMap = showing.participants.map { - val user = userService.getCompleteUser(it.userId).orElseThrow { NotFoundException("user", it.userId, showingId) } - val ftgTicket = (it as? FtgBiljettParticipant)?.ticketNumber - UserAndSfData(user.id, user.sfMembershipId, ftgTicket) + private fun getShowingEntity(id: UUID): Showing = + showingRepo.findById(id).orElseThrow { NotFoundException("showing", showingId = id) } + + fun getShowing(id: UUID): ShowingDTO? = showingRepo + .findById(id) + .map { it.toDto() } + .orElse(null) + + fun getShowing(webId: Base64ID): ShowingDTO? = showingRepo + .findByWebId(webId) + .map { it.toDto() } + .orElse(null) + + fun getShowingOrThrow(id: UUID): ShowingDTO = getShowing(id) + ?: throw NotFoundException(what = "showing", showingId = id) + + fun getShowingByMovie(movieId: UUID): List = showingRepo + .findByMovieIdOrderByDateDesc(movieId) + .map { it.toDto() } + + fun getShowingByUser(user: UserID): List = showingRepo + .findAll() + .filter { it.userIsInvolvedInThisShowing(user) } + .map { it.toDto() } + + fun getAllPublicShowings(afterDate: LocalDate = LocalDate.MIN): List = + showingRepo.findByPrivateOrderByDateDesc(false).toList() + .filter { it.date?.isAfter(afterDate) ?: false } + .map { it.toDto() } + + fun getPrivateShowingsForCurrentUser(afterDate: LocalDate = LocalDate.MIN): List { + val currentLoggedInUser = currentLoggedInUserId() + return showingRepo.findByPrivateOrderByDateDesc(true) + .filter { it.userIsInvolvedInThisShowing(currentLoggedInUser) } + .map { it.toDto() } } - return AdminPaymentDetailsDTO(sfService.getSfBuyLink(showing.movie.id), ticketMap, paymentInfos) - } + /** Info that is needed before you buy the tickets at Filmstaden */ + fun getAdminPaymentDetails(showingId: UUID): AdminPaymentDetailsDTO? { + val showing = getShowingEntity(showingId) + if (showing.admin.id != currentLoggedInUserId()) { + return null + } + + // Note that this list is empty before the showing has been marked as bought + val paymentInfos = paymentInfoRepo.findByShowingId(showingId) + val ticketMap = showing.participants.map { + val user = userService.getCompleteUser(it.userId).orElseThrow { NotFoundException("user", it.userId, showingId) } + val ftgTicket = (it as? FtgBiljettParticipant)?.ticketNumber + UserAndFilmstadenData(user.id, user.filmstadenMembershipId, ftgTicket) + } + + return AdminPaymentDetailsDTO(filmstadenService.getFilmstadenBuyLink(showing.movie.id), ticketMap, paymentInfos) + } - /** Info a user needs for paying the one who bought the tickets */ - fun getAttendeePaymentDetails(showingId: UUID): AttendeePaymentDetailsDTO? = - getAttendeePaymentDetailsForUser(currentLoggedInUser(), showingId) + /** Info a user needs for paying the one who bought the tickets */ + fun getAttendeePaymentDetails(showingId: UUID): AttendeePaymentDetailsDTO? = + getAttendeePaymentDetailsForUser(currentLoggedInUserId(), showingId) + + fun getAttendeePaymentDetailsForUser(userID: UserID, showingId: UUID): AttendeePaymentDetailsDTO? { + val showing = getShowingEntity(showingId) + val payeePhone = showing.payToUser.phone + .orElseThrow { MissingPhoneNumberException(showing.payToUser.id) } + + val participantInfo = paymentInfoRepo + .findByShowingIdAndUserId(showingId, userID) + .orElse(null) ?: return null + + val movieTitle = movieService.getMovieOrThrow(showing.movie.id).title + + val swishTo = when { + !participantInfo.hasPaid && participantInfo.amountOwed.ören > 0 -> constructSwishUri( + showing, + payeePhone, + participantInfo, + movieTitle + ) + else -> null + } + + return AttendeePaymentDetailsDTO( + participantInfo.hasPaid, + participantInfo.amountOwed, + showing.payToUser.id, + swishTo, + payeePhone.number, + userID + ) + } - fun getAttendeePaymentDetailsForUser(userID: UserID, showingId: UUID): AttendeePaymentDetailsDTO? { - val showing = getShowingEntity(showingId) - val payeePhone = showing.payToUser.phone - .orElseThrow { MissingPhoneNumberException(showing.payToUser.id) } + fun attendShowing(showingId: UUID, paymentOption: PaymentOption): ShowingDTO { + val showing = getShowingEntity(showingId) + val userId = currentLoggedInUserId() + assertionService.assertTicketsNotBought(userId, showing) + assertionService.assertUserNotAlreadyAttended(userId, showing) - val participantInfo = paymentInfoRepo - .findByShowingIdAndUserId(showingId, userID) - .orElse(null) ?: return null + val participant: Participant = createParticipantBasedOnPaymentType(paymentOption, userId, showing) - val movieTitle = movieService.getMovieOrThrow(showing.movie.id).title + val newParticipants = showing.participants.plus(participant) + return showingRepo + .save(showing.copy(participants = newParticipants)) + .also { + val user = userService.getCompleteUser(userId) + eventPublisher.publish(UserAttendedEvent(this, it, user, paymentOption.type)) + } + .toDto() - val swishTo = when { - !participantInfo.hasPaid && participantInfo.amountOwed.ören > 0 -> constructSwishUri( - showing, - payeePhone, - participantInfo, - movieTitle - ) - else -> null } - return AttendeePaymentDetailsDTO( - participantInfo.hasPaid, - participantInfo.amountOwed, - showing.payToUser.id, - swishTo, - payeePhone.number, - userID - ) - } - - fun attendShowing(showingId: UUID, paymentOption: PaymentOption): ShowingDTO { - val showing = getShowingEntity(showingId) - val userId = currentLoggedInUser() - assertionService.assertTicketsNotBought(userId, showing) - assertionService.assertUserNotAlreadyAttended(userId, showing) - - val participant: Participant = createParticipantBasedOnPaymentType(paymentOption, userId, showing) - - val newParticipants = showing.participants.plus(participant) - return showingRepo - .save(showing.copy(participants = newParticipants)) - .also { - val user = userService.getCompleteUser(userId) - eventPublisher.publish(UserAttendedEvent(this, it, user, paymentOption.type)) - } - .toDto() - - } - - fun unattendShowing(showingId: UUID): ShowingDTO { - val showing = getShowingEntity(showingId) - val currentUserId = currentLoggedInUser() - assertionService.assertTicketsNotBought(currentUserId, showing) - - val participantLst = showing - .participants - .filter { it.userId == currentUserId } - - if (participantLst.isEmpty()) { - return showing.toDto() - } else if (participantLst.size > 1) { - throw IllegalStateException("Participant $currentUserId has participated more than one time on showing $showingId") + fun unattendShowing(showingId: UUID): ShowingDTO { + val showing = getShowingEntity(showingId) + val currentUserId = currentLoggedInUserId() + assertionService.assertTicketsNotBought(currentUserId, showing) + + val participantLst = showing + .participants + .filter { it.userId == currentUserId } + + if (participantLst.isEmpty()) { + return showing.toDto() + } else if (participantLst.size > 1) { + throw IllegalStateException("Participant $currentUserId has participated more than one time on showing $showingId") + } + + val participant = participantLst.first() + val participantsWithoutLoggedInUser = showing.participants.minus(participant) + return showingRepo + .save(showing.copy(participants = participantsWithoutLoggedInUser)) + .also { + val user = userService.getCompleteUser(participant.userId) + eventPublisher.publish(UserUnattendedEvent(this, it, user)) + } + .toDto() } - val participant = participantLst.first() - val participantsWithoutLoggedInUser = showing.participants.minus(participant) - return showingRepo - .save(showing.copy(participants = participantsWithoutLoggedInUser)) - .also { - val user = userService.getCompleteUser(participant.userId) - eventPublisher.publish(UserUnattendedEvent(this, it, user)) - } - .toDto() - } - - fun createShowing(data: CreateShowingDTO): ShowingDTO { - if (data.date == null || data.location == null || data.movieId == null || data.time == null) throw MissingParametersException() - - val adminUser = userService.getCompleteUser(currentLoggedInUser()) - return showingRepo - .save(data.toShowing(adminUser, movieService.getMovieOrThrow(data.movieId))) - .also { - eventPublisher.publish(NewShowingEvent(this, it, adminUser)) - } - .toDto() - } - - /** Delete the selected showing and return all public showings */ - fun deleteShowing(showingId: UUID): List { - val showing = getShowingEntity(showingId) - assertionService.assertLoggedInUserIsAdmin(showing.admin.id) - assertionService.assertTicketsNotBought(showing.admin.id, showing) - - paymentInfoRepo.deleteByShowingIdAndUserId(showing.id, currentLoggedInUser()) - ticketService.deleteTickets(showing) - showingRepo.delete(showing) - - eventPublisher.publish(DeletedShowingEvent(this, showing, showing.admin)) - return getAllPublicShowings() - } - - fun markAsBought(showingId: UUID): ShowingDTO { - val showing = getShowingEntity(showingId) - assertionService.assertLoggedInUserIsAdmin(showing.admin.id) - assertionService.assertUserHasPhoneNumber(showing.admin.id) - - if (showing.ticketsBought) { - log.info("Showing $showingId is already bought") - return showing.toDto() + fun createShowing(data: CreateShowingDTO): ShowingDTO { + val adminUser = userService.getCompleteUser(currentLoggedInUserId()) + val filmstadenShow = data.filmstadenRemoteEntityId?.let { filmstadenService.fetchFilmstadenShow(it) } + return showingRepo + .save(data.toShowing(adminUser, + movieService.getMovieOrThrow(data.movieId), + filmstadenShow?.screen + )) + .also { + eventPublisher.publish(NewShowingEvent(this, it, adminUser)) + } + .toDto() } - createInitialPaymentInfo(showing) - return showingRepo - .save(showing.copy(ticketsBought = true)) - .also { - eventPublisher.publish(TicketsBoughtEvent(this, it, it.admin)) - } - .toDto() - } - - fun updateShowing(showingId: UUID, newValues: UpdateShowingDTO): ShowingDTO { - val showing = getShowingEntity(showingId) - assertionService.assertLoggedInUserIsAdmin(showing.admin.id) - - log.info("Updating showing ($showingId) to new values: $newValues") - return showingRepo.save( - showing.copy( - price = SEK(newValues.price), - private = newValues.private, - payToUser = userService.getCompleteUser(UserID(newValues.payToUser)), - expectedBuyDate = newValues.expectedBuyDate, - location = locationService.getOrCreateNewLocation(newValues.location), - sfScreen = newValues.sfScreen, - time = newValues.time - ) - ) - .also { - eventPublisher.publish(UpdatedShowingEvent(this, it, it.admin)) - } - .toDto() - } - - fun fetchSeatMap(showingId: UUID): List { - val showing = getShowingOrThrow(showingId) - if (showing.location.sfId == null || showing.sfScreen?.sfId == null) { - log.debug("Showing $showingId is not at a Sf location or does not have an associated Sf screen") - return listOf() - } + /** Delete the selected showing and return all public showings */ + fun deleteShowing(showingId: UUID): List { + val showing = getShowingEntity(showingId) + assertionService.assertLoggedInUserIsAdmin(showing.admin.id) + assertionService.assertTicketsNotBought(showing.admin.id, showing) - return sfService.getSfSeatMap(showing.location.sfId, showing.sfScreen.sfId) - } + paymentInfoRepo.deleteByShowingIdAndUserId(showing.id, currentLoggedInUserId()) + ticketService.deleteTickets(showing) + showingRepo.delete(showing) - private fun createParticipantBasedOnPaymentType(paymentOption: PaymentOption, userId: UserID, showing: Showing): Participant = - when (paymentOption.type) { - PaymentType.Foretagsbiljett -> { - val suppliedTicket = paymentOption.ticketNumber - ?: throw MissingParametersException("User chose to pay with a företagsbiljett, but no ticket number were supplied") - val ticketNumber = TicketNumber(suppliedTicket) + eventPublisher.publish(DeletedShowingEvent(this, showing, showing.admin)) + return getAllPublicShowings() + } - assertionService.assertForetagsbiljettIsUsable(userId, ticketNumber, showing) - FtgBiljettParticipant(userId, ticketNumber) - } - PaymentType.Swish -> SwishParticipant(userId) + fun markAsBought(showingId: UUID, price: SEK): ShowingDTO { + val showing = getShowingEntity(showingId) + assertionService.assertLoggedInUserIsAdmin(showing.admin.id) + assertionService.assertUserHasPhoneNumber(showing.admin.id) + + if (showing.ticketsBought) { + log.info("Showing $showingId is already bought") + return showing.toDto() + } + + createInitialPaymentInfo(showing) + return showingRepo + .save(showing.copy(ticketsBought = true, price = price)) + .also { + eventPublisher.publish(TicketsBoughtEvent(this, it, it.admin)) + } + .toDto() } - private fun Showing.userIsInvolvedInThisShowing(userID: UserID): Boolean { - return this.isAdmin(userID) || this.isParticipantInShowing(userID) - || this.payToUser.id == userID - } + fun updateShowing(showingId: UUID, newValues: UpdateShowingDTO): ShowingDTO { + val showing = getShowingEntity(showingId) + assertionService.assertLoggedInUserIsAdmin(showing.admin.id) + + log.info("Updating showing ($showingId) to new values: $newValues") + val filmstadenShow = newValues.filmstadenRemoteEntityId?.let { filmstadenService.fetchFilmstadenShow(it) } + return showingRepo.save( + showing.copy( + price = SEK(newValues.price), + private = newValues.private, + payToUser = userService.getCompleteUser(UserID(newValues.payToUser)), + expectedBuyDate = newValues.expectedBuyDate, + location = locationService.getOrCreateNewLocation(newValues.location), + time = newValues.time, + filmstadenRemoteEntityId = newValues.filmstadenRemoteEntityId, + filmstadenScreen = filmstadenShow?.screen?.toFilmstadenLiteScreen(), + date = newValues.date + ) + ) + .also { + eventPublisher.publish(UpdatedShowingEvent(this, it, it.admin)) + } + .toDto() + } - private fun Showing.isAdmin(userID: UserID): Boolean = this.admin.id == userID + fun fetchSeatMap(showingId: UUID): List { + val showing = getShowingOrThrow(showingId) + if (showing.location.filmstadenId == null || showing.filmstadenScreen?.filmstadenId == null) { + log.debug("Showing $showingId is not at a Filmstaden location or does not have an associated Filmstaden screen") + return listOf() + } - private fun Showing.isParticipantInShowing(userID: UserID): Boolean = this.participants.any { it.userId == userID } + return filmstadenService.getFilmstadenSeatMap(showing.location.filmstadenId, showing.filmstadenScreen.filmstadenId) + } - /* Fetch location from db or create it if it does not exist before converting the showing */ - private fun CreateShowingDTO.toShowing(admin: User, movie: Movie): Showing { - if (this.location == null) { - throw IllegalArgumentException("Location may not be null") + private fun createParticipantBasedOnPaymentType(paymentOption: PaymentOption, userId: UserID, showing: Showing): Participant = + when (paymentOption.type) { + PaymentType.Foretagsbiljett -> { + val suppliedTicket = paymentOption.ticketNumber + ?: throw MissingParametersException("User chose to pay with a företagsbiljett, but no ticket number were supplied") + val ticketNumber = TicketNumber(suppliedTicket) + + assertionService.assertForetagsbiljettIsUsable(userId, ticketNumber, showing) + FtgBiljettParticipant(userId, ticketNumber) + } + PaymentType.Swish -> SwishParticipant(userId) + } + + private fun Showing.userIsInvolvedInThisShowing(userID: UserID): Boolean { + return this.isAdmin(userID) || this.isParticipantInShowing(userID) + || this.payToUser.id == userID } - val location = locationService.getOrCreateNewLocation(this.location) - return Showing( - webId = Base64ID.random(), - slug = slugService.generateSlugFor(movie), - date = this.date, - time = this.time, - movie = movieService.getMovieOrThrow(this.movieId), - location = location, - sfScreen = this.sfScreen, - admin = admin, - payToUser = admin, - expectedBuyDate = this.expectedBuyDate, - participants = setOf(SwishParticipant(admin.id)) - ) - } - - private fun createInitialPaymentInfo(showing: Showing) { - val participants = showing - .participants - .map { it -> - val hasPaid = it.userId == showing.payToUser.id || it is FtgBiljettParticipant - ParticipantPaymentInfo( - userId = it.userId, - showingId = showing.id, - hasPaid = hasPaid, - amountOwed = if (hasPaid || showing.price == null) SEK(0) else showing.price + + private fun Showing.isAdmin(userID: UserID): Boolean = this.admin.id == userID + + private fun Showing.isParticipantInShowing(userID: UserID): Boolean = this.participants.any { it.userId == userID } + + /* Fetch location from db or create it if it does not exist before converting the showing */ + private fun CreateShowingDTO.toShowing(admin: User, + movie: Movie, + filmstadenScreen: FilmstadenScreenDTO?): Showing { + val location = locationService.getOrCreateNewLocation(this.location) + return Showing( + webId = Base64ID.random(), + slug = slugService.generateSlugFor(movie), + date = this.date, + time = this.time, + movie = movieService.getMovieOrThrow(this.movieId), + location = location, + filmstadenScreen = filmstadenScreen?.toFilmstadenLiteScreen(), + admin = admin, + payToUser = admin, + expectedBuyDate = this.expectedBuyDate, + participants = setOf(SwishParticipant(admin.id)), + filmstadenRemoteEntityId = this.filmstadenRemoteEntityId ) - } - paymentInfoRepo.saveAll(participants) - } + } + + private fun createInitialPaymentInfo(showing: Showing) { + val participants = showing + .participants + .map { it -> + val hasPaid = it.userId == showing.payToUser.id || it is FtgBiljettParticipant + ParticipantPaymentInfo( + userId = it.userId, + showingId = showing.id, + hasPaid = hasPaid, + amountOwed = if (hasPaid || showing.price == null) SEK(0) else showing.price + ) + } + paymentInfoRepo.saveAll(participants) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/SlugService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/SlugService.kt index 87213e5e1..b82ad632f 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/SlugService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/SlugService.kt @@ -6,38 +6,38 @@ import rocks.didit.sefilm.database.entities.Showing @Service class SlugService { - companion object { - const val MAX_LENGTH = 45 - } + companion object { + const val MAX_LENGTH = 45 + } - fun generateSlugFor(movie: Movie): String = sluggifyString(movie.originalTitle ?: movie.title) + fun generateSlugFor(movie: Movie): String = sluggifyString(movie.originalTitle ?: movie.title) - fun generateSlugFor(showing: Showing): String { - val movie = showing.movie - return sluggifyString(movie.originalTitle ?: movie.title) - } + fun generateSlugFor(showing: Showing): String { + val movie = showing.movie + return sluggifyString(movie.originalTitle ?: movie.title) + } - private fun sluggifyString(str: String): String { - return str.toLowerCase() - .replace("-", "") - .replace(" ", "-") - .replace("'", "") - .replace(":", "") - .replace("å", "a") - .replace("ä", "a") - .replace("ö", "o") - .replace("&", "and") - .replace(",", "") - .replace("ó", "o") - .replace("é", "e") - .replace("®", "") - .trimToLength(MAX_LENGTH) - } + private fun sluggifyString(str: String): String { + return str.toLowerCase() + .replace("-", "") + .replace(" ", "-") + .replace("'", "") + .replace(":", "") + .replace("å", "a") + .replace("ä", "a") + .replace("ö", "o") + .replace("&", "and") + .replace(",", "") + .replace("ó", "o") + .replace("é", "e") + .replace("®", "") + .trimToLength(MAX_LENGTH) + } - private fun String.trimToLength(length: Int): String { - if (this.length <= length) { - return this + private fun String.trimToLength(length: Int): String { + if (this.length <= length) { + return this + } + return this.substring(0, MAX_LENGTH) } - return this.substring(0, MAX_LENGTH) - } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/TicketService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/TicketService.kt index 6819b8ee3..3a926ecd6 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/TicketService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/TicketService.kt @@ -2,154 +2,195 @@ package rocks.didit.sefilm.services import org.springframework.security.access.AccessDeniedException import org.springframework.stereotype.Service +import rocks.didit.sefilm.FilmstadenTicketException import rocks.didit.sefilm.NotFoundException -import rocks.didit.sefilm.SfTicketException -import rocks.didit.sefilm.currentLoggedInUser +import rocks.didit.sefilm.Properties +import rocks.didit.sefilm.currentLoggedInUserId import rocks.didit.sefilm.database.entities.Seat import rocks.didit.sefilm.database.entities.Showing import rocks.didit.sefilm.database.entities.Ticket import rocks.didit.sefilm.database.repositories.ShowingRepository import rocks.didit.sefilm.database.repositories.TicketRepository import rocks.didit.sefilm.database.repositories.UserRepository -import rocks.didit.sefilm.domain.SfMembershipId +import rocks.didit.sefilm.domain.FilmstadenMembershipId import rocks.didit.sefilm.domain.UserID +import rocks.didit.sefilm.domain.dto.FilmstadenTicketDTO import rocks.didit.sefilm.domain.dto.SeatRange -import rocks.didit.sefilm.domain.dto.SfTicketDTO import rocks.didit.sefilm.domain.dto.TicketRange -import rocks.didit.sefilm.services.external.SFService +import rocks.didit.sefilm.domain.dto.toFilmstadenLiteScreen +import rocks.didit.sefilm.services.external.FilmstadenService +import java.time.ZoneId import java.util.* @Service class TicketService( - private val sfClient: SFService, - private val userRepository: UserRepository, - private val ticketRepository: TicketRepository, - private val showingRepository: ShowingRepository + private val properties: Properties, + private val filmstadenService: FilmstadenService, + private val locationService: LocationService, + private val userRepository: UserRepository, + private val ticketRepository: TicketRepository, + private val showingRepository: ShowingRepository ) { - fun getTicketsForCurrentUserAndShowing(showingId: UUID): List { - val user = currentLoggedInUser() - isUserIsParticipant(showingId, user) - return ticketRepository.findByShowingIdAndAssignedToUser(showingId, user) - } + fun getTicketsForCurrentUserAndShowing(showingId: UUID): List { + val user = currentLoggedInUserId() + isUserIsParticipant(showingId, user) + return ticketRepository.findByShowingIdAndAssignedToUser(showingId, user) + } + + fun processTickets(userSuppliedTicketUrl: List, showingId: UUID): List { + val currentLoggedInUserId = currentLoggedInUserId() + val showing = + showingRepository.findById(showingId).orElseThrow { NotFoundException("showing", currentLoggedInUserId, showingId) } + + if (showing.admin.id != currentLoggedInUserId) { + throw AccessDeniedException("Only the showing admin is allowed to do that") + } + + validateFilmstadenTicketUrls(userSuppliedTicketUrl) + userSuppliedTicketUrl.firstOrNull()?.let { updateShowingFromTicketUrl(showing, it) } + userSuppliedTicketUrl.forEach { + processTicketUrl(it, showing) + } - fun processTickets(userSuppliedTicketUrl: List, showingId: UUID): List { - val currentLoggedInUser = currentLoggedInUser() - val showing = - showingRepository.findById(showingId).orElseThrow { NotFoundException("showing", currentLoggedInUser, showingId) } + if (properties.enableReassignment) { + reassignLeftoverTickets(showing) + } - if (showing.admin.id != currentLoggedInUser) { - throw AccessDeniedException("Only the showing admin is allowed to do that") + return getTicketsForCurrentUserAndShowing(showingId) } - validateSfTicketUrls(userSuppliedTicketUrl) - userSuppliedTicketUrl.forEach { - processTicketUrl(it, showing) + private fun reassignLeftoverTickets(showing: Showing) { + val adminAssignedTickets = ticketRepository.findByShowingIdAndAssignedToUser(showing.id, showing.admin.id) + if (adminAssignedTickets.size > 1) { + val allTickets = ticketRepository.findByShowingId(showing.id) + val participantsMissingTicket = showing.participants.filter { participant -> + allTickets.none { ticket -> ticket.assignedToUser == participant.userId } + } + + val reassigned = adminAssignedTickets.subList(1, adminAssignedTickets.size) + .zip(participantsMissingTicket) + .map { (ticket, participant) -> ticket.copy(assignedToUser = participant.userId) } + ticketRepository.saveAll(reassigned) + } + } + + private fun updateShowingFromTicketUrl(showing: Showing, ticketUrl: String) { + val (_, filmstadenRemoteEntityId, _) = extractIdsFromUrl(ticketUrl) + val fetchFilmstadenShow = filmstadenService.fetchFilmstadenShow(filmstadenRemoteEntityId) + val location = locationService.getOrCreateNewLocation(fetchFilmstadenShow.cinema.title) + val zonedDateTime = fetchFilmstadenShow.timeUtc.atZone(ZoneId.of("Europe/Stockholm")) + + val updatedShowing = showing.copy( + filmstadenScreen = fetchFilmstadenShow.screen.toFilmstadenLiteScreen(), + time = zonedDateTime.toLocalTime(), + date = zonedDateTime.toLocalDate(), + location = location + ) + + showingRepository.save(updatedShowing) } - return getTicketsForCurrentUserAndShowing(showingId) - } + private fun processTicketUrl(userSuppliedTicketUrl: String, showing: Showing) { + val (sysId, filmstadenRemoteEntityId, ticketId) = extractIdsFromUrl(userSuppliedTicketUrl) + val filmstadenTickets = filmstadenService.fetchTickets(sysId, filmstadenRemoteEntityId, ticketId) - private fun processTicketUrl(userSuppliedTicketUrl: String, showing: Showing) { - val (sysId, sfShowingId, ticketId) = extractIdsFromUrl(userSuppliedTicketUrl) - val sfTickets = sfClient.fetchTickets(sysId, sfShowingId, ticketId) + val tickets = filmstadenTickets.map { + val barcode = filmstadenService.fetchBarcode(it.id) + if (it.profileId == null || it.profileId.isBlank()) { + return@map it.toTicket(showing.id, showing.admin.id, barcode) + } - val tickets = sfTickets.map { - val barcode = sfClient.fetchBarcode(it.id) - if (it.profileId == null || it.profileId.isBlank()) { - return@map it.toTicket(showing.id, showing.admin.id, barcode) - } + val userIdForThatMember = getUserIdFromFilmstadenMembershipId(FilmstadenMembershipId.valueOf(it.profileId), showing) + it.toTicket(showing.id, userIdForThatMember, barcode) + } - val userIdForThatMember = getUserIdFromSfMembershipId(SfMembershipId.valueOf(it.profileId), showing) - it.toTicket(showing.id, userIdForThatMember, barcode) + ticketRepository.saveAll(tickets) } - ticketRepository.saveAll(tickets) - } + private fun getUserIdFromFilmstadenMembershipId(filmstadenMembershipId: FilmstadenMembershipId, showing: Showing): UserID { + val userIdForThatMember = userRepository + .findByFilmstadenMembershipId(filmstadenMembershipId) + ?.id + ?: return showing.admin.id + + if (showing.participants.any { it.userId == userIdForThatMember }) { + return userIdForThatMember + } - private fun getUserIdFromSfMembershipId(sfMembershipId: SfMembershipId, showing: Showing): UserID { - val userIdForThatMember = userRepository - .findBySfMembershipId(sfMembershipId) - ?.id - ?: return showing.admin.id + return showing.admin.id + } - if (showing.participants.any { it.userId == userIdForThatMember }) { - return userIdForThatMember + private fun extractIdsFromUrl(userSuppliedTicketUrl: String): Triple { + val parts = userSuppliedTicketUrl.split('/') + val ids = parts.subList(parts.size - 3, parts.size) + if (ids.size != 3) { + throw IllegalArgumentException("$userSuppliedTicketUrl does not contain three ids.") + } + return Triple(ids[0], ids[1], ids[2]) } - return showing.admin.id - } + fun deleteTickets(showing: Showing) { + val ticketsForShowing = ticketRepository.findByShowingId(showingId = showing.id) + ticketRepository.deleteAll(ticketsForShowing) + } - private fun extractIdsFromUrl(userSuppliedTicketUrl: String): Triple { - val parts = userSuppliedTicketUrl.split('/') - val ids = parts.subList(parts.size - 3, parts.size) - if (ids.size != 3) { - throw IllegalArgumentException("$userSuppliedTicketUrl does not contain three ids.") + fun getTicketRange(showingId: UUID): TicketRange? { + val currentLoggedInUser = currentLoggedInUserId() + if (!isUserIsParticipant(showingId, currentLoggedInUser)) { + return null + } + + val allSeatsForShowing = ticketRepository.findByShowingId(showingId) + .map { it.seat } + .sortedBy { it.number } + + val rows = allSeatsForShowing + .map { it.row } + .distinct() + .sorted() + + val groupedSeats = allSeatsForShowing + .groupBy({ it.row }, { it.number }) + .map { SeatRange(it.key, it.value) } + return TicketRange(rows, groupedSeats, allSeatsForShowing.size) } - return Triple(ids[0], ids[1], ids[2]) - } - - fun deleteTickets(showing: Showing) { - val ticketsForShowing = ticketRepository.findByShowingId(showingId = showing.id) - ticketRepository.deleteAll(ticketsForShowing) - } - - fun getTicketRange(showingId: UUID): TicketRange? { - val currentLoggedInUser = currentLoggedInUser() - if (!isUserIsParticipant(showingId, currentLoggedInUser)) { - return null + + private fun FilmstadenTicketDTO.toTicket(showingId: UUID, assignedToUser: UserID, barcode: String): Ticket { + val seat = Seat(this.seat.row, this.seat.number) + return Ticket( + id = this.id, + showingId = showingId, + assignedToUser = assignedToUser, + customerType = this.customerType, + customerTypeDefinition = this.customerTypeDefinition, + cinema = this.cinema.title, + cinemaCity = this.cinema.city.name, + screen = this.screen.title, + seat = seat, + date = this.show.date, + time = this.show.time, + movieName = this.movie.title, + movieRating = this.movie.rating.displayName, + showAttributes = this.show.attributes.map { it.displayName }, + barcode = barcode, + profileId = this.profileId + ) + } + + private fun isUserIsParticipant(showingId: UUID, currentLoggedInUser: UserID): Boolean { + return showingRepository.findById(showingId) + .orElseThrow { NotFoundException("showing", currentLoggedInUser, showingId) } + .participants + .any { it.userId == currentLoggedInUser } } - val allSeatsForShowing = ticketRepository.findByShowingId(showingId) - .map { it.seat } - .sortedBy { it.number } - - val rows = allSeatsForShowing - .map { it.row } - .distinct() - .sorted() - - val groupedSeats = allSeatsForShowing - .groupBy({ it.row }, { it.number }) - .map { SeatRange(it.key, it.value) } - return TicketRange(rows, groupedSeats, allSeatsForShowing.size) - } - - private fun SfTicketDTO.toTicket(showingId: UUID, assignedToUser: UserID, barcode: String): Ticket { - val seat = Seat(this.seat.row, this.seat.number) - return Ticket( - id = this.id, - showingId = showingId, - assignedToUser = assignedToUser, - customerType = this.customerType, - customerTypeDefinition = this.customerTypeDefinition, - cinema = this.cinema.title, - cinemaCity = this.cinema.city.name, - screen = this.screen.title, - seat = seat, - date = this.show.date, - time = this.show.time, - movieName = this.movie.title, - movieRating = this.movie.rating.displayName, - showAttributes = this.show.attributes.map { it.displayName }, - barcode = barcode, - profileId = this.profileId - ) - } - - private fun isUserIsParticipant(showingId: UUID, currentLoggedInUser: UserID): Boolean { - return showingRepository.findById(showingId) - .orElseThrow { NotFoundException("showing", currentLoggedInUser, showingId) } - .participants - .any { it.userId == currentLoggedInUser } - } - - private fun validateSfTicketUrls(links: List) { - val linkRegex = Regex(".+sf\\.se/bokning/mina-e-biljetter/Sys.+?/AA.+?/RE.+") - links.forEach { - if (!it.matches(linkRegex)) { - throw SfTicketException("$it does not look lika a valid ticket link. The link should look like this: https://www.sf.se/bokning/mina-e-biljetter/Sys99-SE/AA-1156-201712271800/RE-HPOUKRTI2N") - } + private fun validateFilmstadenTicketUrls(links: List) { + val linkRegex = Regex(".+filmstaden\\.se/bokning/mina-e-biljetter/Sys.+?/AA.+?/RE.+") + links.forEach { + if (!it.matches(linkRegex)) { + throw FilmstadenTicketException("$it does not look like a valid ticket link. The link should look like this: https://www.filmstaden.se/bokning/mina-e-biljetter/Sys99-SE/AA-1036-201908221930/RE-99RBBT0ZP6") + } + } } - } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/UserService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/UserService.kt index 3aa3d346e..73b8bbdf0 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/UserService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/UserService.kt @@ -7,12 +7,12 @@ import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Service import rocks.didit.sefilm.NotFoundException import rocks.didit.sefilm.OpenIdConnectUserDetails -import rocks.didit.sefilm.currentLoggedInUser +import rocks.didit.sefilm.currentLoggedInUserId import rocks.didit.sefilm.database.entities.User import rocks.didit.sefilm.database.repositories.UserRepository +import rocks.didit.sefilm.domain.FilmstadenMembershipId import rocks.didit.sefilm.domain.Foretagsbiljett import rocks.didit.sefilm.domain.PhoneNumber -import rocks.didit.sefilm.domain.SfMembershipId import rocks.didit.sefilm.domain.UserID import rocks.didit.sefilm.domain.dto.* import rocks.didit.sefilm.notification.MailSettings @@ -25,136 +25,136 @@ import java.util.* @Service class UserService( - private val userRepo: UserRepository, - private val pushoverService: PushoverService?, - private val foretagsbiljettService: ForetagsbiljettService + private val userRepo: UserRepository, + private val pushoverService: PushoverService?, + private val foretagsbiljettService: ForetagsbiljettService ) { - private val log: Logger = LoggerFactory.getLogger(this.javaClass) - fun allUsers(): List = userRepo.findAll().map { it.toLimitedUserDTO() } - fun getUser(id: UserID): LimitedUserDTO? = userRepo.findById(id).map { it.toLimitedUserDTO() }.orElse(null) - fun getUserOrThrow(id: UserID): LimitedUserDTO = getUser(id).orElseThrow { NotFoundException("user", id) } - fun getUsersThatWantToBeNotified(knownRecipients: List): List { - return knownRecipients.let { - when (it.isEmpty()) { - true -> userRepo.findAll() - false -> userRepo.findAllById(it) - } - }.filter {user -> - user.notificationSettings.let { s -> - s.notificationsEnabled && s.providerSettings.any { it.enabled } - } + private val log: Logger = LoggerFactory.getLogger(this.javaClass) + fun allUsers(): List = userRepo.findAll().map { it.toLimitedUserDTO() } + fun getUser(id: UserID): LimitedUserDTO? = userRepo.findById(id).map { it.toLimitedUserDTO() }.orElse(null) + fun getUserOrThrow(id: UserID): LimitedUserDTO = getUser(id).orElseThrow { NotFoundException("user", id) } + fun getUsersThatWantToBeNotified(knownRecipients: List): List { + return knownRecipients.let { + when (it.isEmpty()) { + true -> userRepo.findAll() + false -> userRepo.findAllById(it) + } + }.filter { user -> + user.notificationSettings.let { s -> + s.notificationsEnabled && s.providerSettings.any { it.enabled } + } + } } - } - /** Get the full user with all fields. Use with care since this contains sensitive fields */ - fun getCompleteUser(id: UserID): User - = userRepo.findById(id) - .orElseThrow { NotFoundException("user", userID = id) } + /** Get the full user with all fields. Use with care since this contains sensitive fields */ + fun getCompleteUser(id: UserID): User = userRepo.findById(id) + .orElseThrow { NotFoundException("user", userID = id) } - fun getCurrentUser(): UserDTO { - return currentLoggedInUser().let { - getCompleteUser(it).toDTO() + fun getCurrentUser(): UserDTO { + return currentLoggedInUserId().let { + getCompleteUser(it).toDTO() + } } - } - fun currentUserOrNull(): User? { - val authentication: Authentication? = SecurityContextHolder.getContext().authentication - if (authentication?.isAuthenticated != false) { - return null + fun currentUserOrNull(): User? { + val authentication: Authentication? = SecurityContextHolder.getContext().authentication + if (authentication?.isAuthenticated != false) { + return null + } + + val principal = authentication.principal as OpenIdConnectUserDetails + return getCompleteUser(UserID(principal.userId)) } - val principal = authentication.principal as OpenIdConnectUserDetails - return getCompleteUser(UserID(principal.userId)) - } + private fun getUserEntityForCurrentUser() = userRepo.findById(currentLoggedInUserId()) + .orElseThrow { NotFoundException("current user", currentLoggedInUserId()) } - private fun getUserEntityForCurrentUser() = userRepo.findById(currentLoggedInUser()) - .orElseThrow { NotFoundException("current user", currentLoggedInUser()) } + fun updateUser(newDetails: UserDetailsDTO): UserDTO { + val newFilmstadenMembershipId = when { + newDetails.filmstadenMembershipId == null || newDetails.filmstadenMembershipId.isBlank() -> null + else -> FilmstadenMembershipId.valueOf(newDetails.filmstadenMembershipId) + } - fun updateUser(newDetails: UserDetailsDTO): UserDTO { - val newSfMembershipId = when { - newDetails.sfMembershipId == null || newDetails.sfMembershipId.isBlank() -> null - else -> SfMembershipId.valueOf(newDetails.sfMembershipId) - } + val newPhoneNumber = when { + newDetails.phone == null || newDetails.phone.isBlank() -> null + else -> PhoneNumber(newDetails.phone) + } + + val updatedUser = getUserEntityForCurrentUser().copy( + phone = newPhoneNumber, + nick = newDetails.nick, + filmstadenMembershipId = newFilmstadenMembershipId + ) - val newPhoneNumber = when { - newDetails.phone == null || newDetails.phone.isBlank() -> null - else -> PhoneNumber(newDetails.phone) + return userRepo.save(updatedUser).toDTO() } - val updatedUser = getUserEntityForCurrentUser().copy( - phone = newPhoneNumber, - nick = newDetails.nick, - sfMembershipId = newSfMembershipId - ) + // TODO: listen for PushoverUserKeyInvalid and disable the key + fun updateNotificationSettings(notificationInput: NotificationSettingsInputDTO): UserDTO { + val currentUser = getUserEntityForCurrentUser() - return userRepo.save(updatedUser).toDTO() - } + val mailSettings = notificationInput.mail.let { + MailSettings(it?.enabled ?: false, it?.mailAddress ?: "${currentUser.firstName?.toLowerCase()}@example.org") + } + val pushoverSettings = notificationInput.pushover?.let { + + val validatedUserKeyStatus = + when (it.enabled) { + true -> pushoverService?.validateUserKey(it.userKey, it.device) + ?: PushoverValidationStatus.UNKNOWN + false -> PushoverValidationStatus.UNKNOWN + } + + PushoverSettings(it.enabled, it.userKey, it.device, validatedUserKeyStatus) + } ?: PushoverSettings() + + return currentUser.copy( + notificationSettings = NotificationSettings( + notificationInput.notificationsEnabled, + notificationInput.enabledTypes, + listOf(mailSettings, pushoverSettings)) + ).let { + userRepo.save(it) + }.also { + log.trace("Update notification settings for user={} settings to={}", it.id, it.notificationSettings) + }.toDTO() + } - // TODO: listen for PushoverUserKeyInvalid and disable the key - fun updateNotificationSettings(notificationInput: NotificationSettingsInputDTO): UserDTO { - val currentUser = getUserEntityForCurrentUser() + fun lookupUserFromCalendarFeedId(calendarFeedId: UUID): UserDTO? = userRepo + .findByCalendarFeedId(calendarFeedId) + ?.toDTO() - val mailSettings = notificationInput.mail.let { - MailSettings(it?.enabled ?: false, it?.mailAddress ?: "${currentUser.firstName?.toLowerCase()}@example.org") + fun invalidateCalendarFeedId(): UserDTO { + return getUserEntityForCurrentUser() + .copy(calendarFeedId = UUID.randomUUID()) + .let { userRepo.save(it) }.toDTO() } - val pushoverSettings = notificationInput.pushover?.let { - val validatedUserKeyStatus = - when (it.enabled) { - true -> pushoverService?.validateUserKey(it.userKey, it.device) ?: PushoverValidationStatus.UNKNOWN - false -> PushoverValidationStatus.UNKNOWN - } + fun disableCalendarFeed(): UserDTO { + return getUserEntityForCurrentUser() + .copy(calendarFeedId = null) + .let { userRepo.save(it) }.toDTO() + } + + fun User.toDTO() = UserDTO( + this.id, + this.name, + this.firstName, + this.lastName, + this.nick, + this.email, + this.filmstadenMembershipId?.value, + this.phone?.number, + this.avatar, + this.foretagsbiljetter.map { it.toDTO() }, + this.notificationSettings, + this.lastLogin, + this.signupDate, + this.calendarFeedId + ) - PushoverSettings(it.enabled, it.userKey, it.device, validatedUserKeyStatus) - } ?: PushoverSettings() - - return currentUser.copy( - notificationSettings = NotificationSettings( - notificationInput.notificationsEnabled, - notificationInput.enabledTypes, - listOf(mailSettings, pushoverSettings)) - ).let { - userRepo.save(it) - }.also { - log.trace("Update notification settings for user={} settings to={}", it.id, it.notificationSettings) - }.toDTO() - } - - fun lookupUserFromCalendarFeedId(calendarFeedId: UUID): UserDTO? = userRepo - .findByCalendarFeedId(calendarFeedId) - ?.toDTO() - - fun invalidateCalendarFeedId(): UserDTO { - return getUserEntityForCurrentUser() - .copy(calendarFeedId = UUID.randomUUID()) - .let { userRepo.save(it) }.toDTO() - } - - fun disableCalendarFeed(): UserDTO { - return getUserEntityForCurrentUser() - .copy(calendarFeedId = null) - .let { userRepo.save(it) }.toDTO() - } - - fun User.toDTO() = UserDTO( - this.id, - this.name, - this.firstName, - this.lastName, - this.nick, - this.email, - this.sfMembershipId?.value, - this.phone?.number, - this.avatar, - this.foretagsbiljetter.map { it.toDTO() }, - this.notificationSettings, - this.lastLogin, - this.signupDate, - this.calendarFeedId - ) - - private fun Foretagsbiljett.toDTO(): ForetagsbiljettDTO { - return ForetagsbiljettDTO(this.number.number, this.expires, foretagsbiljettService.getStatusOfTicket(this)) - } + private fun Foretagsbiljett.toDTO(): ForetagsbiljettDTO { + return ForetagsbiljettDTO(this.number.number, this.expires, foretagsbiljettService.getStatusOfTicket(this)) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/external/FilmstadenService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/external/FilmstadenService.kt new file mode 100644 index 000000000..5efeb385e --- /dev/null +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/external/FilmstadenService.kt @@ -0,0 +1,167 @@ +package rocks.didit.sefilm.services.external + +import org.slf4j.LoggerFactory +import org.springframework.cache.annotation.Cacheable +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.HttpEntity +import org.springframework.http.HttpMethod +import org.springframework.stereotype.Service +import org.springframework.web.client.RestTemplate +import org.springframework.web.util.UriComponentsBuilder +import rocks.didit.sefilm.ExternalProviderException +import rocks.didit.sefilm.NotFoundException +import rocks.didit.sefilm.Properties +import rocks.didit.sefilm.database.repositories.MovieRepository +import rocks.didit.sefilm.domain.dto.* +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit +import java.util.* + +@Service +class FilmstadenService( + private val movieRepo: MovieRepository, + private val restTemplate: RestTemplate, + private val httpEntity: HttpEntity, + private val properties: Properties +) { + + companion object { + private const val API_URL = "https://www.filmstaden.se/api" + private const val SHOW_URL = "$API_URL/v2/show/sv/1/200" + private const val CINEMA_URL = "$API_URL/v2/cinema/sv/1/200" + private const val MOVIES_URL = "$API_URL/v2/movie/sv/1/1000" + private const val SEAT_MAP_URL = "$API_URL/v2/show/seats/sv/{cinemaId}/{screenId}" + private val log = LoggerFactory.getLogger(FilmstadenService::class.java) + } + + @Cacheable("filmstadenDates") + fun getShowingDates(filmstadenId: String, cityAlias: String = "GB"): List { + val startTime = currentDateTimeTruncatedToNearestHalfHour() + .format(DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm")) + + val uri = UriComponentsBuilder.fromUriString(SHOW_URL) + .queryParam("filter.countryAlias", "se") + .queryParam("filter.cityAlias", cityAlias) + .queryParam("filter.movieNcgId", filmstadenId) + .queryParam("filter.timeUtc.greaterThanOrEqualTo", startTime) + .build().toUri() + + val responseBody = restTemplate + .exchange(uri, HttpMethod.GET, httpEntity, object : ParameterizedTypeReference() {}) + .body ?: throw ExternalProviderException("[Filmstaden] Response body is null") + + return responseBody.items.map { FilmstadenShowingDTO.from(it) }.sortedBy { it.timeUtc } + } + + fun getLocationsInCity(cityAlias: String = properties.defaultCity): List { + val uri = UriComponentsBuilder.fromUriString(CINEMA_URL) + .queryParam("filter.countryAlias", "se") + .queryParam("filter.cityAlias", cityAlias) + .build().toUri() + + val responseBody = restTemplate + .exchange(uri, HttpMethod.GET, httpEntity, object : ParameterizedTypeReference() {}) + .body ?: throw ExternalProviderException("[Filmstaden] Response body is null") + + return responseBody.items + } + + fun fetchExtendedInfo(filmstadenId: String): FilmstadenExtendedMovieDTO? { + val body = restTemplate.exchange( + "$API_URL/v2/movie/sv/{filmstadenId}", + HttpMethod.GET, + httpEntity, + FilmstadenExtendedMovieDTO::class.java, + filmstadenId + ).body + if (body?.ncgId == null) { + return null + } + return body + } + + fun allMovies(): List { + val responseBody = restTemplate + .exchange(MOVIES_URL, HttpMethod.GET, httpEntity, object : ParameterizedTypeReference() {}) + .body ?: throw ExternalProviderException("[Filmstaden] Response body is null") + + return responseBody.items + } + + // https://www.filmstaden.se/api/v2/ticket/Sys99-SE/AA-1034-201708222100/RE-4HMOMOJFKH?imageContentType=webp + fun fetchTickets(sysId: String, filmstadenShowingId: String, ticketId: String): List { + val url = "$API_URL/v2/ticket/$sysId/$filmstadenShowingId/$ticketId" + + log.debug("Fetching tickets from $url") + return restTemplate + .exchange(url, HttpMethod.GET, httpEntity, object : ParameterizedTypeReference>() {}) + .body ?: listOf() + } + + /** Returns base64 encoding jpeg of the ticket */ + fun fetchBarcode(ticketId: String): String { + val url = "$API_URL/v2/barcode/{ticketId}/128/128" + log.debug("Fetching barcode from $url") + return restTemplate + .exchange(url, HttpMethod.GET, httpEntity, String::class.java, ticketId) + .body + ?.replace("\"", "") + ?: "" + } + + private fun currentDateTimeTruncatedToNearestHalfHour(): ZonedDateTime { + val now = ZonedDateTime.now(ZoneOffset.UTC) + return now.truncatedTo(ChronoUnit.HOURS) + .plusMinutes(30L * (now.minute / 30L)) + } + + fun getFilmstadenBuyLink(movieId: UUID?): String? { + if (movieId == null) { + throw IllegalArgumentException("Missing movie ID") + } + val movie = movieRepo + .findById(movieId) + .orElseThrow { NotFoundException("movie '$movieId") } + + return when { + movie.filmstadenId != null && movie.filmstadenSlug != null -> "https://www.filmstaden.se/film/${movie.filmstadenId}/${movie.filmstadenSlug}" + else -> null + } + } + + @Cacheable("filmstadenSeatMap") + fun getFilmstadenSeatMap(cinemaId: String, screenId: String): List { + log.debug("Fetching seat map from $SEAT_MAP_URL with cinemaId=$cinemaId, screenId=$screenId") + return restTemplate + .exchange( + SEAT_MAP_URL, + HttpMethod.GET, + httpEntity, + object : ParameterizedTypeReference>() {}, + cinemaId, + screenId + ) + .body ?: listOf() + } + + fun fetchFilmstadenShow(filmstadenRemoteEntityId: String): FilmstadenShowDTO { + + val filmstadenShowItemsDTO = restTemplate + .exchange( + "$API_URL/v2/show/sv/1/200?filter.remoteEntityId=AA-1036-201908221930", + HttpMethod.GET, + httpEntity, + object : ParameterizedTypeReference() {} + ).body ?: throw ExternalProviderException("[Filmstaden] Response body is null") + + if(filmstadenShowItemsDTO.totalNbrOfItems != 1) { + throw ExternalProviderException("[Filmstaden] More than one show found in Filmstaden API") + } + + return filmstadenShowItemsDTO.items.first() + + } +} + diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/external/PushoverService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/external/PushoverService.kt index 1aa10f407..d74af3786 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/external/PushoverService.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/services/external/PushoverService.kt @@ -17,98 +17,98 @@ import rocks.didit.sefilm.notification.ProviderHelper @Service @ConditionalOnProperty(prefix = "sefilm.notification.provider.Pushover", name = ["enabled"], matchIfMissing = true, havingValue = "true") class PushoverService( - properties: Properties, - private val restTemplate: RestTemplate, - private val eventPublisher: EventPublisher + properties: Properties, + private val restTemplate: RestTemplate, + private val eventPublisher: EventPublisher ) { - private val log: Logger = LoggerFactory.getLogger(this.javaClass) - private val pushoverSettings: Properties.Pushover = properties.notification.provider.pushover - - fun send(msg: ProviderHelper.NotificationMessage, userKey: String, device: String? = null) { - log.debug("About to send $msg") - - val payload = payloadWithToken(userKey, device) - .copy(message = msg.message, title = msg.title, url = msg.url, url_title = msg.urlTitle) - - val response = postToPushover(pushoverSettings.url, payload) - when { - response.isTokenInvalid() -> { - log.warn("Pushover token is invalid, disabling notification provider. Current token: ${pushoverSettings.apiToken}") - pushoverSettings.enabled = false - return - } - response.isUserKeyInvalid() -> eventPublisher.publish(PushoverUserKeyInvalidEvent(this, payload.user)) - response.status != 1 -> log.warn("Unknown error occurred in Pushover: $response") - else -> log.debug("Successfully notified {} with Pushover. Request ID: {}", payload.user, response.request) + private val log: Logger = LoggerFactory.getLogger(this.javaClass) + private val pushoverSettings: Properties.Pushover = properties.notification.provider.pushover + + fun send(msg: ProviderHelper.NotificationMessage, userKey: String, device: String? = null) { + log.debug("About to send $msg") + + val payload = payloadWithToken(userKey, device) + .copy(message = msg.message, title = msg.title, url = msg.url, url_title = msg.urlTitle) + + val response = postToPushover(pushoverSettings.url, payload) + when { + response.isTokenInvalid() -> { + log.warn("Pushover token is invalid, disabling notification provider. Current token: ${pushoverSettings.apiToken}") + pushoverSettings.enabled = false + return + } + response.isUserKeyInvalid() -> eventPublisher.publish(PushoverUserKeyInvalidEvent(this, payload.user)) + response.status != 1 -> log.warn("Unknown error occurred in Pushover: $response") + else -> log.debug("Successfully notified {} with Pushover. Request ID: {}", payload.user, response.request) + } } - } - - fun validateUserKey(userKey: String, device: String?): PushoverValidationStatus { - val payload = payloadWithToken(userKey, device) - val response = postToPushover(pushoverSettings.validateUrl, payload) - log.debug("Got validation response from Pushover: {}", response) - - return when { - response.isTokenInvalid() -> PushoverValidationStatus.TOKEN_INVALID - !response.isUserKeyInvalid() && response.isDeviceInvalid() -> PushoverValidationStatus.USER_VALID_DEVICE_INVALID - !response.isUserKeyInvalid() && !response.isDeviceInvalid() -> PushoverValidationStatus.USER_AND_DEVICE_VALID - response.isUserKeyInvalid() -> PushoverValidationStatus.USER_INVALID - else -> PushoverValidationStatus.UNKNOWN + + fun validateUserKey(userKey: String, device: String?): PushoverValidationStatus { + val payload = payloadWithToken(userKey, device) + val response = postToPushover(pushoverSettings.validateUrl, payload) + log.debug("Got validation response from Pushover: {}", response) + + return when { + response.isTokenInvalid() -> PushoverValidationStatus.TOKEN_INVALID + !response.isUserKeyInvalid() && response.isDeviceInvalid() -> PushoverValidationStatus.USER_VALID_DEVICE_INVALID + !response.isUserKeyInvalid() && !response.isDeviceInvalid() -> PushoverValidationStatus.USER_AND_DEVICE_VALID + response.isUserKeyInvalid() -> PushoverValidationStatus.USER_INVALID + else -> PushoverValidationStatus.UNKNOWN + } } - } - private fun payloadWithToken(userKey: String, device: String?): PushoverPayload = pushoverSettings.apiToken?.let { - PushoverPayload(token = it, user = userKey, device = device) - } ?: throw MissingAPIKeyException(APIService.Pushover) + private fun payloadWithToken(userKey: String, device: String?): PushoverPayload = pushoverSettings.apiToken?.let { + PushoverPayload(token = it, user = userKey, device = device) + } ?: throw MissingAPIKeyException(APIService.Pushover) - private fun postToPushover(url: String, payload: PushoverPayload): PushoverResponse { - val pushoverResponse = (restTemplate.postForObject(url, payload) - ?: throw IllegalArgumentException("Got null response when POST:ing to Pushover")) - log.trace("Pushover response: {}", pushoverResponse) - return pushoverResponse - } + private fun postToPushover(url: String, payload: PushoverPayload): PushoverResponse { + val pushoverResponse = (restTemplate.postForObject(url, payload) + ?: throw IllegalArgumentException("Got null response when POST:ing to Pushover")) + log.trace("Pushover response: {}", pushoverResponse) + return pushoverResponse + } - private fun PushoverResponse.isUserKeyInvalid() = - this.status != 1 && this.user == "invalid" + private fun PushoverResponse.isUserKeyInvalid() = + this.status != 1 && this.user == "invalid" - private fun PushoverResponse.isDeviceInvalid() = - this.status != 1 && this.devices.isEmpty() + private fun PushoverResponse.isDeviceInvalid() = + this.status != 1 && this.devices.isEmpty() - private fun PushoverResponse.isTokenInvalid() = - this.status != 1 && this.token == "invalid" + private fun PushoverResponse.isTokenInvalid() = + this.status != 1 && this.token == "invalid" - fun isUsable() = pushoverSettings.apiToken != null && pushoverSettings.enabled + fun isUsable() = pushoverSettings.apiToken != null && pushoverSettings.enabled } @JsonInclude(JsonInclude.Include.NON_NULL) private data class PushoverPayload( - val token: String, - val user: String, - val message: String = "", - val title: String? = null, - val url: String? = null, - val url_title: String? = null, - val device: String? = null, - val priority: Int? = null, - val timestamp: Long? = null, - val sound: String? = null + val token: String, + val user: String, + val message: String = "", + val title: String? = null, + val url: String? = null, + val url_title: String? = null, + val device: String? = null, + val priority: Int? = null, + val timestamp: Long? = null, + val sound: String? = null ) private data class PushoverResponse( - val status: Int, - val request: String, - val user: String = "valid", - val errors: List = listOf(), - val devices: List = listOf(), - val device: String? = null, - val group: Int? = null, - val token: String = "valid") + val status: Int, + val request: String, + val user: String = "valid", + val errors: List = listOf(), + val devices: List = listOf(), + val device: String? = null, + val group: Int? = null, + val token: String = "valid") enum class PushoverValidationStatus { - USER_AND_DEVICE_VALID, - USER_VALID_DEVICE_INVALID, - USER_INVALID, - TOKEN_INVALID, - UNKNOWN + USER_AND_DEVICE_VALID, + USER_VALID_DEVICE_INVALID, + USER_INVALID, + TOKEN_INVALID, + UNKNOWN } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/services/external/SFService.kt b/backend/src/main/kotlin/rocks/didit/sefilm/services/external/SFService.kt deleted file mode 100644 index 10df666ef..000000000 --- a/backend/src/main/kotlin/rocks/didit/sefilm/services/external/SFService.kt +++ /dev/null @@ -1,149 +0,0 @@ -package rocks.didit.sefilm.services.external - -import org.slf4j.LoggerFactory -import org.springframework.cache.annotation.Cacheable -import org.springframework.core.ParameterizedTypeReference -import org.springframework.http.HttpEntity -import org.springframework.http.HttpMethod -import org.springframework.stereotype.Service -import org.springframework.web.client.RestTemplate -import org.springframework.web.util.UriComponentsBuilder -import rocks.didit.sefilm.ExternalProviderException -import rocks.didit.sefilm.NotFoundException -import rocks.didit.sefilm.Properties -import rocks.didit.sefilm.database.repositories.MovieRepository -import rocks.didit.sefilm.domain.dto.* -import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.time.temporal.ChronoUnit -import java.util.* - -@Service -class SFService( - private val movieRepo: MovieRepository, - private val restTemplate: RestTemplate, - private val httpEntity: HttpEntity, - private val properties: Properties -) { - - companion object { - private const val API_URL = "https://www.filmstaden.se/api" - private const val SHOW_URL = "$API_URL/v2/show/sv/1/200" - private const val CINEMA_URL = "$API_URL/v2/cinema/sv/1/200" - private const val MOVIES_URL = "$API_URL/v2/movie/sv/1/1000" - private const val SEAT_MAP_URL = "$API_URL/v2/show/seats/sv/{cinemaId}/{screenId}" - private val log = LoggerFactory.getLogger(SFService::class.java) - } - - @Cacheable("sfdates") - fun getShowingDates(sfId: String, cityAlias: String = "GB"): List { - val startTime = currentDateTimeTruncatedToNearestHalfHour() - .format(DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm")) - - val uri = UriComponentsBuilder.fromUriString(SHOW_URL) - .queryParam("filter.countryAlias", "se") - .queryParam("filter.cityAlias", cityAlias) - .queryParam("filter.movieNcgId", sfId) - .queryParam("filter.timeUtc.greaterThanOrEqualTo", startTime) - .build().toUri() - - val responseBody = restTemplate - .exchange(uri, HttpMethod.GET, httpEntity, object : ParameterizedTypeReference() {}) - .body ?: throw ExternalProviderException("[SF] Response body is null") - - return responseBody.items.map { SfShowingDTO.from(it) }.sortedBy { it.timeUtc } - } - - fun getLocationsInCity(cityAlias: String = properties.defaultCity): List { - val uri = UriComponentsBuilder.fromUriString(CINEMA_URL) - .queryParam("filter.countryAlias", "se") - .queryParam("filter.cityAlias", cityAlias) - .build().toUri() - - val responseBody = restTemplate - .exchange(uri, HttpMethod.GET, httpEntity, object : ParameterizedTypeReference() {}) - .body ?: throw ExternalProviderException("[SF] Response body is null") - - return responseBody.items - } - - fun fetchExtendedInfo(sfId: String): SfExtendedMovieDTO? { - val body = restTemplate.exchange( - API_URL + "/v2/movie/sv/{sfId}", - HttpMethod.GET, - httpEntity, - SfExtendedMovieDTO::class.java, - sfId - ).body - if (body?.ncgId == null) { - return null - } - return body - } - - fun allMovies(): List { - val responseBody = restTemplate - .exchange(MOVIES_URL, HttpMethod.GET, httpEntity, object : ParameterizedTypeReference() {}) - .body ?: throw ExternalProviderException("[SF] Response body is null") - - return responseBody.items - } - - // https://www.sf.se/api/v2/ticket/Sys99-SE/AA-1034-201708222100/RE-4HMOMOJFKH?imageContentType=webp - fun fetchTickets(sysId: String, sfShowingId: String, ticketId: String): List { - val url = "$API_URL/v2/ticket/$sysId/$sfShowingId/$ticketId" - - log.debug("Fetching tickets from $url") - return restTemplate - .exchange(url, HttpMethod.GET, httpEntity, object : ParameterizedTypeReference>() {}) - .body ?: listOf() - } - - /** Returns base64 encoding jpeg of the ticket */ - fun fetchBarcode(ticketId: String): String { - val url = "$API_URL/v2/barcode/{ticketId}/128/128" - log.debug("Fetching barcode from $url") - return restTemplate - .exchange(url, HttpMethod.GET, httpEntity, String::class.java, ticketId) - .body - ?.replace("\"", "") - ?: "" - } - - private fun currentDateTimeTruncatedToNearestHalfHour(): ZonedDateTime { - val now = ZonedDateTime.now(ZoneOffset.UTC) - return now.truncatedTo(ChronoUnit.HOURS) - .plusMinutes(30L * (now.minute / 30L)) - } - - fun getSfBuyLink(movieId: UUID?): String? { - if (movieId == null) { - throw IllegalArgumentException("Missing movie ID") - } - val movie = movieRepo - .findById(movieId) - .orElseThrow { NotFoundException("movie '$movieId") } - - return when { - movie.sfId != null && movie.sfSlug != null -> "https://www.sf.se/film/${movie.sfId}/${movie.sfSlug}" - else -> null - } - } - - @Cacheable("sfSeatMap") - fun getSfSeatMap(cinemaId: String, screenId: String): List { - log.debug("Fetching seat map from $SEAT_MAP_URL with cinemaId=$cinemaId, screenId=$screenId") - return restTemplate - .exchange( - SEAT_MAP_URL, - HttpMethod.GET, - httpEntity, - object : ParameterizedTypeReference>() {}, - cinemaId, - screenId - ) - .body ?: listOf() - } -} - diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/utils/MovieFilterUtil.kt b/backend/src/main/kotlin/rocks/didit/sefilm/utils/MovieFilterUtil.kt index 0f767e8d6..3ffe64f98 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/utils/MovieFilterUtil.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/utils/MovieFilterUtil.kt @@ -1,62 +1,62 @@ package rocks.didit.sefilm.utils import org.springframework.stereotype.Component -import rocks.didit.sefilm.domain.dto.SfMovieDTO +import rocks.didit.sefilm.domain.dto.FilmstadenMovieDTO import java.time.LocalDate @Component class MovieFilterUtil { - companion object { - private val STUFF_TO_REMOVE = listOf( - " - Klassiker", - " - Bollywood", - " (Director´s cut)", - " 70 mm", - " - Exklusiv smygpremiär", - " - Smygpremiär", - " - på Filmstaden Råsunda" - ) - - private val GENRES_TO_IGNORE = listOf( - "Opera", - "Balett", - "Konsert", - "Hemlig" - ) - - private val TITLES_TO_IGNORE = listOf( - " (IMAX®)", - " i IMAX", - " VIP", - " nyårskonsert" - ) - } - - fun isTitleUnwanted(title: String): Boolean { - return TITLES_TO_IGNORE.any { - title.contains(it, true) + companion object { + private val STUFF_TO_REMOVE = listOf( + " - Klassiker", + " - Bollywood", + " (Director´s cut)", + " 70 mm", + " - Exklusiv smygpremiär", + " - Smygpremiär", + " - på Filmstaden Råsunda" + ) + + private val GENRES_TO_IGNORE = listOf( + "Opera", + "Balett", + "Konsert", + "Hemlig" + ) + + private val TITLES_TO_IGNORE = listOf( + " (IMAX®)", + " i IMAX", + " VIP", + " nyårskonsert" + ) } - } - fun isMovieUnwantedBasedOnGenre(genres: Collection): Boolean { - return genres - .intersect(GENRES_TO_IGNORE) - .isNotEmpty() - } + fun isTitleUnwanted(title: String): Boolean { + return TITLES_TO_IGNORE.any { + title.contains(it, true) + } + } - fun titleRequiresTrimming(title: String) = - STUFF_TO_REMOVE.any { - title.contains(it) + fun isMovieUnwantedBasedOnGenre(genres: Collection): Boolean { + return genres + .intersect(GENRES_TO_IGNORE) + .isNotEmpty() } - fun trimTitle(title: String): String { - var trimmedTitle = title - STUFF_TO_REMOVE.forEach { - trimmedTitle = trimmedTitle.replace(it, "") + fun titleRequiresTrimming(title: String) = + STUFF_TO_REMOVE.any { + title.contains(it) + } + + fun trimTitle(title: String): String { + var trimmedTitle = title + STUFF_TO_REMOVE.forEach { + trimmedTitle = trimmedTitle.replace(it, "") + } + return trimmedTitle.trim() } - return trimmedTitle.trim() - } - fun isNewerThan(movie: SfMovieDTO, thisDate: LocalDate = LocalDate.now().minusMonths(1)) = - movie.releaseDate.isAfter(thisDate) + fun isNewerThan(movie: FilmstadenMovieDTO, thisDate: LocalDate = LocalDate.now().minusMonths(1)) = + movie.releaseDate.isAfter(thisDate) } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/utils/SwishUtil.kt b/backend/src/main/kotlin/rocks/didit/sefilm/utils/SwishUtil.kt index 19c5cfd85..ff5b57e81 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/utils/SwishUtil.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/utils/SwishUtil.kt @@ -8,29 +8,29 @@ import rocks.didit.sefilm.domain.StringValue import rocks.didit.sefilm.domain.SwishDataDTO class SwishUtil { - companion object { - fun constructSwishUri( - showing: Showing, - payeePhone: PhoneNumber, - participantInfo: ParticipantPaymentInfo, - movieTitle: String - ): String { - return SwishDataDTO( - payee = StringValue(payeePhone.number), - amount = IntValue(participantInfo.amountOwed.toKronor()), - message = generateSwishMessage(movieTitle, showing) - ) - .generateUri() - .toASCIIString() - } + companion object { + fun constructSwishUri( + showing: Showing, + payeePhone: PhoneNumber, + participantInfo: ParticipantPaymentInfo, + movieTitle: String + ): String { + return SwishDataDTO( + payee = StringValue(payeePhone.number), + amount = IntValue(participantInfo.amountOwed.toKronor()), + message = generateSwishMessage(movieTitle, showing) + ) + .generateUri() + .toASCIIString() + } - private fun generateSwishMessage(movieTitle: String, showing: Showing): StringValue { - val timeAndDate = " @ ${showing.date} ${showing.time}" - val maxSize = Math.min(movieTitle.length, 50 - timeAndDate.length) - val truncatedMovieTitle = movieTitle.substring(0, maxSize) + private fun generateSwishMessage(movieTitle: String, showing: Showing): StringValue { + val timeAndDate = " @ ${showing.date} ${showing.time}" + val maxSize = Math.min(movieTitle.length, 50 - timeAndDate.length) + val truncatedMovieTitle = movieTitle.substring(0, maxSize) - return StringValue("$truncatedMovieTitle$timeAndDate") - } + return StringValue("$truncatedMovieTitle$timeAndDate") + } - } + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/CalendarController.kt b/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/CalendarController.kt index 712318559..c44b858bc 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/CalendarController.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/CalendarController.kt @@ -14,15 +14,15 @@ import javax.servlet.http.HttpServletRequest @RequestMapping(CalendarController.PATH) class CalendarController(private val calendarService: CalendarService) { - companion object { - const val PATH = "${Application.API_BASE_PATH}/ical" - const val FEED_PATH = "/{feedId}" - private val log = LoggerFactory.getLogger(CalendarController::class.java) - } + companion object { + const val PATH = "${Application.API_BASE_PATH}/ical" + const val FEED_PATH = "/{feedId}" + private val log = LoggerFactory.getLogger(CalendarController::class.java) + } - @GetMapping(FEED_PATH, produces = ["text/calendar; charset=utf-8"]) - fun calendarFeedForUser(@PathVariable feedId: UUID, req: HttpServletRequest): String { - log.debug("Calendar request from: ${req.remoteAddr}:${req.remotePort} for $feedId") - return calendarService.getCalendarFeed(feedId).write() - } + @GetMapping(FEED_PATH, produces = ["text/calendar; charset=utf-8"]) + fun calendarFeedForUser(@PathVariable feedId: UUID, req: HttpServletRequest): String { + log.debug("Calendar request from: ${req.remoteAddr}:${req.remotePort} for $feedId") + return calendarService.getCalendarFeed(feedId).write() + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/ErrorController.kt b/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/ErrorController.kt index e4c0e65ab..68dec182d 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/ErrorController.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/ErrorController.kt @@ -10,36 +10,36 @@ import java.util.* @RestController class ErrorController(private val errorAttributes: ErrorAttributes) : ErrorController { - companion object { - private const val PATH = "/error" - } - - @RequestMapping(value = [PATH]) - internal fun error(request: WebRequest): ErrorDTO { - val errorAttribs = getErrorAttributes(request, true) - val exception = getError(request) - val reason = exception?.cause?.message - ?: exception?.message - ?: errorAttribs["message"] as String - - return ErrorDTO( - reason = reason, - status_code = errorAttribs["status"] as Int, - status_text = errorAttribs["error"] as String, - timestamp = (errorAttribs["timestamp"] as Date).toInstant() - ) - } - - override fun getErrorPath(): String { - return PATH - } - - private fun getErrorAttributes(request: WebRequest, includeStackTrace: Boolean): Map { - return errorAttributes.getErrorAttributes(request, includeStackTrace) - } - - private fun getError(request: WebRequest): Throwable? { - return errorAttributes.getError(request) - } + companion object { + private const val PATH = "/error" + } + + @RequestMapping(value = [PATH]) + internal fun error(request: WebRequest): ErrorDTO { + val errorAttribs = getErrorAttributes(request, true) + val exception = getError(request) + val reason = exception?.cause?.message + ?: exception?.message + ?: errorAttribs["message"] as String + + return ErrorDTO( + reason = reason, + status_code = errorAttribs["status"] as Int, + status_text = errorAttribs["error"] as String, + timestamp = (errorAttribs["timestamp"] as Date).toInstant() + ) + } + + override fun getErrorPath(): String { + return PATH + } + + private fun getErrorAttributes(request: WebRequest, includeStackTrace: Boolean): Map { + return errorAttributes.getErrorAttributes(request, includeStackTrace) + } + + private fun getError(request: WebRequest): Throwable? { + return errorAttributes.getError(request) + } } diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/ExceptionAdvice.kt b/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/ExceptionAdvice.kt index 1acd66500..548d7c99a 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/ExceptionAdvice.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/ExceptionAdvice.kt @@ -9,14 +9,14 @@ import rocks.didit.sefilm.domain.ErrorDTO @ControllerAdvice class ExceptionAdvice { - @ExceptionHandler(IllegalArgumentException::class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ResponseBody - fun handleIllegalArgumentException(e: IllegalArgumentException): ErrorDTO { - return ErrorDTO( - status_code = HttpStatus.BAD_REQUEST.value(), - status_text = HttpStatus.BAD_REQUEST.reasonPhrase, - reason = e.localizedMessage - ) - } + @ExceptionHandler(IllegalArgumentException::class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + fun handleIllegalArgumentException(e: IllegalArgumentException): ErrorDTO { + return ErrorDTO( + status_code = HttpStatus.BAD_REQUEST.value(), + status_text = HttpStatus.BAD_REQUEST.reasonPhrase, + reason = e.localizedMessage + ) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/MetaController.kt b/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/MetaController.kt index f26a4d18a..550f5bbc1 100644 --- a/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/MetaController.kt +++ b/backend/src/main/kotlin/rocks/didit/sefilm/web/controllers/MetaController.kt @@ -18,34 +18,34 @@ import java.util.* @RestController @RequestMapping(PATH) class MetaController( - private val showingService: ShowingService, - private val movieService: MovieService + private val showingService: ShowingService, + private val movieService: MovieService ) { - companion object { - const val PATH = "${Application.API_BASE_PATH}/meta" - } + companion object { + const val PATH = "${Application.API_BASE_PATH}/meta" + } - @GetMapping("/showings/{webId}/{slug}") - fun showingMeta(@PathVariable webId: String, @PathVariable slug: String): ModelAndView { - val showing = showingService.getShowing(Base64ID(webId)) ?: throw NotFoundException("showing with WebID=$webId") - return createViewModelFromShowing(showing) - } + @GetMapping("/showings/{webId}/{slug}") + fun showingMeta(@PathVariable webId: String, @PathVariable slug: String): ModelAndView { + val showing = showingService.getShowing(Base64ID(webId)) ?: throw NotFoundException("showing with WebID=$webId") + return createViewModelFromShowing(showing) + } - @GetMapping("/showings/{showingId}") - fun showingMeta(@PathVariable showingId: UUID): ModelAndView { - val showing = showingService.getShowingOrThrow(showingId) - return createViewModelFromShowing(showing) - } + @GetMapping("/showings/{showingId}") + fun showingMeta(@PathVariable showingId: UUID): ModelAndView { + val showing = showingService.getShowingOrThrow(showingId) + return createViewModelFromShowing(showing) + } - private fun createViewModelFromShowing(showing: ShowingDTO): ModelAndView { - val movie = movieService.getMovieOrThrow(showing.movieId) - return ModelAndView("metaShowing") - .addObject("movieTitle", movie.title) - .addObject("movieDescription", movie.synopsis ?: "Come watch ${movie.title} with us") - .addObject("date", showing.fullDate().format(DateTimeFormatter.ofPattern("eeee d MMM HH:mm", Locale.ENGLISH))) - .addObject("hasPoster", movie.poster != null) - .addObject("posterUrl", movie.poster ?: "") - .addObject("slug", showing.slug) - .addObject("webId", showing.webId.id) - } + private fun createViewModelFromShowing(showing: ShowingDTO): ModelAndView { + val movie = movieService.getMovieOrThrow(showing.movieId) + return ModelAndView("metaShowing") + .addObject("movieTitle", movie.title) + .addObject("movieDescription", movie.synopsis ?: "Come watch ${movie.title} with us") + .addObject("date", showing.fullDate().format(DateTimeFormatter.ofPattern("eeee d MMM HH:mm", Locale.ENGLISH))) + .addObject("hasPoster", movie.poster != null) + .addObject("posterUrl", movie.poster ?: "") + .addObject("slug", showing.slug) + .addObject("webId", showing.webId.id) + } } \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 696a8789b..0f586b588 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -13,7 +13,7 @@ spring: serialization: write-dates-as-timestamps: false cache: - cache-names: sfdates,sfdate,sfSeatMap + cache-names: filmstadenDates,filmstadenSeatMap caffeine: spec: maximumSize=500,expireAfterWrite=43200s @@ -28,6 +28,9 @@ sefilm: clientId: 692064172675-montab9pi57304e68r932c6lm7111oaf.apps.googleusercontent.com jwkUri: https://www.googleapis.com/oauth2/v3/certs + # Whetever or not tickets should be auto assigned to other than admin + enableReassignment: ${enableReassignment:true} + # Whether or not data should be seeded on application startup enableSeeding: ${enableSeeding:true} defaultCity: ${defaultCity:GB} diff --git a/backend/src/main/resources/graphql/location.graphqls b/backend/src/main/resources/graphql/location.graphqls index 72d4d0847..f100826b5 100644 --- a/backend/src/main/resources/graphql/location.graphqls +++ b/backend/src/main/resources/graphql/location.graphqls @@ -10,7 +10,7 @@ type Location { alias: [String!]! } -type SfCity { +type FilmstadenCity { name: String! alias: String! } \ No newline at end of file diff --git a/backend/src/main/resources/graphql/movie.graphqls b/backend/src/main/resources/graphql/movie.graphqls index 91bb8c689..b82029e69 100644 --- a/backend/src/main/resources/graphql/movie.graphqls +++ b/backend/src/main/resources/graphql/movie.graphqls @@ -1,8 +1,8 @@ type Movie { id: UUID! imdbId: IMDbID - sfId: String - sfSlug: String + filmstadenId: String + filmstadenSlug: String tmdbId: TMDbID title: String! synopsis: String @@ -16,6 +16,6 @@ type Movie { popularity: Float! popularityLastUpdated: String! archived: Boolean! - sfShowings(city: String = "GB", afterDate: LocalDate): [SfShowing!]! + filmstadenShowings(city: String = "GB", afterDate: LocalDate): [FilmstadenShowing!]! } diff --git a/backend/src/main/resources/graphql/mutations.graphqls b/backend/src/main/resources/graphql/mutations.graphqls index 06a95e02b..529a4c748 100644 --- a/backend/src/main/resources/graphql/mutations.graphqls +++ b/backend/src/main/resources/graphql/mutations.graphqls @@ -9,7 +9,7 @@ type Mutation { # Delete a showign and return all public showings deleteShowing(showingId: UUID!): [Showing!] - markAsBought(showingId: UUID!): Showing! + markAsBought(showingId: UUID!, price: SEK!): Showing! processTicketUrls(showingId: UUID!, ticketUrls: [String!]): Showing! updateShowing(showingId: UUID!, newValues: UpdateShowingInput): Showing! @@ -18,8 +18,8 @@ type Mutation { ## // showing ### - # Fetch any new movies from SF, returns the movies that were added - fetchNewMoviesFromSf: [Movie!] + # Fetch any new movies from Filmstaden, returns the movies that were added + fetchNewMoviesFromFilmstaden: [Movie!] updateParticipantPaymentInfo(paymentInfo: ParticipantPaymentInput!): ParticipantPaymentInfo! diff --git a/backend/src/main/resources/graphql/payment.graphqls b/backend/src/main/resources/graphql/payment.graphqls index d9234a92c..a5f8fdd3e 100644 --- a/backend/src/main/resources/graphql/payment.graphqls +++ b/backend/src/main/resources/graphql/payment.graphqls @@ -1,6 +1,6 @@ type AdminPaymentDetails { - sfBuyLink: String - sfData: [SfData!]! + filmstadenBuyLink: String + filmstadenData: [FilmstadenData!]! participantPaymentInfos: [ParticipantPaymentInfo!]! } @@ -12,9 +12,9 @@ type AttendeePaymentDetails { swishLink: String } -type SfData { +type FilmstadenData { user: User! - sfMembershipId: String + filmstadenMembershipId: String foretagsbiljett: String } diff --git a/backend/src/main/resources/graphql/queries.graphqls b/backend/src/main/resources/graphql/queries.graphqls index a5b11fee0..29055d071 100644 --- a/backend/src/main/resources/graphql/queries.graphqls +++ b/backend/src/main/resources/graphql/queries.graphqls @@ -21,7 +21,7 @@ type Query { ## Location ## previousLocations: [Location!]! location(id: String!): Location - sfCities: [SfCity!]! + filmstadenCities: [FilmstadenCity!]! ## Notifications ## allNotificationProviders: [NotificationProvider!]! diff --git a/backend/src/main/resources/graphql/showing.graphqls b/backend/src/main/resources/graphql/showing.graphqls index daa808eee..b66579ef1 100644 --- a/backend/src/main/resources/graphql/showing.graphqls +++ b/backend/src/main/resources/graphql/showing.graphqls @@ -6,7 +6,7 @@ type Showing { time: String! movie: Movie! location: Location! - sfScreen: SfScreen + filmstadenScreen: FilmstadenScreen private: Boolean! price: SEK ticketsBought: Boolean! @@ -15,7 +15,8 @@ type Showing { expectedBuyDate: String participants: [Participant!]! lastModifiedDate: String! - sfSeatMap: [SfSeatMap!]! + filmstadenSeatMap: [FilmstadenSeatMap!]! + filmstadenRemoteEntityId: String } type Participant { @@ -23,33 +24,34 @@ type Participant { paymentType: String! } -type SfScreen { - sfId: ID! +type FilmstadenScreen { + filmstadenId: ID! name: String! } -type SfShowing { - cinemaName: String, - screen: SfScreen, - seatCount: Int, - timeUtc: String, - tags: [String!]! +type FilmstadenShowing { + cinemaName: String!, + screen: FilmstadenScreen!, + seatCount: Int!, + timeUtc: String!, + tags: [String!]!, + filmstadenRemoteEntityId: String! } -type SfSeatMap { +type FilmstadenSeatMap { row: Int! number: Int! seatType: String! - coordinates: SfSeatCoordinates! - dimensions: SfSeatDimensions! + coordinates: FilmstadenSeatCoordinates! + dimensions: FilmstadenSeatDimensions! } -type SfSeatCoordinates { +type FilmstadenSeatCoordinates { x: Float! y: Float! } -type SfSeatDimensions { +type FilmstadenSeatDimensions { width: Int! height: Int! } diff --git a/backend/src/main/resources/graphql/showingInput.graphqls b/backend/src/main/resources/graphql/showingInput.graphqls index 59342c22e..8120830db 100644 --- a/backend/src/main/resources/graphql/showingInput.graphqls +++ b/backend/src/main/resources/graphql/showingInput.graphqls @@ -3,8 +3,9 @@ input CreateShowingInput { time: LocalTime! movieId: UUID! location: String! - sfScreen: SfScreenInput + filmstadenScreen: FilmstadenScreenInput expectedBuyDate: LocalDate + filmstadenRemoteEntityId: String } input UpdateShowingInput { @@ -13,11 +14,12 @@ input UpdateShowingInput { payToUser: ID! expectedBuyDate: LocalDate location: String! - sfScreen: SfScreenInput + filmstadenRemoteEntityId: String time: LocalTime! + date: LocalDate! } -input SfScreenInput { - sfId: String! +input FilmstadenScreenInput { + filmstadenId: String! name: String } diff --git a/backend/src/main/resources/graphql/ticket.graphqls b/backend/src/main/resources/graphql/ticket.graphqls index 3796dec18..8ca392c79 100644 --- a/backend/src/main/resources/graphql/ticket.graphqls +++ b/backend/src/main/resources/graphql/ticket.graphqls @@ -1,4 +1,4 @@ -# A ticket assigned to a user that allowes access to SF +# A ticket assigned to a user that allowes access to Filmstaden type Ticket { id: String! showingId: UUID! diff --git a/backend/src/main/resources/graphql/user.graphqls b/backend/src/main/resources/graphql/user.graphqls index 652552c55..1cb9ca885 100644 --- a/backend/src/main/resources/graphql/user.graphqls +++ b/backend/src/main/resources/graphql/user.graphqls @@ -5,7 +5,7 @@ type CurrentUser { lastName: String nick: String email: String! - sfMembershipId: String + filmstadenMembershipId: String phone: String avatar: String foretagsbiljetter: [Foretagsbiljett!] @@ -41,7 +41,7 @@ enum ForetagsbiljettStatus { input NewUserInfo { nick: String - sfMembershipId: String + filmstadenMembershipId: String phone: String } diff --git a/backend/src/main/resources/seeds/sf-cities.json b/backend/src/main/resources/seeds/filmstaden-cities.json similarity index 100% rename from backend/src/main/resources/seeds/sf-cities.json rename to backend/src/main/resources/seeds/filmstaden-cities.json diff --git a/backend/src/main/resources/seeds/sf-location-aliases.json b/backend/src/main/resources/seeds/filmstaden-location-aliases.json similarity index 60% rename from backend/src/main/resources/seeds/sf-location-aliases.json rename to backend/src/main/resources/seeds/filmstaden-location-aliases.json index 64c18d41e..b6caafdc5 100644 --- a/backend/src/main/resources/seeds/sf-location-aliases.json +++ b/backend/src/main/resources/seeds/filmstaden-location-aliases.json @@ -1,12 +1,12 @@ [ { - "sfId": "NCG12773", + "filmstadenId": "NCG12773", "alias": [ "Bergakungen" ] }, { - "sfId": "NCG55655", + "filmstadenId": "NCG55655", "alias": [ "Göta" ] diff --git a/backend/src/test/kotlin/rocks/didit/sefilm/domain/Base64IDTest.kt b/backend/src/test/kotlin/rocks/didit/sefilm/domain/Base64IDTest.kt index 3205bdc45..5dcaaf9c6 100644 --- a/backend/src/test/kotlin/rocks/didit/sefilm/domain/Base64IDTest.kt +++ b/backend/src/test/kotlin/rocks/didit/sefilm/domain/Base64IDTest.kt @@ -4,14 +4,14 @@ import org.junit.Assert import org.junit.Test internal class Base64IDTest { - companion object { - const val STANDARD_ID_LENGTH = 7 - } + companion object { + const val STANDARD_ID_LENGTH = 7 + } - @Test - fun testLengthOfRandomId() { - val randomId = Base64ID.random() - println("Randomized ID: $randomId") - Assert.assertEquals(STANDARD_ID_LENGTH, randomId.id.length) - } + @Test + fun testLengthOfRandomId() { + val randomId = Base64ID.random() + println("Randomized ID: $randomId") + Assert.assertEquals(STANDARD_ID_LENGTH, randomId.id.length) + } } \ No newline at end of file diff --git a/backend/src/test/kotlin/rocks/didit/sefilm/domain/FilmstadenMembershipIdTest.kt b/backend/src/test/kotlin/rocks/didit/sefilm/domain/FilmstadenMembershipIdTest.kt new file mode 100644 index 000000000..b5d4f57e6 --- /dev/null +++ b/backend/src/test/kotlin/rocks/didit/sefilm/domain/FilmstadenMembershipIdTest.kt @@ -0,0 +1,53 @@ +package rocks.didit.sefilm.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +class FilmstadenMembershipIdTest { + @Test + fun testMaxLength() { + val filmstadenMembershipId = FilmstadenMembershipId("xxx-xxx") + assertThat(filmstadenMembershipId.value.length).isEqualTo(7) + assertThat(filmstadenMembershipId.value).isEqualTo("xxx-xxx") + } + + @Test + fun testMinLength() { + val filmstadenMembershipId = FilmstadenMembershipId("xxxxxx") + assertThat(filmstadenMembershipId.value.length).isEqualTo(6) + assertThat(filmstadenMembershipId.value).isEqualTo("xxxxxx") + } + + @Test + fun testTooShortLength() { + val e = assertThrows(IllegalArgumentException::class.java) { FilmstadenMembershipId("12345") } + assertThat(e).hasMessageContaining("The Filmstaden membership id has wrong size. Expected 6-7, got 5") + } + + @Test + fun testTooLongLength() { + val e = assertThrows(IllegalArgumentException::class.java) { FilmstadenMembershipId("12345678") } + assertThat(e).hasMessageContaining("The Filmstaden membership id has wrong size. Expected 6-7, got 8") + } + + @Test + fun testFormat() { + val e = assertThrows(IllegalArgumentException::class.java) { FilmstadenMembershipId("123+456") } + assertThat(e).hasMessageContaining("is an invalid membership id. Expected XXX-XXX") + } + + @Test + fun testValueOfWithoutDash() { + val profileId = "asdfgh" + val membershipId = FilmstadenMembershipId.valueOf(profileId) + assertThat(membershipId.value).isEqualTo("asd-fgh") + } + + @Test + fun testValueOfWithDash() { + val profileId = "asd-fgh" + val membershipId = FilmstadenMembershipId.valueOf(profileId) + assertThat(membershipId.value).isEqualTo("asd-fgh") + } +} \ No newline at end of file diff --git a/backend/src/test/kotlin/rocks/didit/sefilm/domain/SfMembershipIdTest.kt b/backend/src/test/kotlin/rocks/didit/sefilm/domain/SfMembershipIdTest.kt deleted file mode 100644 index f0cb081d1..000000000 --- a/backend/src/test/kotlin/rocks/didit/sefilm/domain/SfMembershipIdTest.kt +++ /dev/null @@ -1,53 +0,0 @@ -package rocks.didit.sefilm.domain - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Test - -class SfMembershipIdTest { - @Test - fun testMaxLength() { - val sfMembershipId = SfMembershipId("xxx-xxx") - assertThat(sfMembershipId.value.length).isEqualTo(7) - assertThat(sfMembershipId.value).isEqualTo("xxx-xxx") - } - - @Test - fun testMinLength() { - val sfMembershipId = SfMembershipId("xxxxxx") - assertThat(sfMembershipId.value.length).isEqualTo(6) - assertThat(sfMembershipId.value).isEqualTo("xxxxxx") - } - - @Test - fun testTooShortLength() { - val e = assertThrows(IllegalArgumentException::class.java, { SfMembershipId("12345") }) - assertThat(e).hasMessageContaining("The SF membership id has wrong size. Expected 6-7, got 5") - } - - @Test - fun testTooLongLength() { - val e = assertThrows(IllegalArgumentException::class.java, { SfMembershipId("12345678") }) - assertThat(e).hasMessageContaining("The SF membership id has wrong size. Expected 6-7, got 8") - } - - @Test - fun testFormat() { - val e = assertThrows(IllegalArgumentException::class.java, { SfMembershipId("123+456") }) - assertThat(e).hasMessageContaining("is an invalid membership id. Expected XXX-XXX") - } - - @Test - fun testValueOfWithoutDash() { - val profileId = "asdfgh" - val membershipId = SfMembershipId.valueOf(profileId) - assertThat(membershipId.value).isEqualTo("asd-fgh") - } - - @Test - fun testValueOfWithDash() { - val profileId = "asd-fgh" - val membershipId = SfMembershipId.valueOf(profileId) - assertThat(membershipId.value).isEqualTo("asd-fgh") - } -} \ No newline at end of file diff --git a/backend/src/test/kotlin/rocks/didit/sefilm/managers/SlugServiceTest.kt b/backend/src/test/kotlin/rocks/didit/sefilm/managers/SlugServiceTest.kt index 76ba183a5..53a192c40 100644 --- a/backend/src/test/kotlin/rocks/didit/sefilm/managers/SlugServiceTest.kt +++ b/backend/src/test/kotlin/rocks/didit/sefilm/managers/SlugServiceTest.kt @@ -15,203 +15,203 @@ import rocks.didit.sefilm.services.SlugService @RunWith(MockitoJUnitRunner.StrictStubs::class) class SlugServiceTest { - @Mock - private lateinit var movieServiceoMock: MovieService + @Mock + private lateinit var movieServiceoMock: MovieService - @InjectMocks - private lateinit var slugService: SlugService + @InjectMocks + private lateinit var slugService: SlugService - @Test - fun testGenerateSlugForShowing() { - val expectedSlugs = listOf( - Pair("Atomic Blonde", "atomic-blonde"), - Pair("A United Kingdom", "a-united-kingdom"), - Pair("Alien", "alien"), - Pair("Alien: Covenant", "alien-covenant"), - Pair("All Eyez on Me", "all-eyez-on-me"), - Pair("Annabelle: Creation", "annabelle-creation"), - Pair("Apornas planet: Striden", "apornas-planet-striden"), - Pair("Baby Driver", "baby-driver"), - Pair("Baby-bossen", "babybossen"), - Pair("Bamse och häxans dotter", "bamse-och-haxans-dotter"), - Pair("Baywatch", "baywatch"), - Pair("Bigfoot Junior", "bigfoot-junior"), - Pair("Bilar 3", "bilar-3"), - Pair("Blade Runner 2049", "blade-runner-2049"), - Pair("Borg vs McEnroe", "borg-vs-mcenroe"), - Pair("Brev från månen", "brev-fran-manen"), - Pair("Coco", "coco"), - Pair("Darkland", "darkland"), - Pair("De bedragna", "de-bedragna"), - Pair("Win It All", "win-it-all"), - Pair("Dolda tillgångar", "dolda-tillgangar"), - Pair("Dumma mej 3", "dumma-mej-3"), - Pair("Dunkirk", "dunkirk"), - Pair("En flodhäst i tesalongen", "en-flodhast-i-tesalongen"), - Pair("En obekväm uppföljare", "en-obekvam-uppfoljare"), - Pair("Fast & Furious 8", "fast-and-furious-8"), - Pair("Flyg sa Alfons Åberg", "flyg-sa-alfons-aberg"), - Pair("Frantz", "frantz"), - Pair("För min dotters skull", "for-min-dotters-skull"), - Pair("Galna av lycka", "galna-av-lycka"), - Pair("Get Out", "get-out"), - Pair("Ghost in the Shell", "ghost-in-the-shell"), - Pair("Guardians of the Galaxy, vol. 2", "guardians-of-the-galaxy-vol.-2"), - Pair("Half Girlfriend", "half-girlfriend"), - Pair("Ingenting och allting", "ingenting-och-allting"), - Pair("Inte hela världen", "inte-hela-varlden"), - Pair("It comes at night", "it-comes-at-night"), - Pair("Jacques - Havets utforskare", "jacques--havets-utforskare"), - Pair("Justice League", "justice-league"), - Pair("King Arthur: Legend of the Sword", "king-arthur-legend-of-the-sword"), - Pair("Kingsman: The Golden Circle", "kingsman-the-golden-circle"), - Pair("Klas Klättermus och de andra djuren i Hackebackeskogen", "klas-klattermus-och-de-andra-djuren-i-hackeba"), - Pair("Kong: Skull Island", "kong-skull-island"), - Pair("Kungens val", "kungens-val"), - Pair("Kärleken och evigheten", "karleken-och-evigheten"), - Pair("La La Land", "la-la-land"), - Pair("Life", "life"), - Pair("Lion", "lion"), - Pair("Logan", "logan"), - Pair("Logan Lucky", "logan-lucky"), - Pair("Lost in Paris", "lost-in-paris"), - Pair("Milo - Månvaktaren", "milo--manvaktaren"), - Pair("Paris Can Wait", "paris-can-wait"), - Pair("Pingvinresan 2", "pingvinresan-2"), - Pair("Pirates of the Caribbean: Salazar's Revenge", "pirates-of-the-caribbean-salazars-revenge"), - Pair("Raula", "raula"), - Pair("Rough Night", "rough-night"), - Pair("Rum 213", "rum-213"), - Pair("Sameblod", "sameblod"), - Pair("Skönheten och odjuret", "skonheten-och-odjuret"), - Pair("Sleepless", "sleepless"), - Pair("Smurfs: The Lost Village", "smurfs-the-lost-village"), - Pair("Song to Song", "song-to-song"), - Pair("Souvenir", "souvenir"), - Pair("Spider-Man: Homecoming", "spiderman-homecoming"), - Pair("Star Wars: The Last Jedi", "star-wars-the-last-jedi"), - Pair("Superswede", "superswede"), - Pair("The Belko Experiment", "the-belko-experiment"), - Pair("The Circle", "the-circle"), - Pair("The Dark Tower", "the-dark-tower"), - Pair("The Emoji Movie", "the-emoji-movie"), - Pair("The Handmaiden", "the-handmaiden"), - Pair("The Hitman's Bodyguard", "the-hitmans-bodyguard"), - Pair("The LEGO Ninjago Movie", "the-lego-ninjago-movie"), - Pair("The Mummy", "the-mummy"), - Pair("The Nile Hilton Incident", "the-nile-hilton-incident"), - Pair("The Promise", "the-promise"), - Pair("Their finest hour", "their-finest-hour"), - Pair("Thor: Ragnarok", "thor-ragnarok"), - Pair("Ferdinand", "ferdinand"), - Pair("Transformers: The Last Knight", "transformers-the-last-knight"), - Pair("Tubelight", "tubelight"), - Pair("Tulpanfeber", "tulpanfeber"), - Pair("Ture och Jerry", "ture-och-jerry"), - Pair("Vaiana", "vaiana"), - Pair("Valerian and the City of a Thousand Planets", "valerian-and-the-city-of-a-thousand-planets"), - Pair("Wonder Woman", "wonder-woman"), - Pair("Vår tid ska komma", "var-tid-ska-komma"), - Pair("Your Name", "your-name"), - Pair("Fyren mellan haven", "fyren-mellan-haven"), - Pair("Moonlight", "moonlight"), - Pair("Oskars Amerika", "oskars-amerika"), - Pair("The Bar", "the-bar"), - Pair("Vad döljer du för mig?", "vad-doljer-du-for-mig?"), - Pair("Alfons leker Einstein", "alfons-leker-einstein"), - Pair("Couples Retreat", "couples-retreat"), - Pair("American Made", "american-made"), - Pair("Ballerina", "ballerina"), - Pair("The Midwife", "the-midwife"), - Pair("Becker - Kungen av Tingsryd", "becker--kungen-av-tingsryd"), - Pair("Fireman Sam: Alien Alert!", "fireman-sam-alien-alert!"), - Pair("Electric Bannana Band - En Familjeföreställning", "electric-bannana-band--en-familjeforestallnin"), - Pair("En kvinnas martyrium", "en-kvinnas-martyrium"), - Pair("Euphoria", "euphoria"), - Pair("Fanny's Journey", "fannys-journey"), - Pair("Stefan Zweig: Farewell to Europe", "stefan-zweig-farewell-to-europe"), - Pair("Flatliners", "flatliners"), - Pair("Orphan", "orphan"), - Pair("Guldfeber", "guldfeber"), - Pair("Happy Death Day", "happy-death-day"), - Pair("Heartstone", "heartstone"), - Pair("Hampstead", "hampstead"), - Pair("Hiss till galgen", "hiss-till-galgen"), - Pair("Hockney - Landscape, portraits and still life", "hockney--landscape-portraits-and-still-life"), - Pair("Two Is a Family", "two-is-a-family"), - Pair("Jakten på den försvunna skatten", "jakten-pa-den-forsvunna-skatten"), - Pair("Jordgubbslandet", "jordgubbslandet"), - Pair("Jumanji: Welcome to the Jungle", "jumanji-welcome-to-the-jungle"), - Pair("A War", "a-war"), - Pair("150 Milligrams", "150-milligrams"), - Pair("Lady M", "lady-m"), - Pair("Loving Vincent", "loving-vincent"), - Pair("Matisse", "matisse"), - Pair("Maudie", "maudie"), - Pair("Mordet på Orientexpressen", "mordet-pa-orientexpressen"), - Pair("My Cousin Rachel", "my-cousin-rachel"), - Pair("My Little Pony: The Movie", "my-little-pony-the-movie"), - Pair("Märta Proppmätt", "marta-proppmatt"), - Pair("On the Milky Road", "on-the-milky-road"), - Pair("Ouaga Girls", "ouaga-girls"), - Pair("Paddington 2", "paddington-2"), - Pair("Para knas", "para-knas"), - Pair("Pianot", "pianot"), - Pair("Pitch Perfect 3", "pitch-perfect-3"), - Pair("Pop Aye", "pop-aye"), - Pair("Rovdjuret", "rovdjuret"), - Pair("Silvana - Väck mig när ni vaknat", "silvana--vack-mig-nar-ni-vaknat"), - Pair("Suspiria: Flykten från helvetet", "suspiria-flykten-fran-helvetet"), - Pair("Så länge hjärtat kan slå", "sa-lange-hjartat-kan-sla"), - Pair("The Big Sick", "the-big-sick"), - Pair("The Greatest Showman", "the-greatest-showman"), - Pair("The Man With the Iron Heart", "the-man-with-the-iron-heart"), - Pair("The Mountain Between Us", "the-mountain-between-us"), - Pair("The Shining", "the-shining"), - Pair("The Square", "the-square"), - Pair("The Trip to Spain", "the-trip-to-spain"), - Pair("Tillbaka till Montauk", "tillbaka-till-montauk"), - Pair("Verónica", "veronica"), - Pair("Victoria and Abdul", "victoria-and-abdul"), - Pair("Villebråd", "villebrad"), - Pair("Wish Upon", "wish-upon"), - Pair("År 2001: Ett rymdäventyr", "ar-2001-ett-rymdaventyr"), - Pair("Good Time", "good-time"), - Pair("Neruda", "neruda"), - Pair("Om själ och kropp", "om-sjal-och-kropp"), - Pair("Snömannen", "snomannen"), - Pair("Table 19", "table-19"), - Pair("Ted - För kärlekens skull", "ted--for-karlekens-skull"), - Pair("Trafikljusen blir blå i morgon", "trafikljusen-blir-bla-i-morgon"), - Pair("American Assassin", "american-assassin"), - Pair("Black Panther", "black-panther"), - Pair("Carmen MET", "carmen-met"), - Pair("Ett veck i tiden", "ett-veck-i-tiden"), - Pair("Home Again - Kärleken flyttar in", "home-again--karleken-flyttar-in"), - Pair("Kedi", "kedi"), - Pair("Ready Player One", "ready-player-one"), - Pair("L'empereur", "lempereur"), - Pair("Éternité", "eternite"), - Pair("Teströl és lélekröl", "testrol-es-lelekrol"), - Pair("Winnerbäck - Ett slags liv", "winnerback--ett-slags-liv") - ) + @Test + fun testGenerateSlugForShowing() { + val expectedSlugs = listOf( + Pair("Atomic Blonde", "atomic-blonde"), + Pair("A United Kingdom", "a-united-kingdom"), + Pair("Alien", "alien"), + Pair("Alien: Covenant", "alien-covenant"), + Pair("All Eyez on Me", "all-eyez-on-me"), + Pair("Annabelle: Creation", "annabelle-creation"), + Pair("Apornas planet: Striden", "apornas-planet-striden"), + Pair("Baby Driver", "baby-driver"), + Pair("Baby-bossen", "babybossen"), + Pair("Bamse och häxans dotter", "bamse-och-haxans-dotter"), + Pair("Baywatch", "baywatch"), + Pair("Bigfoot Junior", "bigfoot-junior"), + Pair("Bilar 3", "bilar-3"), + Pair("Blade Runner 2049", "blade-runner-2049"), + Pair("Borg vs McEnroe", "borg-vs-mcenroe"), + Pair("Brev från månen", "brev-fran-manen"), + Pair("Coco", "coco"), + Pair("Darkland", "darkland"), + Pair("De bedragna", "de-bedragna"), + Pair("Win It All", "win-it-all"), + Pair("Dolda tillgångar", "dolda-tillgangar"), + Pair("Dumma mej 3", "dumma-mej-3"), + Pair("Dunkirk", "dunkirk"), + Pair("En flodhäst i tesalongen", "en-flodhast-i-tesalongen"), + Pair("En obekväm uppföljare", "en-obekvam-uppfoljare"), + Pair("Fast & Furious 8", "fast-and-furious-8"), + Pair("Flyg sa Alfons Åberg", "flyg-sa-alfons-aberg"), + Pair("Frantz", "frantz"), + Pair("För min dotters skull", "for-min-dotters-skull"), + Pair("Galna av lycka", "galna-av-lycka"), + Pair("Get Out", "get-out"), + Pair("Ghost in the Shell", "ghost-in-the-shell"), + Pair("Guardians of the Galaxy, vol. 2", "guardians-of-the-galaxy-vol.-2"), + Pair("Half Girlfriend", "half-girlfriend"), + Pair("Ingenting och allting", "ingenting-och-allting"), + Pair("Inte hela världen", "inte-hela-varlden"), + Pair("It comes at night", "it-comes-at-night"), + Pair("Jacques - Havets utforskare", "jacques--havets-utforskare"), + Pair("Justice League", "justice-league"), + Pair("King Arthur: Legend of the Sword", "king-arthur-legend-of-the-sword"), + Pair("Kingsman: The Golden Circle", "kingsman-the-golden-circle"), + Pair("Klas Klättermus och de andra djuren i Hackebackeskogen", "klas-klattermus-och-de-andra-djuren-i-hackeba"), + Pair("Kong: Skull Island", "kong-skull-island"), + Pair("Kungens val", "kungens-val"), + Pair("Kärleken och evigheten", "karleken-och-evigheten"), + Pair("La La Land", "la-la-land"), + Pair("Life", "life"), + Pair("Lion", "lion"), + Pair("Logan", "logan"), + Pair("Logan Lucky", "logan-lucky"), + Pair("Lost in Paris", "lost-in-paris"), + Pair("Milo - Månvaktaren", "milo--manvaktaren"), + Pair("Paris Can Wait", "paris-can-wait"), + Pair("Pingvinresan 2", "pingvinresan-2"), + Pair("Pirates of the Caribbean: Salazar's Revenge", "pirates-of-the-caribbean-salazars-revenge"), + Pair("Raula", "raula"), + Pair("Rough Night", "rough-night"), + Pair("Rum 213", "rum-213"), + Pair("Sameblod", "sameblod"), + Pair("Skönheten och odjuret", "skonheten-och-odjuret"), + Pair("Sleepless", "sleepless"), + Pair("Smurfs: The Lost Village", "smurfs-the-lost-village"), + Pair("Song to Song", "song-to-song"), + Pair("Souvenir", "souvenir"), + Pair("Spider-Man: Homecoming", "spiderman-homecoming"), + Pair("Star Wars: The Last Jedi", "star-wars-the-last-jedi"), + Pair("Superswede", "superswede"), + Pair("The Belko Experiment", "the-belko-experiment"), + Pair("The Circle", "the-circle"), + Pair("The Dark Tower", "the-dark-tower"), + Pair("The Emoji Movie", "the-emoji-movie"), + Pair("The Handmaiden", "the-handmaiden"), + Pair("The Hitman's Bodyguard", "the-hitmans-bodyguard"), + Pair("The LEGO Ninjago Movie", "the-lego-ninjago-movie"), + Pair("The Mummy", "the-mummy"), + Pair("The Nile Hilton Incident", "the-nile-hilton-incident"), + Pair("The Promise", "the-promise"), + Pair("Their finest hour", "their-finest-hour"), + Pair("Thor: Ragnarok", "thor-ragnarok"), + Pair("Ferdinand", "ferdinand"), + Pair("Transformers: The Last Knight", "transformers-the-last-knight"), + Pair("Tubelight", "tubelight"), + Pair("Tulpanfeber", "tulpanfeber"), + Pair("Ture och Jerry", "ture-och-jerry"), + Pair("Vaiana", "vaiana"), + Pair("Valerian and the City of a Thousand Planets", "valerian-and-the-city-of-a-thousand-planets"), + Pair("Wonder Woman", "wonder-woman"), + Pair("Vår tid ska komma", "var-tid-ska-komma"), + Pair("Your Name", "your-name"), + Pair("Fyren mellan haven", "fyren-mellan-haven"), + Pair("Moonlight", "moonlight"), + Pair("Oskars Amerika", "oskars-amerika"), + Pair("The Bar", "the-bar"), + Pair("Vad döljer du för mig?", "vad-doljer-du-for-mig?"), + Pair("Alfons leker Einstein", "alfons-leker-einstein"), + Pair("Couples Retreat", "couples-retreat"), + Pair("American Made", "american-made"), + Pair("Ballerina", "ballerina"), + Pair("The Midwife", "the-midwife"), + Pair("Becker - Kungen av Tingsryd", "becker--kungen-av-tingsryd"), + Pair("Fireman Sam: Alien Alert!", "fireman-sam-alien-alert!"), + Pair("Electric Bannana Band - En Familjeföreställning", "electric-bannana-band--en-familjeforestallnin"), + Pair("En kvinnas martyrium", "en-kvinnas-martyrium"), + Pair("Euphoria", "euphoria"), + Pair("Fanny's Journey", "fannys-journey"), + Pair("Stefan Zweig: Farewell to Europe", "stefan-zweig-farewell-to-europe"), + Pair("Flatliners", "flatliners"), + Pair("Orphan", "orphan"), + Pair("Guldfeber", "guldfeber"), + Pair("Happy Death Day", "happy-death-day"), + Pair("Heartstone", "heartstone"), + Pair("Hampstead", "hampstead"), + Pair("Hiss till galgen", "hiss-till-galgen"), + Pair("Hockney - Landscape, portraits and still life", "hockney--landscape-portraits-and-still-life"), + Pair("Two Is a Family", "two-is-a-family"), + Pair("Jakten på den försvunna skatten", "jakten-pa-den-forsvunna-skatten"), + Pair("Jordgubbslandet", "jordgubbslandet"), + Pair("Jumanji: Welcome to the Jungle", "jumanji-welcome-to-the-jungle"), + Pair("A War", "a-war"), + Pair("150 Milligrams", "150-milligrams"), + Pair("Lady M", "lady-m"), + Pair("Loving Vincent", "loving-vincent"), + Pair("Matisse", "matisse"), + Pair("Maudie", "maudie"), + Pair("Mordet på Orientexpressen", "mordet-pa-orientexpressen"), + Pair("My Cousin Rachel", "my-cousin-rachel"), + Pair("My Little Pony: The Movie", "my-little-pony-the-movie"), + Pair("Märta Proppmätt", "marta-proppmatt"), + Pair("On the Milky Road", "on-the-milky-road"), + Pair("Ouaga Girls", "ouaga-girls"), + Pair("Paddington 2", "paddington-2"), + Pair("Para knas", "para-knas"), + Pair("Pianot", "pianot"), + Pair("Pitch Perfect 3", "pitch-perfect-3"), + Pair("Pop Aye", "pop-aye"), + Pair("Rovdjuret", "rovdjuret"), + Pair("Silvana - Väck mig när ni vaknat", "silvana--vack-mig-nar-ni-vaknat"), + Pair("Suspiria: Flykten från helvetet", "suspiria-flykten-fran-helvetet"), + Pair("Så länge hjärtat kan slå", "sa-lange-hjartat-kan-sla"), + Pair("The Big Sick", "the-big-sick"), + Pair("The Greatest Showman", "the-greatest-showman"), + Pair("The Man With the Iron Heart", "the-man-with-the-iron-heart"), + Pair("The Mountain Between Us", "the-mountain-between-us"), + Pair("The Shining", "the-shining"), + Pair("The Square", "the-square"), + Pair("The Trip to Spain", "the-trip-to-spain"), + Pair("Tillbaka till Montauk", "tillbaka-till-montauk"), + Pair("Verónica", "veronica"), + Pair("Victoria and Abdul", "victoria-and-abdul"), + Pair("Villebråd", "villebrad"), + Pair("Wish Upon", "wish-upon"), + Pair("År 2001: Ett rymdäventyr", "ar-2001-ett-rymdaventyr"), + Pair("Good Time", "good-time"), + Pair("Neruda", "neruda"), + Pair("Om själ och kropp", "om-sjal-och-kropp"), + Pair("Snömannen", "snomannen"), + Pair("Table 19", "table-19"), + Pair("Ted - För kärlekens skull", "ted--for-karlekens-skull"), + Pair("Trafikljusen blir blå i morgon", "trafikljusen-blir-bla-i-morgon"), + Pair("American Assassin", "american-assassin"), + Pair("Black Panther", "black-panther"), + Pair("Carmen MET", "carmen-met"), + Pair("Ett veck i tiden", "ett-veck-i-tiden"), + Pair("Home Again - Kärleken flyttar in", "home-again--karleken-flyttar-in"), + Pair("Kedi", "kedi"), + Pair("Ready Player One", "ready-player-one"), + Pair("L'empereur", "lempereur"), + Pair("Éternité", "eternite"), + Pair("Teströl és lélekröl", "testrol-es-lelekrol"), + Pair("Winnerbäck - Ett slags liv", "winnerback--ett-slags-liv") + ) - expectedSlugs.forEach { - val (movieName, expectedSlug) = it - testGenerateSlugForShowing(movieName, expectedSlug) - } + expectedSlugs.forEach { + val (movieName, expectedSlug) = it + testGenerateSlugForShowing(movieName, expectedSlug) + } - } + } - @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") - private fun testGenerateSlugForShowing(movieName: String, expectedSlug: String) { - val movie = Movie(title = movieName) - val showing = Showing(movie = movie) + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + private fun testGenerateSlugForShowing(movieName: String, expectedSlug: String) { + val movie = Movie(title = movieName) + val showing = Showing(movie = movie) - Mockito.`when`(movieServiceoMock.getMovieOrThrow(Mockito.any())).thenReturn(movie) + Mockito.`when`(movieServiceoMock.getMovieOrThrow(Mockito.any())).thenReturn(movie) - val generatedSlug = slugService.generateSlugFor(showing) - Assert.assertEquals("Expected slug to be $expectedSlug for $movieName", expectedSlug, generatedSlug) - } + val generatedSlug = slugService.generateSlugFor(showing) + Assert.assertEquals("Expected slug to be $expectedSlug for $movieName", expectedSlug, generatedSlug) + } } \ No newline at end of file diff --git a/backend/src/test/kotlin/rocks/didit/sefilm/utils/MovieFilterUtilTest.kt b/backend/src/test/kotlin/rocks/didit/sefilm/utils/MovieFilterUtilTest.kt index d0986daef..8e0fe044a 100644 --- a/backend/src/test/kotlin/rocks/didit/sefilm/utils/MovieFilterUtilTest.kt +++ b/backend/src/test/kotlin/rocks/didit/sefilm/utils/MovieFilterUtilTest.kt @@ -5,45 +5,45 @@ import org.junit.jupiter.api.Test internal class MovieFilterUtilTest { - @Test - fun `Title is unwanted if it contains IMAX`() { - val movieFilter = MovieFilterUtil() - val title = "Fifty Shades Freed i IMAX" - assertThat(movieFilter.isTitleUnwanted(title)).isTrue() - } - - @Test - fun `Title is wanted if does NOT contain IMAX`() { - val movieFilter = MovieFilterUtil() - val title = "Fifty Shades Freed" - assertThat(movieFilter.isTitleUnwanted(title)).isFalse() - } - - @Test - fun `Movie is wanted`() { - val movieFilter = MovieFilterUtil() - val genres = listOf("Sci-Fi", "Action") - assertThat(movieFilter.isMovieUnwantedBasedOnGenre(genres)).isFalse() - } - - @Test - fun `Movie is unwanted if Balett`() { - val movieFilter = MovieFilterUtil() - val genres = listOf("Balett") - assertThat(movieFilter.isMovieUnwantedBasedOnGenre(genres)).isTrue() - } - - @Test + @Test + fun `Title is unwanted if it contains IMAX`() { + val movieFilter = MovieFilterUtil() + val title = "Fifty Shades Freed i IMAX" + assertThat(movieFilter.isTitleUnwanted(title)).isTrue() + } + + @Test + fun `Title is wanted if does NOT contain IMAX`() { + val movieFilter = MovieFilterUtil() + val title = "Fifty Shades Freed" + assertThat(movieFilter.isTitleUnwanted(title)).isFalse() + } + + @Test + fun `Movie is wanted`() { + val movieFilter = MovieFilterUtil() + val genres = listOf("Sci-Fi", "Action") + assertThat(movieFilter.isMovieUnwantedBasedOnGenre(genres)).isFalse() + } + + @Test + fun `Movie is unwanted if Balett`() { + val movieFilter = MovieFilterUtil() + val genres = listOf("Balett") + assertThat(movieFilter.isMovieUnwantedBasedOnGenre(genres)).isTrue() + } + + @Test fun `Movie is unwanted if Opera`() { - val movieFilter = MovieFilterUtil() - val genres = listOf("Opera") - assertThat(movieFilter.isMovieUnwantedBasedOnGenre(genres)).isTrue() - } - - @Test - fun `Movie is unwanted if Konsert`() { - val movieFilter = MovieFilterUtil() - val genres = listOf("Konsert") - assertThat(movieFilter.isMovieUnwantedBasedOnGenre(genres)).isTrue() - } + val movieFilter = MovieFilterUtil() + val genres = listOf("Opera") + assertThat(movieFilter.isMovieUnwantedBasedOnGenre(genres)).isTrue() + } + + @Test + fun `Movie is unwanted if Konsert`() { + val movieFilter = MovieFilterUtil() + val genres = listOf("Konsert") + assertThat(movieFilter.isMovieUnwantedBasedOnGenre(genres)).isTrue() + } } \ No newline at end of file diff --git a/frontend/.eslintrc b/frontend/.eslintrc index b193928fd..5e603ecd1 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -1,8 +1,3 @@ { - "extends": "react-app", - "plugins": ["react-hooks"], - "rules": { - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn" - } + "extends": "react-app" } diff --git a/frontend/.graphqlconfig b/frontend/.graphqlconfig index ef4e65ad5..250db1f52 100644 --- a/frontend/.graphqlconfig +++ b/frontend/.graphqlconfig @@ -1,13 +1,6 @@ { - "schemaPath": "schema.graphql", + "schemaPath": "graphql.schema.json", "extensions": { - "endpoints": { - "dev": { - "url": "http://localhost:8080/graphql", - "headers": { - "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjcyOGY0MDE2NjUyMDc5YjllZDk5ODYxYmIwOWJhZmM1YTQ1YmFhODYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiNjkyMDY0MTcyNjc1LW1vbnRhYjlwaTU3MzA0ZTY4cjkzMmM2bG03MTExb2FmLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNjkyMDY0MTcyNjc1LW1vbnRhYjlwaTU3MzA0ZTY4cjkzMmM2bG03MTExb2FmLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEwNTczNTc0MzExNDI0NTgwMTE3IiwiZW1haWwiOiJqb2hhbi5saW5kc2tvZ2VuQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoicjNUUEtDcEEzSlJrX21OREtjWk1WUSIsIm5hbWUiOiJKb2hhbiBMaW5kc2tvZ2VuIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tZ1pGNGdDQjBxcUEvQUFBQUFBQUFBQUkvQUFBQUFBQUFKS2svMnJWckNDRl93QlEvczk2LWMvcGhvdG8uanBnIiwiZ2l2ZW5fbmFtZSI6IkpvaGFuIiwiZmFtaWx5X25hbWUiOiJMaW5kc2tvZ2VuIiwibG9jYWxlIjoic3YiLCJpYXQiOjE1Mzk5NjY1MzksImV4cCI6MTUzOTk3MDEzOSwianRpIjoiOGFiYTc0ZjBjYWE4NmY5M2JhMTA1NDU1YjE3YjgwNTQ5MDQ5ZWIxMCJ9.WShGH8Sg2zjCbv9f7AxZdAPIalHJBgbLNuWHJJ56Sm0uM5FI9v7nA1ipQIEAxba5rNSA5GOV3HkhPOMRNrDfjWyexWo4dZGHlg4d6jiwiKgDYCntypaPyXIxBavp9dx5UMZP5YzIzH-QJQxm9olcLkbkaKMeJNkYkhvwS2sEooQUxslJ8dLWVeW8fu_sZIfJJS3WmQ7iTaCg6VTkzdTo1xHR7HWQL0PuLkrqgJs8dVFNKa63kCoLhhbzZIPaRUNxc_dPE7gZBmRpowB-r9U7gWi1DYyQVIRzBuYAqY5SSifo296oKVw24rywlE1qlrrGkqy9k95k6N11l0uS93Bjrw" - } - } - } + "endpoints": {} } } \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index bf10b43de..66c600452 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -12,5 +12,10 @@ LABEL Description="The SeFilm frontend served by nginx" COPY nginx.conf /etc/nginx/nginx.conf RUN mkdir -p /srv/ COPY --from=builder /srv/http/build /srv/http + +ARG TAG +ENV TAG=${TAG:-"dev"} +RUN echo $TAG > /srv/http/gitinfo + EXPOSE 80 RUN chown nginx:nginx /srv/http -R diff --git a/frontend/README.md b/frontend/README.md index 7e7c7b539..2b24cacf6 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -7,7 +7,21 @@ This project was bootstrapped with [Create React App](https://github.com/faceboo - `yarn` ### Get up and running + ```sh $ yarn $ yarn start ``` + +### Update GQL Typescript typings + +```sh +# Remove old typings +$ find src -name __generated__ -exec rm -r {} \; + +# Fetch new typings unauthorized +$ yarn apollo client:codegen --target typescript --includes "src/**/*.{js,ts,tsx}" --globalTypesFile=src/__generated__/globalTypes.ts --endpoint http://localhost:8080/graphql + +# Fetch new typings with bearer token +$ yarn apollo client:codegen --target typescript --includes "src/**/*.{js,ts,tsx}" --globalTypesFile=src/__generated__/globalTypes.ts --endpoint http://localhost:8080/graphql --header 'Authorization: Bearer token_here' +``` diff --git a/frontend/analyze.js b/frontend/analyze.js deleted file mode 100644 index 120d43aab..000000000 --- a/frontend/analyze.js +++ /dev/null @@ -1,14 +0,0 @@ -process.env.NODE_ENV = "production"; -var BundleAnalyzerPlugin = require("webpack-bundle-analyzer") - .BundleAnalyzerPlugin; - -const webpackConfigProd = require("react-scripts/config/webpack.config.prod"); - -webpackConfigProd.plugins.push( - new BundleAnalyzerPlugin({ - analyzerMode: "static", - reportFilename: "report.html" - }) -); - -require("react-scripts/scripts/build"); diff --git a/frontend/graphql.schema.json b/frontend/graphql.schema.json new file mode 100644 index 000000000..9ae470b93 --- /dev/null +++ b/frontend/graphql.schema.json @@ -0,0 +1,5327 @@ +{ + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "description": "", + "fields": [ + { + "name": "allMovies", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Movie", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "archivedMovies", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Movie", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "movie", + "description": "", + "args": [ + { + "name": "id", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Movie", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showing", + "description": "", + "args": [ + { + "name": "id", + "description": "", + "type": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "webId", + "description": "", + "type": { + "kind": "SCALAR", + "name": "Base64ID", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "publicShowings", + "description": "", + "args": [ + { + "name": "afterDate", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "privateShowingsForCurrentUser", + "description": "", + "args": [ + { + "name": "afterDate", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showingForMovie", + "description": "", + "args": [ + { + "name": "movieId", + "description": "", + "type": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allBiobudord", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "BioBudord", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "randomBudord", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "BioBudord", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allUsers", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "currentUser", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "previousLocations", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Location", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "location", + "description": "", + "args": [ + { + "name": "id", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Location", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filmstadenCities", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "FilmstadenCity", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allNotificationProviders", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "NotificationProvider", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allNotificationTypes", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "NotificationType", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Movie", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "imdbId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "IMDbID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filmstadenId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filmstadenSlug", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tmdbId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "TMDbID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "synopsis", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "originalTitle", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "releaseDate", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "productionYear", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "runtime", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "poster", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "genres", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastModifiedDate", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "popularity", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "popularityLastUpdated", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "archived", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filmstadenShowings", + "description": "", + "args": [ + { + "name": "city", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"GB\"" + }, + { + "name": "afterDate", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "FilmstadenShowing", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "UUID", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "IMDbID", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "TMDbID", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Float", + "description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "LocalDate", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FilmstadenShowing", + "description": "", + "fields": [ + { + "name": "cinemaName", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "screen", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "FilmstadenScreen", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "seatCount", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timeUtc", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tags", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filmstadenRemoteEntityId", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FilmstadenScreen", + "description": "", + "fields": [ + { + "name": "filmstadenId", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Base64ID", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Showing", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "webId", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Base64ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "slug", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "date", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "time", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "movie", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Movie", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "location", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Location", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filmstadenScreen", + "description": "", + "args": [], + "type": { + "kind": "OBJECT", + "name": "FilmstadenScreen", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "private", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "price", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "SEK", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ticketsBought", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "admin", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payToUser", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expectedBuyDate", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "participants", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Participant", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastModifiedDate", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filmstadenSeatMap", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "FilmstadenSeatMap", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filmstadenRemoteEntityId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "adminPaymentDetails", + "description": "", + "args": [], + "type": { + "kind": "OBJECT", + "name": "AdminPaymentDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attendeePaymentDetails", + "description": "", + "args": [], + "type": { + "kind": "OBJECT", + "name": "AttendeePaymentDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "myTickets", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Ticket", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ticketRange", + "description": "", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TicketRange", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Location", + "description": "", + "fields": [ + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "latitude", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "longitude", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "city", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cityAlias", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "streetAddress", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postalCode", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postalAddress", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "alias", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "SEK", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "User", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UserID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "firstName", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastName", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nick", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "phone", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "avatar", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "UserID", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Participant", + "description": "", + "fields": [ + { + "name": "user", + "description": "", + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentType", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FilmstadenSeatMap", + "description": "", + "fields": [ + { + "name": "row", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "seatType", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "coordinates", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "FilmstadenSeatCoordinates", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dimensions", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "FilmstadenSeatDimensions", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FilmstadenSeatCoordinates", + "description": "", + "fields": [ + { + "name": "x", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "y", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FilmstadenSeatDimensions", + "description": "", + "fields": [ + { + "name": "width", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "height", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AdminPaymentDetails", + "description": "", + "fields": [ + { + "name": "filmstadenBuyLink", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filmstadenData", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "FilmstadenData", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "participantPaymentInfos", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ParticipantPaymentInfo", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FilmstadenData", + "description": "", + "fields": [ + { + "name": "user", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filmstadenMembershipId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "foretagsbiljett", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ParticipantPaymentInfo", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showing", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPaid", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amountOwed", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "SEK", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AttendeePaymentDetails", + "description": "", + "fields": [ + { + "name": "hasPaid", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amountOwed", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "SEK", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payTo", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payer", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "swishLink", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Ticket", + "description": "A ticket assigned to a user that allowes access to Filmstaden", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showingId", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignedToUser", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "profileId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "barcode", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customerType", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customerTypeDefinition", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cinema", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cinemaCity", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "screen", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "seat", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Seat", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "date", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "time", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "movieName", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "movieRating", + "description": "15 år, 11 år etc.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showAttributes", + "description": "\"textad\", \"en\" etc", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Seat", + "description": "The row and seat number for a ticket", + "fields": [ + { + "name": "row", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TicketRange", + "description": "In what range is the seating assigned for each row", + "fields": [ + { + "name": "rows", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "seatings", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SeatRange", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": "The total number of tickets for this showing", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SeatRange", + "description": "", + "fields": [ + { + "name": "row", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "numbers", + "description": "All numbers for this particular row", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "BioBudord", + "description": "", + "fields": [ + { + "name": "number", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "phrase", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CurrentUser", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UserID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "firstName", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastName", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nick", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filmstadenMembershipId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "phone", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "avatar", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "foretagsbiljetter", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Foretagsbiljett", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calendarFeedId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calendarFeedUrl", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notificationSettings", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "NotificationSettings", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastLogin", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "signupDate", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Foretagsbiljett", + "description": "", + "fields": [ + { + "name": "number", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expires", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "ForetagsbiljettStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ForetagsbiljettStatus", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "Available", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Pending", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Used", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Expired", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "NotificationSettings", + "description": "", + "fields": [ + { + "name": "notificationsEnabled", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enabledTypes", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "NotificationType", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "providerSettings", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INTERFACE", + "name": "ProviderSettings", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "NotificationType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "UpdateShowing", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NewShowing", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DeletedShowing", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TicketsBought", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UserAttended", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UserUnattended", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "ProviderSettings", + "description": "", + "fields": [ + { + "name": "enabled", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "MailSettings", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "PushoverSettings", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "FilmstadenCity", + "description": "", + "fields": [ + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "alias", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "NotificationProvider", + "description": "", + "fields": [ + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isSubscribable", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Mutation", + "description": "", + "fields": [ + { + "name": "attendShowing", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "paymentOption", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PaymentOption", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unattendShowing", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createShowing", + "description": "", + "args": [ + { + "name": "showing", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateShowingInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteShowing", + "description": "Delete a showign and return all public showings", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "markAsBought", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "price", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "SEK", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processTicketUrls", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "ticketUrls", + "description": "", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateShowing", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "newValues", + "description": "", + "type": { + "kind": "INPUT_OBJECT", + "name": "UpdateShowingInput", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "promoteToAdmin", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "userToPromote", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UserID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fetchNewMoviesFromFilmstaden", + "description": "Fetch any new movies from Filmstaden, returns the movies that were added", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Movie", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateParticipantPaymentInfo", + "description": "", + "args": [ + { + "name": "paymentInfo", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ParticipantPaymentInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ParticipantPaymentInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateUser", + "description": "", + "args": [ + { + "name": "newInfo", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "NewUserInfo", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "invalidateCalendarFeed", + "description": "Generate a new calendar feed id, invalidating the old one", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "disableCalendarFeed", + "description": "Remove the calendar feed for the current user", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateNotificationSettings", + "description": "", + "args": [ + { + "name": "notificationSettings", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "NotificationSettingsInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "addForetagsBiljetter", + "description": "", + "args": [ + { + "name": "biljetter", + "description": "", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ForetagsbiljettInput", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteForetagsBiljett", + "description": "", + "args": [ + { + "name": "biljett", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ForetagsbiljettInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PaymentOption", + "description": "Used for supplying how the use will pay, when attending a showing", + "fields": null, + "inputFields": [ + { + "name": "type", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PaymentType", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "ticketNumber", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PaymentType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "Swish", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Foretagsbiljett", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateShowingInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "date", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "time", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "LocalTime", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "movieId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "location", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "filmstadenScreen", + "description": "", + "type": { + "kind": "INPUT_OBJECT", + "name": "FilmstadenScreenInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "expectedBuyDate", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "filmstadenRemoteEntityId", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "LocalTime", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "FilmstadenScreenInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "filmstadenId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "name", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateShowingInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "price", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "SEK", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "private", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "payToUser", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "expectedBuyDate", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "location", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "filmstadenRemoteEntityId", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "time", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "LocalTime", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "date", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ParticipantPaymentInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "userId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "showingId", + "description": "", + "type": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "hasPaid", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "amountOwed", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "SEK", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "NewUserInfo", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "nick", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "filmstadenMembershipId", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "phone", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "NotificationSettingsInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "notificationsEnabled", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "enabledTypes", + "description": "", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "NotificationType", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "pushover", + "description": "", + "type": { + "kind": "INPUT_OBJECT", + "name": "InputPushoverSettings", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "mail", + "description": "", + "type": { + "kind": "INPUT_OBJECT", + "name": "InputMailSettings", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InputPushoverSettings", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "enabled", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "userKey", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "device", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InputMailSettings", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "enabled", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "mailAddress", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ForetagsbiljettInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "number", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "expires", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": "Location adjacent to a variable definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MailSettings", + "description": "", + "fields": [ + { + "name": "enabled", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mailAddress", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "ProviderSettings", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PushoverSettings", + "description": "", + "fields": [ + { + "name": "enabled", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userKey", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "device", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userKeyStatus", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PushoverValidationStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "ProviderSettings", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PushoverValidationStatus", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "USER_AND_DEVICE_VALID", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "USER_VALID_DEVICE_INVALID", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "USER_INVALID", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TOKEN_INVALID", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNKNOWN", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if`'argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "defer", + "description": "This directive allows results to be deferred during execution", + "locations": [ + "FIELD" + ], + "args": [] + } + ] + } +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 68b89df11..863cf9727 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,48 +3,56 @@ "version": "0.1.0", "private": true, "devDependencies": { - "@packtracker/webpack-plugin": "2.0.0", - "eslint-plugin-react-hooks": "1.5.0", - "react-scripts": "2.1.8", - "webpack-bundle-analyzer": "3.0.4" + "apollo": "^2.17.4", + "source-map-explorer": "^2.0.1" }, "dependencies": { - "@emotion/core": "10.0.7", - "@emotion/styled": "10.0.7", - "@fortawesome/fontawesome-free": "5.6.0", - "@fortawesome/fontawesome-svg-core": "1.2.9", - "@fortawesome/free-solid-svg-icons": "5.6.0", - "@fortawesome/react-fontawesome": "0.1.3", - "apollo-cache-inmemory": "1.3.11", + "@types/jest": "^24.0.11", + "@types/lodash-es": "^4.17.3", + "@types/node": "^11.11.0", + "@types/react": "^16.9.2", + "@types/react-dom": "^16.8.5", + "@types/react-router": "^5.0.3", + "@types/react-router-dom": "^4.3.4", + "@emotion/core": "^10.0.16", + "@emotion/styled": "^10.0.15", + "@fortawesome/fontawesome-free": "5.10.1", + "@fortawesome/fontawesome-svg-core": "1.2.21", + "@fortawesome/free-solid-svg-icons": "5.10.1", + "@fortawesome/react-fontawesome": "0.1.4", + "@testing-library/react": "9.1.3", + "apollo-cache": "^1.3.2", + "apollo-cache-inmemory": "1.6.3", "apollo-cache-persist": "0.1.1", - "apollo-client": "2.4.7", - "apollo-link": "1.2.4", - "apollo-link-context": "1.0.10", - "apollo-link-error": "1.1.2", - "apollo-link-http": "1.5.7", + "apollo-client": "2.6.4", + "apollo-link": "1.2.12", + "apollo-link-context": "1.0.18", + "apollo-link-error": "1.1.11", + "apollo-link-http": "1.5.15", + "apollo-utilities": "^1.3.2", "date-fns": "1.30.1", - "graphql": "14.0.2", - "graphql-tag": "2.10.0", - "lodash-es": "4.17.11", - "prop-types": "15.6.2", - "react": "16.8.3", - "react-apollo": "2.3.2", - "react-day-picker": "7.2.4", - "react-dom": "16.8.3", - "react-modal": "3.7.1", - "react-router": "4.3.1", - "react-router-dom": "4.3.1", - "react-select": "2.1.2", - "react-testing-library": "6.0.0", + "graphql": "14.4.2", + "graphql-tag": "2.10.1", + "lodash-es": "4.17.15", + "prop-types": "15.7.2", + "react": "16.9.0", + "react-apollo": "3.0.1", + "react-day-picker": "7.3.2", + "react-dom": "16.9.0", + "react-modal": "3.9.1", + "react-router": "5.0.1", + "react-router-dom": "5.0.1", + "react-select": "3.0.4", + "react-scripts": "3.1.1", "recompose": "0.30.0", - "rollbar": "2.5.1", + "rollbar": "2.12.0", + "typescript": "^3.5.3", "whatwg-fetch": "3.0.0" }, "scripts": { + "analyze": "yarn build && yarn source-map-explorer build/static/js/main.*", "start": "react-scripts start", - "lint": "eslint src", "build": "react-scripts build", - "packtracker": "react-scripts build --stats && packtracker-upload --stats=build/bundle-stats.json", "test": "react-scripts test --transformIgnorePatterns \"node_modules/(?!(lodash-es))/\"", "eject": "react-scripts eject" }, diff --git a/frontend/schema.json b/frontend/schema.json new file mode 100644 index 000000000..26e85305f --- /dev/null +++ b/frontend/schema.json @@ -0,0 +1,5245 @@ +{ + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "description": "", + "fields": [ + { + "name": "allMovies", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Movie", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "archivedMovies", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Movie", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "movie", + "description": "", + "args": [ + { + "name": "id", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Movie", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showing", + "description": "", + "args": [ + { + "name": "id", + "description": "", + "type": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "webId", + "description": "", + "type": { + "kind": "SCALAR", + "name": "Base64ID", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "publicShowings", + "description": "", + "args": [ + { + "name": "afterDate", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "privateShowingsForCurrentUser", + "description": "", + "args": [ + { + "name": "afterDate", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showingForMovie", + "description": "", + "args": [ + { + "name": "movieId", + "description": "", + "type": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allBiobudord", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "BioBudord", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "randomBudord", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "BioBudord", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allUsers", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "currentUser", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "previousLocations", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Location", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "location", + "description": "", + "args": [ + { + "name": "id", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Location", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sfCities", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SfCity", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allNotificationProviders", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "NotificationProvider", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allNotificationTypes", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "NotificationType", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Movie", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "imdbId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "IMDbID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sfId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sfSlug", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tmdbId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "TMDbID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "synopsis", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "originalTitle", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "releaseDate", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "productionYear", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "runtime", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "poster", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "genres", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastModifiedDate", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "popularity", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "popularityLastUpdated", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "archived", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sfShowings", + "description": "", + "args": [ + { + "name": "city", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"GB\"" + }, + { + "name": "afterDate", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SfShowing", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "UUID", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "IMDbID", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "TMDbID", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Float", + "description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "LocalDate", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SfShowing", + "description": "", + "fields": [ + { + "name": "cinemaName", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "screen", + "description": "", + "args": [], + "type": { + "kind": "OBJECT", + "name": "SfScreen", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "seatCount", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timeUtc", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tags", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SfScreen", + "description": "", + "fields": [ + { + "name": "sfId", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Base64ID", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Showing", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "webId", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Base64ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "slug", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "date", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "time", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "movie", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Movie", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "location", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Location", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sfScreen", + "description": "", + "args": [], + "type": { + "kind": "OBJECT", + "name": "SfScreen", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "private", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "price", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "SEK", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ticketsBought", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "admin", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payToUser", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expectedBuyDate", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "participants", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Participant", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastModifiedDate", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sfSeatMap", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SfSeatMap", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "adminPaymentDetails", + "description": "", + "args": [], + "type": { + "kind": "OBJECT", + "name": "AdminPaymentDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attendeePaymentDetails", + "description": "", + "args": [], + "type": { + "kind": "OBJECT", + "name": "AttendeePaymentDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "myTickets", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Ticket", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ticketRange", + "description": "", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TicketRange", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Location", + "description": "", + "fields": [ + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "latitude", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "longitude", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "city", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cityAlias", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "streetAddress", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postalCode", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postalAddress", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "alias", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "SEK", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "User", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UserID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "firstName", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastName", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nick", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "phone", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "avatar", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "UserID", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Participant", + "description": "", + "fields": [ + { + "name": "user", + "description": "", + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentType", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SfSeatMap", + "description": "", + "fields": [ + { + "name": "row", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "seatType", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "coordinates", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SfSeatCoordinates", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dimensions", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SfSeatDimensions", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SfSeatCoordinates", + "description": "", + "fields": [ + { + "name": "x", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "y", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SfSeatDimensions", + "description": "", + "fields": [ + { + "name": "width", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "height", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AdminPaymentDetails", + "description": "", + "fields": [ + { + "name": "sfBuyLink", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sfData", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SfData", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "participantPaymentInfos", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ParticipantPaymentInfo", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SfData", + "description": "", + "fields": [ + { + "name": "user", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sfMembershipId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "foretagsbiljett", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ParticipantPaymentInfo", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showing", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPaid", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amountOwed", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "SEK", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AttendeePaymentDetails", + "description": "", + "fields": [ + { + "name": "hasPaid", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amountOwed", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "SEK", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payTo", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payer", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "swishLink", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Ticket", + "description": "A ticket assigned to a user that allowes access to SF", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showingId", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignedToUser", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "profileId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "barcode", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customerType", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customerTypeDefinition", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cinema", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cinemaCity", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "screen", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "seat", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Seat", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "date", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "time", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "movieName", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "movieRating", + "description": "15 år, 11 år etc.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showAttributes", + "description": "\"textad\", \"en\" etc", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Seat", + "description": "The row and seat number for a ticket", + "fields": [ + { + "name": "row", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TicketRange", + "description": "In what range is the seating assigned for each row", + "fields": [ + { + "name": "rows", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "seatings", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SeatRange", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": "The total number of tickets for this showing", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SeatRange", + "description": "", + "fields": [ + { + "name": "row", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "numbers", + "description": "All numbers for this particular row", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "BioBudord", + "description": "", + "fields": [ + { + "name": "number", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "phrase", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CurrentUser", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UserID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "firstName", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastName", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nick", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sfMembershipId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "phone", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "avatar", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "foretagsbiljetter", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Foretagsbiljett", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calendarFeedId", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calendarFeedUrl", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notificationSettings", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "NotificationSettings", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastLogin", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "signupDate", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Foretagsbiljett", + "description": "", + "fields": [ + { + "name": "number", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expires", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "ForetagsbiljettStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ForetagsbiljettStatus", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "Available", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Pending", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Used", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Expired", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "NotificationSettings", + "description": "", + "fields": [ + { + "name": "notificationsEnabled", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enabledTypes", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "NotificationType", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "providerSettings", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INTERFACE", + "name": "ProviderSettings", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "NotificationType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "UpdateShowing", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NewShowing", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DeletedShowing", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TicketsBought", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UserAttended", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UserUnattended", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "ProviderSettings", + "description": "", + "fields": [ + { + "name": "enabled", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "MailSettings", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "PushoverSettings", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "SfCity", + "description": "", + "fields": [ + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "alias", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "NotificationProvider", + "description": "", + "fields": [ + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isSubscribable", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Mutation", + "description": "", + "fields": [ + { + "name": "attendShowing", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "paymentOption", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PaymentOption", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unattendShowing", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createShowing", + "description": "", + "args": [ + { + "name": "showing", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateShowingInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteShowing", + "description": "Delete a showign and return all public showings", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "markAsBought", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processTicketUrls", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "ticketUrls", + "description": "", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateShowing", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "newValues", + "description": "", + "type": { + "kind": "INPUT_OBJECT", + "name": "UpdateShowingInput", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "promoteToAdmin", + "description": "", + "args": [ + { + "name": "showingId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "userToPromote", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UserID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Showing", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fetchNewMoviesFromSf", + "description": "Fetch any new movies from SF, returns the movies that were added", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Movie", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateParticipantPaymentInfo", + "description": "", + "args": [ + { + "name": "paymentInfo", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ParticipantPaymentInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ParticipantPaymentInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateUser", + "description": "", + "args": [ + { + "name": "newInfo", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "NewUserInfo", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "invalidateCalendarFeed", + "description": "Generate a new calendar feed id, invalidating the old one", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "disableCalendarFeed", + "description": "Remove the calendar feed for the current user", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateNotificationSettings", + "description": "", + "args": [ + { + "name": "notificationSettings", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "NotificationSettingsInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "addForetagsBiljetter", + "description": "", + "args": [ + { + "name": "biljetter", + "description": "", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ForetagsbiljettInput", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteForetagsBiljett", + "description": "", + "args": [ + { + "name": "biljett", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ForetagsbiljettInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CurrentUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PaymentOption", + "description": "Used for supplying how the use will pay, when attending a showing", + "fields": null, + "inputFields": [ + { + "name": "type", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PaymentType", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "ticketNumber", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PaymentType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "Swish", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Foretagsbiljett", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateShowingInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "date", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "time", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "LocalTime", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "movieId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "location", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "sfScreen", + "description": "", + "type": { + "kind": "INPUT_OBJECT", + "name": "SfScreenInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "expectedBuyDate", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "LocalTime", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SfScreenInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "sfId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "name", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateShowingInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "price", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "SEK", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "private", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "payToUser", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "expectedBuyDate", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "location", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "sfScreen", + "description": "", + "type": { + "kind": "INPUT_OBJECT", + "name": "SfScreenInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "time", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "LocalTime", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ParticipantPaymentInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "userId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "showingId", + "description": "", + "type": { + "kind": "SCALAR", + "name": "UUID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "hasPaid", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "amountOwed", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "SEK", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "NewUserInfo", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "nick", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "sfMembershipId", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "phone", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "NotificationSettingsInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "notificationsEnabled", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "enabledTypes", + "description": "", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "NotificationType", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "pushover", + "description": "", + "type": { + "kind": "INPUT_OBJECT", + "name": "InputPushoverSettings", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "mail", + "description": "", + "type": { + "kind": "INPUT_OBJECT", + "name": "InputMailSettings", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InputPushoverSettings", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "enabled", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "userKey", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "device", + "description": "", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InputMailSettings", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "enabled", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "mailAddress", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ForetagsbiljettInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "number", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "expires", + "description": "", + "type": { + "kind": "SCALAR", + "name": "LocalDate", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": "Location adjacent to a variable definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MailSettings", + "description": "", + "fields": [ + { + "name": "enabled", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mailAddress", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "ProviderSettings", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PushoverSettings", + "description": "", + "fields": [ + { + "name": "enabled", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userKey", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "device", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userKeyStatus", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PushoverValidationStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "ProviderSettings", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PushoverValidationStatus", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "USER_AND_DEVICE_VALID", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "USER_VALID_DEVICE_INVALID", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "USER_INVALID", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TOKEN_INVALID", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNKNOWN", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if`'argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "defer", + "description": "This directive allows results to be deferred during execution", + "locations": [ + "FIELD" + ], + "args": [] + } + ] + } +} \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js deleted file mode 100644 index 33af4bc5a..000000000 --- a/frontend/src/App.js +++ /dev/null @@ -1,98 +0,0 @@ -import React, { Suspense, lazy } from "react"; -import { Route, Switch } from "react-router-dom"; -import { graphql } from "react-apollo"; -import { compose, branch, renderComponent } from "recompose"; -import gql from "graphql-tag"; -import styled from "@emotion/styled"; - -import NavBar from "./use-cases/common/ui/NavBar"; -import Footer from "./use-cases/common/ui/footer/Footer"; -import WelcomeModal from "./use-cases/common/utils/WelcomeModal"; -import { completeUserFragment } from "./apollo/queries/currentUser"; -import Loader from "./use-cases/common/utils/ProjectorLoader"; -import { MissingShowing } from "./use-cases/common/showing/MissingShowing"; -import { PageTitleTemplate } from "./use-cases/common/utils/PageTitle"; - -const MainGridContainer = styled.div` - flex: 1; - grid-area: content; - display: grid; - grid-template-columns: minmax(1rem, 1fr) minmax(min-content, 1000px) minmax( - 1rem, - 1fr - ); - grid-template-rows: min-content auto; - grid-template-areas: - "jumbo jumbo jumbo" - ". center ."; - background-color: #f8f8f8; - align-items: start; -`; - -const AsyncHome = lazy(() => import("./use-cases/my-showings/Home")); -const AsyncUser = lazy(() => import("./use-cases/user/index")); -const AsyncShowings = lazy(() => import("./use-cases/showings-list/Showings")); -const AsyncNewShowing = lazy(() => - import("./use-cases/new-showing/NewShowing") -); -const AsyncEditShowing = lazy(() => - import("./use-cases/edit-showing/EditShowing") -); -const AsyncShowingTickets = lazy(() => - import("./use-cases/showing-tickets/index") -); -const AsyncSingleShowing = lazy(() => - import("./use-cases/single-showing/screen/SingleShowingScreen") -); - -const App = ({ data: { me }, signout }) => ( - <> - - - - - }> - - } /> - } /> - } /> - } - /> - } - /> - } - /> - } - /> - } /> - - - -