Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/addon purchase api impl #2355

Merged
merged 9 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
query TravelAddonBanner($flow: UpsellTravelAddonFlow!) {
currentMember {
upsellTravelAddonBanner (flow: $flow) {
badges
contractIds
descriptionDisplayName
titleDisplayName
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ package com.hedvig.android.data.addons.data

import arrow.core.Either
import arrow.core.NonEmptyList
import arrow.core.nonEmptyListOf
import arrow.core.raise.either
import arrow.core.toNonEmptyListOrNull
import com.apollographql.apollo.ApolloClient
import com.hedvig.android.apollo.safeExecute
import com.hedvig.android.core.common.ErrorMessage
import com.hedvig.android.featureflags.FeatureManager
import com.hedvig.android.featureflags.flags.Feature
import com.hedvig.android.logger.LogPriority
import com.hedvig.android.logger.logcat
import kotlin.String
import kotlinx.coroutines.flow.first
import octopus.TravelAddonBannerQuery
import octopus.type.UpsellTravelAddonFlow

interface GetTravelAddonBannerInfoUseCase {
suspend fun invoke(source: TravelAddonBannerSource): Either<ErrorMessage, TravelAddonBannerInfo?>
Expand All @@ -30,13 +33,39 @@ internal class GetTravelAddonBannerInfoUseCaseImpl(
}
null
} else {
// TODO: actual impl here!!!!
// TODO: and null if eligibleInsurancesIds is empty
TravelAddonBannerInfo(
title = "Travel Plus",
description = "Extended travel insurance with extra coverage for your travels",
labels = listOf("Popular"),
eligibleInsurancesIds = nonEmptyListOf("id1"),
val mappedSource = when (source) {
TravelAddonBannerSource.TRAVEL_CERTIFICATES -> UpsellTravelAddonFlow.APP_UPSELL_UPGRADE
TravelAddonBannerSource.INSURANCES_TAB -> UpsellTravelAddonFlow.APP_ONLY_UPSALE
}
apolloClient.query(TravelAddonBannerQuery(mappedSource)).safeExecute().fold(
ifLeft = { error ->
logcat(LogPriority.ERROR) { "Error from travelAddonBannerQuery from source: $mappedSource: $error" }
raise(ErrorMessage())
},
ifRight = { result ->
val bannerData = result.currentMember.upsellTravelAddonBanner
if (bannerData == null) {
logcat(LogPriority.DEBUG) { "Got null response from TravelAddonBannerQuery" }
null
} else {
val nonEmptyContracts = bannerData.contractIds.toNonEmptyListOrNull()
if (nonEmptyContracts.isNullOrEmpty()) {
logcat(LogPriority.ERROR) {
"Got non null response from TravelAddonBannerQuery from source: " +
"$mappedSource, but contractIds are empty"
}
null
} else {
TravelAddonBannerInfo(
title = bannerData.titleDisplayName,
description = bannerData.descriptionDisplayName,
labels = bannerData.badges,
eligibleInsurancesIds = nonEmptyContracts,
bannerSource = mappedSource,
)
}
}
},
)
}
}
Expand All @@ -48,6 +77,7 @@ data class TravelAddonBannerInfo(
val description: String,
val labels: List<String>,
val eligibleInsurancesIds: NonEmptyList<String>,
val bannerSource: UpsellTravelAddonFlow,
)

enum class TravelAddonBannerSource {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
mutation UpsellAddonOffer($contractId: ID!) {
upsellTravelAddonOffer(contractId: $contractId) {
offer {
activationDate
currentAddon {
displayItems {
displayValue
displayTitle
}
premium {
...MoneyFragment
}
}
descriptionDisplayName
quotes {
addonId
displayItems {
displayTitle
displayValue
}
displayName
premium {
...MoneyFragment
}
quoteId
}
titleDisplayName
}
userError {
message
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mutation UpsellTravelAddonActivate($addonId: ID!, $quoteId: ID!) {
upsellTravelAddonActivate(addonId: $addonId, quoteId: $quoteId) {
userError {
message
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.hedvig.android.feature.addon.purchase.data

import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.cache.normalized.FetchPolicy
import com.apollographql.apollo.cache.normalized.fetchPolicy
Expand Down Expand Up @@ -41,21 +42,12 @@ internal class GetInsuranceForTravelAddonUseCaseImpl(
}
raise(ErrorMessage())
} else {
// TODO: remove mock!
// val result = memberResponse.bind().currentMember.toInsurancesForAddon(ids)
// ensure(result.isNotEmpty()) {
// { "Tried to get list of insurances for addon purchase but the list is empty!" }
// ErrorMessage()
// }
// result
listOf<InsuranceForAddon>(
InsuranceForAddon(
id = "id",
displayName = "Rent Bas",
"Tulegatan 1",
ContractGroup.RENTAL,
),
) // TODO: remove mock!!
val result = memberResponse.bind().currentMember.toInsurancesForAddon(ids)
ensure(result.isNotEmpty()) {
logcat { "Tried to get list of insurances for addon purchase but the list is empty!" }
ErrorMessage()
}
result
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package com.hedvig.android.feature.addon.purchase.data

import arrow.core.Either
import arrow.core.NonEmptyList
import arrow.core.nonEmptyListOf
import arrow.core.raise.either
import arrow.core.toNonEmptyListOrNull
import com.apollographql.apollo.ApolloClient
import com.hedvig.android.apollo.safeExecute
import com.hedvig.android.core.common.ErrorMessage
import com.hedvig.android.core.uidata.UiCurrencyCode
import com.hedvig.android.core.uidata.UiMoney
import com.hedvig.android.data.productvariant.InsuranceVariantDocument
import com.hedvig.android.feature.addon.purchase.data.Addon.TravelAddonOffer
import com.hedvig.android.featureflags.FeatureManager
import com.hedvig.android.featureflags.flags.Feature
import com.hedvig.android.logger.LogPriority
import com.hedvig.android.logger.logcat
import kotlin.String
import kotlinx.coroutines.flow.first
import kotlinx.datetime.LocalDate
import octopus.UpsellAddonOfferMutation

internal interface GetTravelAddonOfferUseCase {
suspend fun invoke(id: String): Either<ErrorMessage, TravelAddonOffer>
Expand All @@ -21,13 +30,77 @@ internal class GetTravelAddonOfferUseCaseImpl(
private val featureManager: FeatureManager,
) : GetTravelAddonOfferUseCase {
override suspend fun invoke(id: String): Either<ErrorMessage, TravelAddonOffer> {
// todo: REMOVE MOCK!
return either {
mockWithUpgrade
val isAddonFlagOn = featureManager.isFeatureEnabled(Feature.TRAVEL_ADDON).first()
if (!isAddonFlagOn) {
logcat(LogPriority.ERROR) { "Tried to start UpsellAddonOfferMutation but addon feature flag is off" }
raise(ErrorMessage())
}
apolloClient.mutation(UpsellAddonOfferMutation(id)).safeExecute().fold(
ifLeft = { error ->
logcat(LogPriority.ERROR) { "Tried to start UpsellAddonOfferMutation but got error: $error" }
// not passing error message to the member here, as we want to redirect member to chat if there is a message
raise(ErrorMessage())
},
ifRight = { result ->
if (result.upsellTravelAddonOffer.userError != null) {
raise(ErrorMessage(result.upsellTravelAddonOffer.userError.message))
// the only case where we want to redirect to chat
}
val data = result.upsellTravelAddonOffer.offer
if (data == null) {
logcat(LogPriority.ERROR) { "Tried to do UpsellAddonOfferMutation but got null offer" }
raise(ErrorMessage())
}
val nonEmptyQuotes = data.quotes.toNonEmptyListOrNull()
if (nonEmptyQuotes.isNullOrEmpty()) {
logcat(LogPriority.ERROR) { "Tried to do UpsellAddonOfferMutation but got empty quotes" }
raise(ErrorMessage())
}

TravelAddonOffer(
addonOptions = nonEmptyQuotes.toTravelAddonQuotes(),
title = data.titleDisplayName,
description = data.descriptionDisplayName,
activationDate = data.activationDate,
currentTravelAddon = data.currentAddon.toCurrentAddon(),
)
},
)
}
}
}

private fun NonEmptyList<UpsellAddonOfferMutation.Data.UpsellTravelAddonOffer.Offer.Quote>.toTravelAddonQuotes(): NonEmptyList<TravelAddonQuote> {
return this.map {
TravelAddonQuote(
quoteId = it.quoteId,
addonId = it.addonId,
displayName = it.displayName,
price = UiMoney.fromMoneyFragment(it.premium),
addonVariant = AddonVariant(
documents = listOf(), // todo: Addons - populate when api changes!
termsVersion = "", // todo: Addons - populate when api changes!
panasetskaya marked this conversation as resolved.
Show resolved Hide resolved
displayDetails = it.displayItems.map { item ->
item.displayTitle to item.displayValue
},
),
)
}
}

private fun UpsellAddonOfferMutation.Data.UpsellTravelAddonOffer.Offer.CurrentAddon?.toCurrentAddon(): CurrentTravelAddon? {
return this?.let {
CurrentTravelAddon(
price = UiMoney.fromMoneyFragment(premium),
displayDetails = displayItems.map {
it.displayTitle to it.displayValue
},
)
}
}

// todo: remove mocks when not needed
private val mockWithoutUpgrade = TravelAddonOffer(
addonOptions = nonEmptyListOf(
TravelAddonQuote(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,32 @@ package com.hedvig.android.feature.addon.purchase.data
import arrow.core.Either
import arrow.core.raise.either
import com.apollographql.apollo.ApolloClient
import com.hedvig.android.apollo.safeExecute
import com.hedvig.android.core.common.ErrorMessage
import com.hedvig.android.featureflags.FeatureManager
import kotlinx.datetime.LocalDate
import com.hedvig.android.logger.LogPriority
import com.hedvig.android.logger.logcat
import octopus.UpsellTravelAddonActivateMutation

internal interface SubmitAddonPurchaseUseCase {
suspend fun invoke(quoteId: String, addonId: String): Either<ErrorMessage, LocalDate>
suspend fun invoke(quoteId: String, addonId: String): Either<ErrorMessage, Unit>
panasetskaya marked this conversation as resolved.
Show resolved Hide resolved
}

internal class SubmitAddonPurchaseUseCaseImpl(
private val apolloClient: ApolloClient,
private val featureManager: FeatureManager,
) : SubmitAddonPurchaseUseCase {
override suspend fun invoke(quoteId: String, addonId: String): Either<ErrorMessage, LocalDate> {
// TODO: REMOVE MOCK!
override suspend fun invoke(quoteId: String, addonId: String): Either<ErrorMessage, Unit> {
return either {
LocalDate(2025, 1, 1)
apolloClient.mutation(UpsellTravelAddonActivateMutation(addonId = addonId, quoteId = quoteId)).safeExecute().fold(
ifLeft = { error ->
logcat(LogPriority.ERROR) { "Tried to do UpsellTravelAddonActivateMutation but got error: $error" }
raise(ErrorMessage())
},
ifRight = { result ->
if (result.upsellTravelAddonActivate.userError != null) {
raise(ErrorMessage(result.upsellTravelAddonActivate.userError.message))
}
},
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ val addonPurchaseModule = module {
single<SubmitAddonPurchaseUseCase> {
SubmitAddonPurchaseUseCaseImpl(
apolloClient = get<ApolloClient>(),
featureManager = get<FeatureManager>(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,12 @@ internal class AddonSummaryPresenter(
).fold(
ifLeft = {
currentState = initialState.copy(navigateToFailure = true)
// todo: not really passing UserError message here. Should we? Or should we maybe redirect to chat in
// the case of final failure?
Comment on lines +58 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the question regarding if we show the "open chat" button here or not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well yes, but it's not in the beginning of the flow, but in the end. Should we also redirect them to chat there, wdyt? Because UserError seems like it's smth member-specific, so

},
ifRight = { date ->
currentState = initialState.copy(activationDateForSuccessfullyPurchasedAddon = date)
currentState =
initialState.copy(activationDateForSuccessfullyPurchasedAddon = summaryParameters.activationDate)
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import com.hedvig.android.pullrefresh.pullRefresh
import com.hedvig.android.pullrefresh.rememberPullRefreshState
import hedvig.resources.R
import kotlinx.datetime.LocalDate
import octopus.type.UpsellTravelAddonFlow

@Composable
internal fun InsuranceDestination(
Expand Down Expand Up @@ -492,6 +493,7 @@ private class InsuranceUiStateProvider : CollectionPreviewParameterProvider<Insu
description = "Extended travel insurance with extra coverage for your travels",
labels = listOf("Popular"),
eligibleInsurancesIds = nonEmptyListOf("id"),
bannerSource = UpsellTravelAddonFlow.APP_ONLY_UPSALE,
),
),
InsuranceUiState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import kotlinx.coroutines.test.runTest
import kotlinx.datetime.LocalDate
import octopus.CrossSellsQuery
import octopus.type.CrossSellType
import octopus.type.UpsellTravelAddonFlow
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -543,5 +544,6 @@ internal class InsurancePresenterTest {
"desc",
listOf(),
nonEmptyListOf("id"),
bannerSource = UpsellTravelAddonFlow.APP_ONLY_UPSALE,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import java.io.File
import kotlin.String
import kotlinx.datetime.LocalDate
import kotlinx.datetime.toJavaLocalDate
import octopus.type.UpsellTravelAddonFlow

@Composable
internal fun TravelCertificateHistoryDestination(
Expand Down Expand Up @@ -376,6 +377,7 @@ private class TravelCertificateHistoryUiStatePreviewProvider :
description = "Extended travel insurance with extra coverage for your travels",
labels = listOf("Popular"),
eligibleInsurancesIds = nonEmptyListOf("id"),
bannerSource = UpsellTravelAddonFlow.APP_UPSELL_UPGRADE,
),
),
SuccessDownloadingHistory(
Expand Down
Loading