diff --git a/domain/search/src/main/kotlin/dev/adriankuta/flights/domain/search/GetFlightsSearchContentUseCase.kt b/domain/search/src/main/kotlin/dev/adriankuta/flights/domain/search/GetFlightsSearchContentUseCase.kt new file mode 100644 index 0000000..4992807 --- /dev/null +++ b/domain/search/src/main/kotlin/dev/adriankuta/flights/domain/search/GetFlightsSearchContentUseCase.kt @@ -0,0 +1,8 @@ +package dev.adriankuta.flights.domain.search + +import dev.adriankuta.flights.domain.search.entities.SearchOptions +import dev.adriankuta.flights.domain.types.Flight + +fun interface GetFlightsSearchContentUseCase { + suspend operator fun invoke(searchOptions: SearchOptions): Flight +} diff --git a/domain/search/src/main/kotlin/dev/adriankuta/flights/domain/search/entities/SearchOptions.kt b/domain/search/src/main/kotlin/dev/adriankuta/flights/domain/search/entities/SearchOptions.kt new file mode 100644 index 0000000..376c4a1 --- /dev/null +++ b/domain/search/src/main/kotlin/dev/adriankuta/flights/domain/search/entities/SearchOptions.kt @@ -0,0 +1,13 @@ +package dev.adriankuta.flights.domain.search.entities + +import dev.adriankuta.flights.domain.types.Airport +import java.time.LocalDate + +data class SearchOptions( + val origin: Airport.Departure, + val destination: Airport.Arrival, + val date: LocalDate, + val adults: Int, + val teens: Int, + val children: Int, +) diff --git a/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/FlightService.kt b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/FlightService.kt index 50b483e..c85c7a0 100644 --- a/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/FlightService.kt +++ b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/FlightService.kt @@ -12,7 +12,7 @@ interface FlightService { "client: android", "client-version: 3.300.0", ) - @GET("v4/en-gb/Availability") + @GET("api/v4/en-gb/Availability") @Suppress("LongParameterList") suspend fun getFlights( @Query("dateout") date: String, diff --git a/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/di/NetworkModule.kt b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/di/NetworkModule.kt index 5ef405a..70bad93 100644 --- a/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/di/NetworkModule.kt +++ b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/di/NetworkModule.kt @@ -12,6 +12,7 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory +import javax.inject.Qualifier import javax.inject.Singleton @Module @@ -37,7 +38,8 @@ class NetworkModule { @Singleton @Provides - fun provideRetrofit(client: OkHttpClient, moshi: Moshi): Retrofit = Retrofit.Builder() + @ServicesAPI + fun provideServicesRetrofit(client: OkHttpClient, moshi: Moshi): Retrofit = Retrofit.Builder() // TODO("add URL provided in the task instructions") .baseUrl("https://services-api.ryanair.com") .addConverterFactory(MoshiConverterFactory.create(moshi)) @@ -46,16 +48,34 @@ class NetworkModule { @Singleton @Provides - fun provideAirportService(retrofit: Retrofit): AirportService = + @NativeAppsAPI + fun provideNativeAppsRetrofit(client: OkHttpClient, moshi: Moshi): Retrofit = Retrofit.Builder() + // TODO("add URL provided in the task instructions") + .baseUrl("https://nativeapps.ryanair.com") + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .client(client) + .build() + + @Singleton + @Provides + fun provideAirportService(@ServicesAPI retrofit: Retrofit): AirportService = retrofit.create(AirportService::class.java) @Singleton @Provides - fun provideRoutesService(retrofit: Retrofit): RoutesService = + fun provideRoutesService(@ServicesAPI retrofit: Retrofit): RoutesService = retrofit.create(RoutesService::class.java) @Singleton @Provides - fun provideFlightService(retrofit: Retrofit): FlightService = + fun provideFlightService(@NativeAppsAPI retrofit: Retrofit): FlightService = retrofit.create(FlightService::class.java) } + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ServicesAPI + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class NativeAppsAPI diff --git a/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/di/GetFlightsSearchContentUseCaseModule.kt b/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/di/GetFlightsSearchContentUseCaseModule.kt new file mode 100644 index 0000000..cfa9c42 --- /dev/null +++ b/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/di/GetFlightsSearchContentUseCaseModule.kt @@ -0,0 +1,19 @@ +package dev.adriankuta.flights.model.repository.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dev.adriankuta.flights.domain.search.GetFlightsSearchContentUseCase +import dev.adriankuta.flights.model.repository.usecases.GetFlightsSearchContentUseCaseImpl + +@Module +@InstallIn(SingletonComponent::class) +@Suppress("UnnecessaryAbstractClass") +internal abstract class GetFlightsSearchContentUseCaseModule { + + @Binds + abstract fun bind( + getFlightsSearchContentUseCaseImpl: GetFlightsSearchContentUseCaseImpl, + ): GetFlightsSearchContentUseCase +} diff --git a/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/mappers/FlightDomainMapper.kt b/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/mappers/FlightDomainMapper.kt new file mode 100644 index 0000000..1e691de --- /dev/null +++ b/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/mappers/FlightDomainMapper.kt @@ -0,0 +1,75 @@ +package dev.adriankuta.flights.model.repository.mappers + +import dev.adriankuta.flights.domain.types.Flight +import dev.adriankuta.flights.domain.types.RegularFare +import dev.adriankuta.flights.domain.types.Segment +import dev.adriankuta.flights.domain.types.Trip +import dev.adriankuta.flights.domain.types.TripDate +import dev.adriankuta.flights.domain.types.TripFare +import dev.adriankuta.flights.domain.types.TripFlight +import dev.adriankuta.model.data.api.entities.FlightResponse +import dev.adriankuta.model.data.api.entities.RegularFare as ApiRegularFare +import dev.adriankuta.model.data.api.entities.Segment as ApiSegment +import dev.adriankuta.model.data.api.entities.Trip as ApiTrip +import dev.adriankuta.model.data.api.entities.TripDate as ApiTripDate +import dev.adriankuta.model.data.api.entities.TripFare as ApiTripFare +import dev.adriankuta.model.data.api.entities.TripFlight as ApiTripFlight + +internal fun FlightResponse.toDomain(): Flight { + return Flight( + currency = currency ?: "", + currPrecision = currPrecision ?: 0, + trips = trips.orEmpty().map { it.toDomain() }, + ) +} + +internal fun ApiTrip.toDomain(): Trip { + return Trip( + dates = dates.orEmpty().map { it.toDomain() }, + origin = origin ?: "", + destination = destination ?: "", + ) +} + +internal fun ApiTripDate.toDomain(): TripDate { + return TripDate( + dateOut = dateOut ?: "", + flights = flights.orEmpty().map { it.toDomain() }, + ) +} + +internal fun ApiTripFlight.toDomain(): TripFlight { + return TripFlight( + faresLeft = faresLeft ?: 0, + regularFare = regularFare?.toDomain() ?: RegularFare(emptyList()), + flightNumber = flightNumber ?: "", + dateTimes = dateTimes.orEmpty(), + duration = duration ?: "", + segments = segments.orEmpty().map { it.toDomain() }, + operatedBy = operatedBy ?: "", + ) +} + +internal fun ApiRegularFare.toDomain(): RegularFare { + return RegularFare( + fares = fares.orEmpty().map { it.toDomain() }, + ) +} + +internal fun ApiTripFare.toDomain(): TripFare { + return TripFare( + type = type ?: "", + amount = amount ?: 0.0, + count = count ?: 0, + ) +} + +internal fun ApiSegment.toDomain(): Segment { + return Segment( + origin = origin ?: "", + destination = destination ?: "", + flightNumber = flightNumber ?: "", + dateTimes = dateTimes.orEmpty(), + duration = duration ?: "", + ) +} diff --git a/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/usecases/GetFlightsSearchContentUseCaseImpl.kt b/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/usecases/GetFlightsSearchContentUseCaseImpl.kt new file mode 100644 index 0000000..2014598 --- /dev/null +++ b/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/usecases/GetFlightsSearchContentUseCaseImpl.kt @@ -0,0 +1,27 @@ +package dev.adriankuta.flights.model.repository.usecases + +import dev.adriankuta.flights.domain.search.GetFlightsSearchContentUseCase +import dev.adriankuta.flights.domain.search.entities.SearchOptions +import dev.adriankuta.flights.domain.types.Flight +import dev.adriankuta.flights.model.repository.mappers.toDomain +import dev.adriankuta.model.data.api.FlightService +import java.time.format.DateTimeFormatter +import javax.inject.Inject + +internal class GetFlightsSearchContentUseCaseImpl @Inject constructor( + private val flightService: FlightService, +) : GetFlightsSearchContentUseCase { + + override suspend fun invoke(searchOptions: SearchOptions): Flight { + val result = flightService.getFlights( + origin = searchOptions.origin.code, + destination = searchOptions.destination.code, + date = searchOptions.date.format(DateTimeFormatter.ISO_DATE), + adult = searchOptions.adults, + teen = searchOptions.teens, + child = searchOptions.children, + ) + + return result.toDomain() + } +} diff --git a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt index 450d5af..162b76f 100644 --- a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt +++ b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt @@ -5,7 +5,10 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.adriankuta.flights.core.util.Result import dev.adriankuta.flights.core.util.asResult +import dev.adriankuta.flights.domain.search.GetFlightsSearchContentUseCase import dev.adriankuta.flights.domain.search.ObserveAirportsUseCase +import dev.adriankuta.flights.domain.search.entities.SearchOptions +import dev.adriankuta.flights.domain.types.Airport import dev.adriankuta.flights.domain.types.AirportInfo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -13,6 +16,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import timber.log.Timber import java.time.LocalDate import javax.inject.Inject @@ -20,6 +24,7 @@ import javax.inject.Inject @HiltViewModel class HomeScreenViewModel @Inject constructor( observeAirportsUseCase: ObserveAirportsUseCase, + private val getFlightsSearchContentUseCase: GetFlightsSearchContentUseCase, ) : ViewModel() { private val selectedOriginAirport = MutableStateFlow(null) @@ -86,7 +91,27 @@ class HomeScreenViewModel @Inject constructor( } fun search() { - Timber.d("Search clicked") + viewModelScope.launch { + val results = getFlightsSearchContentUseCase( + searchOptions = SearchOptions( + origin = Airport.Departure( + code = selectedOriginAirport.value?.code ?: return@launch, + name = selectedOriginAirport.value?.name ?: return@launch, + macCity = selectedOriginAirport.value?.macCity, + ), + destination = Airport.Arrival( + code = selectedDestinationAirport.value?.code ?: return@launch, + name = selectedDestinationAirport.value?.name ?: return@launch, + macCity = selectedDestinationAirport.value?.macCity, + ), + date = selectedDate.value, + adults = adultCount.value, + teens = teenCount.value, + children = childCount.value, + ), + ) + Timber.d("Result $results") + } } }