diff --git a/app/src/main/graphql/fragments.graphql b/app/src/main/graphql/fragments.graphql index e163ded49d..0b240ef644 100644 --- a/app/src/main/graphql/fragments.graphql +++ b/app/src/main/graphql/fragments.graphql @@ -115,6 +115,16 @@ fragment fullProject on Project { items { ... rewardItems } + ... on Reward { + simpleShippingRulesExpanded { + cost + country + estimatedMax + estimatedMin + locationId + locationName + } + } } } risks @@ -300,9 +310,6 @@ fragment reward on Reward { convertedAmount{ ... amount } - shippingRules { - ... shippingRule - } shippingPreference remainingQuantity limit diff --git a/app/src/main/java/com/kickstarter/services/transformers/GraphQLTransformers.kt b/app/src/main/java/com/kickstarter/services/transformers/GraphQLTransformers.kt index df320d5674..94ee86b1e9 100644 --- a/app/src/main/java/com/kickstarter/services/transformers/GraphQLTransformers.kt +++ b/app/src/main/java/com/kickstarter/services/transformers/GraphQLTransformers.kt @@ -130,6 +130,7 @@ fun environmentalCommitmentTransformer(envCommit: fragment.EnvironmentalCommitme fun rewardTransformer( rewardGr: fragment.Reward, shippingRulesExpanded: List = emptyList(), + simpleShippingRules: List = emptyList(), allowedAddons: Boolean = false, rewardItems: List = emptyList(), addOnItems: List = emptyList() @@ -161,15 +162,12 @@ fun rewardTransformer( val limit = if (isAddOn) chooseLimit(rewardGr.limit(), rewardGr.limitPerBacker()) else rewardGr.limit() - val shippingRules = if (shippingRulesExpanded.isNotEmpty()) { - shippingRulesExpanded.map { - shippingRuleTransformer(it) - } - } else { - rewardGr.shippingRules().map { - shippingRuleTransformer(it.fragments().shippingRule()) - } - } + val shippingRules = + shippingRulesExpanded.takeIf { it.isNotEmpty() }?.map { shippingRuleTransformer(it) } + ?: simpleShippingRules.takeIf { it.isNotEmpty() }?.map { + return@map simpleShippingRuleTransformer(it) + } + ?: emptyList() val localReceiptLocation = locationTransformer(rewardGr.localReceiptLocation()?.fragments()?.location()) @@ -200,6 +198,30 @@ fun rewardTransformer( .build() } +fun simpleShippingRuleTransformer(simpleShippingRules: FullProject.SimpleShippingRulesExpanded): ShippingRule { + val id = decodeRelayId(simpleShippingRules.locationId()) ?: -1 + val country = simpleShippingRules.country() ?: "" + val displayName = simpleShippingRules.locationName() + + val location = Location.builder() + .id(id) + .country(country) + .displayableName(displayName) + .name(displayName) + .build() + val cost = simpleShippingRules.cost()?.toDoubleOrNull() ?: 0.0 + val estimatedMin = simpleShippingRules.estimatedMin()?.toDoubleOrNull() ?: 0.0 + val estimatedMax = simpleShippingRules.estimatedMax()?.toDoubleOrNull() ?: 0.0 + + return ShippingRule.builder() + .id(id) + .location(location) + .cost(cost) + .estimatedMax(estimatedMax) + .estimatedMin(estimatedMin) + .build() +} + /** * Choose the available limit being the smallest one, we can have limit by backer available just in add-ons * or limit by reward, available in V1 and Graphql and for both add-ons and Rewards @@ -305,8 +327,10 @@ fun projectTransformer(projectFragment: FullProject?): Project { val minPledge = projectFragment?.minPledge()?.toDouble() ?: 1.0 val rewards = projectFragment?.rewards()?.nodes()?.map { + val shippingRules = it.simpleShippingRulesExpanded() rewardTransformer( it.fragments().reward(), + simpleShippingRules = shippingRules, allowedAddons = it.allowedAddons().pageInfo().startCursor()?.isNotEmpty() ?: false, rewardItems = complexRewardItemsTransformer(it.items()?.fragments()?.rewardItems()) ) diff --git a/app/src/main/java/com/kickstarter/viewmodels/projectpage/RewardsSelectionViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/projectpage/RewardsSelectionViewModel.kt index e069323e88..e1747307c0 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/projectpage/RewardsSelectionViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/projectpage/RewardsSelectionViewModel.kt @@ -93,7 +93,6 @@ class RewardsSelectionViewModel(private val environment: Environment, private va environment.currentConfigV2()?.observable()?.asFlow()?.collectLatest { if (shippingRulesUseCase == null) { shippingRulesUseCase = GetShippingRulesUseCase( - apolloClient, projectData.project(), it, viewModelScope, diff --git a/app/src/main/java/com/kickstarter/viewmodels/usecases/GetShippingRulesUseCase.kt b/app/src/main/java/com/kickstarter/viewmodels/usecases/GetShippingRulesUseCase.kt index d6716794a2..2aefc60b07 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/usecases/GetShippingRulesUseCase.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/usecases/GetShippingRulesUseCase.kt @@ -9,18 +9,14 @@ import com.kickstarter.models.Location import com.kickstarter.models.Project import com.kickstarter.models.Reward import com.kickstarter.models.ShippingRule -import com.kickstarter.services.ApolloClientTypeV2 import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.rx2.asFlow data class ShippingRulesState( val shippingRules: List = emptyList(), @@ -43,7 +39,6 @@ data class ShippingRulesState( * As the UseCase is lifecycle agnostic and is scoped to the class that uses it. */ class GetShippingRulesUseCase( - private val apolloClient: ApolloClientTypeV2, private val project: Project, private val config: Config?, private val scope: CoroutineScope, @@ -97,7 +92,7 @@ class GetShippingRulesUseCase( if (rewardsByShippingType.isNotEmpty() && project.isAllowedToPledge()) { rewardsByShippingType.forEachIndexed { index, reward -> - if (RewardUtils.shipsToRestrictedLocations(reward)) { + if (RewardUtils.shipsToRestrictedLocations(reward) || RewardUtils.shipsWorldwide(reward)) { reward.shippingRules()?.map { avShipMap.put( requireNotNull( @@ -107,9 +102,6 @@ class GetShippingRulesUseCase( ) } } - if (RewardUtils.shipsWorldwide(reward)) { - getGlobalShippingRulesForReward(reward, avShipMap) - } // - Filter rewards once all shipping rules have been collected if (index == rewardsByShippingType.size - 1) { @@ -162,29 +154,6 @@ class GetShippingRulesUseCase( ) } - private suspend fun getGlobalShippingRulesForReward( - reward: Reward, - shippingRules: MutableMap - ) { - apolloClient.getShippingRules(reward) - .asFlow() - .map { rulesEnvelope -> - rulesEnvelope.shippingRules()?.map { rule -> - rule?.let { - shippingRules.put( - requireNotNull( - it.location()?.id() - ), - it - ) - } - } - } - .catch { throwable -> - emitCurrentState(isLoading = false, errorMessage = throwable?.message) - }.collect() - } - /** * Check if the given @param rule is available in the list * of @param allAvailableShippingRules for this project. diff --git a/app/src/test/java/com/kickstarter/services/GraphQLTransformersTest.kt b/app/src/test/java/com/kickstarter/services/GraphQLTransformersTest.kt index 98c6d676b5..52208cb093 100644 --- a/app/src/test/java/com/kickstarter/services/GraphQLTransformersTest.kt +++ b/app/src/test/java/com/kickstarter/services/GraphQLTransformersTest.kt @@ -6,18 +6,26 @@ import com.kickstarter.services.transformers.aiDisclosureTransformer import com.kickstarter.services.transformers.decodeRelayId import com.kickstarter.services.transformers.environmentalCommitmentTransformer import com.kickstarter.services.transformers.projectFaqTransformer +import com.kickstarter.services.transformers.rewardTransformer +import com.kickstarter.services.transformers.simpleShippingRuleTransformer import com.kickstarter.services.transformers.updateTransformer import com.kickstarter.services.transformers.userPrivacyTransformer import com.kickstarter.services.transformers.userTransformer import fragment.AiDisclosure +import fragment.Amount import fragment.EnvironmentalCommitment import fragment.Faq +import fragment.FullProject +import fragment.Reward +import fragment.Reward.AllowedAddons import fragment.User import org.joda.time.DateTime import org.junit.Test import org.mockito.Mockito.mock import org.mockito.Mockito.`when` import type.EnvironmentalCommitmentCategory +import type.RewardType +import type.ShippingPreference class GraphQLTransformersTest : KSRobolectricTestCase() { @@ -176,4 +184,136 @@ class GraphQLTransformersTest : KSRobolectricTestCase() { assertTrue(post.publishedAt() == date) assertTrue(post.updatedAt() == date) } + + @Test + fun `test simpleShippingRuleTransformer provides appropriate shippingRule`() { + val canadaSimpleSR = FullProject.SimpleShippingRulesExpanded( + "SimpleShippingRule", + "17.34562379823645234875620384756203847234", + "CA", + null, + null, + "TG9jYXRpb24tMjM0MjQ3NzU=", + "Canada" + ) + + val australiaSR = FullProject.SimpleShippingRulesExpanded( + "SimpleShippingRule", + "0", + "AU", + "20", // - Shipping Rule disable at shipping, cost = 0, estimated range max/min values not null + "2", + "TG9jYXRpb24tMjM0MjQ3NDg=", + "Australia" + ) + + val forbiddenValues = FullProject.SimpleShippingRulesExpanded( + "SimpleShippingRule", + "Pikachusito", + "AU", + "AyOma", // - Shipping Rule disable at shipping, cost = 0, estimated range max/min values not null + "", + "TG9jYXRpb24tMjM0MjQ3NDg=", + "Australia" + ) + + val cadShipping = simpleShippingRuleTransformer(canadaSimpleSR) + val ausShipping = simpleShippingRuleTransformer(australiaSR) + + // - Expected output model in case of forbidden values (non-numeric) on Cost/Monetary related fields + val nonDoubleValues = simpleShippingRuleTransformer(forbiddenValues) + + assert(cadShipping.location()?.id() == decodeRelayId(canadaSimpleSR.locationId())) + assert(cadShipping.cost() == 17.345623798236453) // - Rounds up after 15th digit + assert(cadShipping.estimatedMax() == 0.0) + assert(cadShipping.estimatedMin() == 0.0) + assert(cadShipping.location()?.name() == canadaSimpleSR.locationName()) + assert(cadShipping.location()?.displayableName() == canadaSimpleSR.locationName()) + assert(cadShipping.location()?.country() == canadaSimpleSR.country()) + + assert(ausShipping.location()?.id() == decodeRelayId(australiaSR.locationId())) + assert(ausShipping.cost() == 0.0) + assert(ausShipping.estimatedMax() == 20.0) + assert(ausShipping.estimatedMin() == 2.0) + assert(ausShipping.location()?.name() == australiaSR.locationName()) + assert(ausShipping.location()?.displayableName() == australiaSR.locationName()) + assert(ausShipping.location()?.country() == australiaSR.country()) + + assert(nonDoubleValues.location()?.id() == decodeRelayId(forbiddenValues.locationId())) + assert(nonDoubleValues.cost() == 0.0) + assert(nonDoubleValues.estimatedMax() == 0.0) + assert(nonDoubleValues.estimatedMin() == 0.0) + assert(nonDoubleValues.location()?.name() == forbiddenValues.locationName()) + assert(nonDoubleValues.location()?.displayableName() == forbiddenValues.locationName()) + assert(nonDoubleValues.location()?.country() == forbiddenValues.country()) + } + + /** + * Reward{__typename=Reward, id=UmV3YXJkLTk2NTM3NzY=, name=Stormgate Supporter, ... + * Mock of a rewardFragment received from GraphQL data types + */ + val fragmentReward = Reward( + "Reward", + "UmV3YXJkLTk2NTM3NzY", + "Stormgate Supporter", + 452, + "Some reward description here", + DateTime.now().toDate(), + true, + Reward.Amount("Amount", Reward.Amount.Fragments(Amount("Money", "20.0", null, null))), + Reward.PledgeAmount( + "PledgeAmount", + Reward.PledgeAmount.Fragments(Amount("Money", "10.0", null, null)) + ), + Reward.LatePledgeAmount( + "LatePledgeAmount", + Reward.LatePledgeAmount.Fragments(Amount("Money", "30.0", null, null)) + ), + Reward.ConvertedAmount( + "ConvertedAmount", + Reward.ConvertedAmount.Fragments(Amount("Money", "30.0", null, null)) + ), + ShippingPreference.UNRESTRICTED, + 3, + 3, + 3, + DateTime.now().minusDays(50), + DateTime.now().plusDays(50), + RewardType.BASE, + AllowedAddons("RewardConnection", listOf(Reward.Node("Reward", "UmV3YXJkLTk3MDA2NjA"))), + null, + ) + + @Test + fun `test rewardTransformer returns appropriate shippingRules field when querying for simpleShippingRulesExpanded`() { + + val canadaSimpleSR = FullProject.SimpleShippingRulesExpanded( + "SimpleShippingRule", + "17.34562379823645234875620384756203847234", + "CA", + null, + null, + "TG9jYXRpb24tMjM0MjQ3NzU=", + "Canada" + ) + + val australiaSR = FullProject.SimpleShippingRulesExpanded( + "SimpleShippingRule", + "0", + "AU", + "20", // - Shipping Rule disable at shipping, cost = 0, estimated range max/min values not null + "2", + "TG9jYXRpb24tMjM0MjQ3NDg=", + "Australia" + ) + + val reward = rewardTransformer( + rewardGr = fragmentReward, + simpleShippingRules = listOf(canadaSimpleSR, australiaSR) + ) + + assertTrue(reward.shippingRules()?.size == 2) + assertTrue(reward.shippingRules()?.first()?.id() == decodeRelayId(canadaSimpleSR.locationId())) + assertTrue(reward.shippingRules()?.last()?.id() == decodeRelayId(australiaSR.locationId())) + } } diff --git a/app/src/test/java/com/kickstarter/viewmodels/RewardsSelectionViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/RewardsSelectionViewModelTest.kt index bf9f83db07..b1e8557518 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/RewardsSelectionViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/RewardsSelectionViewModelTest.kt @@ -13,11 +13,9 @@ import com.kickstarter.mock.factories.RewardFactory import com.kickstarter.mock.factories.ShippingRuleFactory import com.kickstarter.mock.factories.ShippingRulesEnvelopeFactory import com.kickstarter.mock.factories.UserFactory -import com.kickstarter.mock.services.MockApolloClientV2 import com.kickstarter.models.Backing import com.kickstarter.models.Project import com.kickstarter.models.Reward -import com.kickstarter.services.apiresponses.ShippingRulesEnvelope import com.kickstarter.ui.data.PledgeReason import com.kickstarter.ui.data.ProjectData import com.kickstarter.viewmodels.projectpage.FlowUIState @@ -25,7 +23,6 @@ import com.kickstarter.viewmodels.projectpage.RewardSelectionUIState import com.kickstarter.viewmodels.projectpage.RewardsSelectionViewModel import com.kickstarter.viewmodels.usecases.GetShippingRulesUseCase import com.kickstarter.viewmodels.usecases.ShippingRulesState -import io.reactivex.Observable import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch @@ -324,6 +321,7 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { Reward.builder().title("$it").id(it.toLong()).isAvailable(true).hasAddons(it != 2) .pledgeAmount(3.0) .shippingPreference(Reward.ShippingPreference.UNRESTRICTED.name) + .shippingRules(testShippingRulesList.shippingRules()) .shippingPreferenceType(Reward.ShippingPreference.UNRESTRICTED) .build() else @@ -349,17 +347,11 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { .currentUserV2(MockCurrentUserV2(user)) .build() - val apolloClient = object : MockApolloClientV2() { - override fun getShippingRules(reward: Reward): Observable { - return Observable.just(testShippingRulesList) - } - } - val dispatcher = UnconfinedTestDispatcher(testScheduler) val shippingUiState = mutableListOf() backgroundScope.launch(dispatcher) { - val useCase = GetShippingRulesUseCase(apolloClient, testProject, config, this, dispatcher) + val useCase = GetShippingRulesUseCase(testProject, config, this, dispatcher) createViewModel(env, useCase) viewModel.provideProjectData(testProjectData) @@ -379,9 +371,9 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { testRewards[8] ) - assertEquals(shippingUiState.size, 5) - assertEquals(shippingUiState[3].loading, true) - assertEquals(shippingUiState[4].loading, false) + assertEquals(shippingUiState.size, 4) + assertEquals(shippingUiState[2].loading, true) + assertEquals(shippingUiState[3].loading, false) // - make sure the uiState output reward list is filtered assertEquals(shippingUiState.last().filteredRw.size, filteredRewards.size) @@ -475,12 +467,10 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { .currentUserV2(MockCurrentUserV2(user)) .build() - val apolloClient = requireNotNull(env.apolloClientV2()) - val dispatcher = UnconfinedTestDispatcher(testScheduler) val shippingUiState = mutableListOf() backgroundScope.launch(dispatcher) { - val useCase = GetShippingRulesUseCase(apolloClient, project, config, this, dispatcher) + val useCase = GetShippingRulesUseCase(project, config, this, dispatcher) createViewModel(env, useCase) viewModel.provideProjectData(projectData) viewModel.shippingUIState.toList(shippingUiState) @@ -496,23 +486,18 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { @Test fun `config is from Canada and available rules are global so Default Shipping is Canada, and list of shipping Rules provided matches all available reward global shipping`() = runTest { + val testShippingRulesList = ShippingRulesEnvelopeFactory.shippingRules() val rw = RewardFactory .reward() .toBuilder() .shippingPreference(Reward.ShippingPreference.UNRESTRICTED.name) .shippingPreferenceType(Reward.ShippingPreference.UNRESTRICTED) + .shippingRules(testShippingRulesList.shippingRules()) .build() val user = UserFactory.user() val project = ProjectFactory.project().toBuilder().rewards(listOf(rw, rw, rw)).build() val projectData = ProjectDataFactory.project(project, null, null) - val testShippingRulesList = ShippingRulesEnvelopeFactory.shippingRules() - val apolloClient = object : MockApolloClientV2() { - override fun getShippingRules(reward: Reward): Observable { - return Observable.just(testShippingRulesList) - } - } - val config = ConfigFactory.configForCA() val currentConfig = MockCurrentConfigV2() currentConfig.config(config) @@ -520,14 +505,13 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { val env = environment() .toBuilder() .currentConfig2(currentConfig) - .apolloClientV2(apolloClient) .currentUserV2(MockCurrentUserV2(user)) .build() val dispatcher = UnconfinedTestDispatcher(testScheduler) val shippingUiState = mutableListOf() backgroundScope.launch(dispatcher) { - val useCase = GetShippingRulesUseCase(apolloClient, project, config, this, dispatcher) + val useCase = GetShippingRulesUseCase(project, config, this, dispatcher) createViewModel(env, useCase) viewModel.provideProjectData(projectData) viewModel.shippingUIState.toList(shippingUiState) @@ -535,7 +519,7 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { advanceUntilIdle() // wait until all state emissions completed - assertEquals(shippingUiState.size, 3) + assertEquals(shippingUiState.size, 2) assertEquals(shippingUiState.last().selectedShippingRule.location()?.name(), "Canada") assertEquals(shippingUiState.last().shippingRules, testShippingRulesList.shippingRules()) } diff --git a/app/src/test/java/com/kickstarter/viewmodels/usecases/GetShippingRulesUseCaseTest.kt b/app/src/test/java/com/kickstarter/viewmodels/usecases/GetShippingRulesUseCaseTest.kt index 8c83f68909..fb0a3b14f3 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/usecases/GetShippingRulesUseCaseTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/usecases/GetShippingRulesUseCaseTest.kt @@ -33,7 +33,7 @@ class GetShippingRulesUseCaseTest : KSRobolectricTestCase() { val dispatcher = UnconfinedTestDispatcher(testScheduler) val scope = backgroundScope - val useCase = GetShippingRulesUseCase(apolloClient, project, config, scope, dispatcher) + val useCase = GetShippingRulesUseCase(project, config, scope, dispatcher) val state = mutableListOf() scope.launch(dispatcher) { @@ -77,7 +77,7 @@ class GetShippingRulesUseCaseTest : KSRobolectricTestCase() { val dispatcher = UnconfinedTestDispatcher(testScheduler) val scope = backgroundScope - val useCase = GetShippingRulesUseCase(apolloClient, project, config, scope, dispatcher) + val useCase = GetShippingRulesUseCase(project, config, scope, dispatcher) val state = mutableListOf() scope.launch(dispatcher) {