From 2e4494b5a35ad1c8a05bc5f2d1e80c31f5124796 Mon Sep 17 00:00:00 2001 From: Mohit Rajput Date: Fri, 30 May 2025 00:16:43 -0700 Subject: [PATCH] NTP-65849 | pay bill view model modularization (#16378) --- .../bbps/common/utils/NaviBbpsCommonUtils.kt | 4 - .../bbps/feature/paybill/PayBillViewModel.kt | 526 +++--------------- .../feature/paybill/helper/BbpsArcHelper.kt | 53 ++ .../helper/NotifyDuplicatePaymentHandler.kt | 303 ++++++++++ .../helper/PayBillAmountChipsHelper.kt | 174 ++++++ .../bbps/feature/paybill/ui/PayBillScreen.kt | 20 +- 6 files changed, 620 insertions(+), 460 deletions(-) create mode 100644 android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/BbpsArcHelper.kt create mode 100644 android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/NotifyDuplicatePaymentHandler.kt create mode 100644 android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/PayBillAmountChipsHelper.kt diff --git a/android/navi-bbps/src/main/kotlin/com/navi/bbps/common/utils/NaviBbpsCommonUtils.kt b/android/navi-bbps/src/main/kotlin/com/navi/bbps/common/utils/NaviBbpsCommonUtils.kt index 1156af13a4..560c7d8d4e 100644 --- a/android/navi-bbps/src/main/kotlin/com/navi/bbps/common/utils/NaviBbpsCommonUtils.kt +++ b/android/navi-bbps/src/main/kotlin/com/navi/bbps/common/utils/NaviBbpsCommonUtils.kt @@ -136,10 +136,6 @@ object NaviBbpsCommonUtils { return categoryId == CATEGORY_ID_CREDIT_CARD } - fun isCategoryOfTypeAmountChipsRequired(categoryId: String): Boolean { - return categoryId in listOf(CATEGORY_ID_DTH, CATEGORY_ID_FASTAG) - } - fun isCategoryOfTypeLocationRequired(categoryId: String): Boolean { return categoryId in listOf( diff --git a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/PayBillViewModel.kt b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/PayBillViewModel.kt index 3af5fec12d..c96a6471f6 100644 --- a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/PayBillViewModel.kt +++ b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/PayBillViewModel.kt @@ -10,16 +10,12 @@ package com.navi.bbps.feature.paybill import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.navi.adverse.flux.provider.GsonProvider.gson -import com.navi.base.cache.repository.NaviCacheRepository import com.navi.base.model.CtaData -import com.navi.base.utils.DateUtils import com.navi.base.utils.EMPTY import com.navi.base.utils.NaviNetworkConnectivity import com.navi.base.utils.ResourceProvider import com.navi.base.utils.TrustedTimeAccessor import com.navi.base.utils.ZERO_STRING -import com.navi.base.utils.isNotNull import com.navi.base.utils.orFalse import com.navi.base.utils.orZero import com.navi.bbps.R @@ -32,29 +28,23 @@ import com.navi.bbps.common.CATEGORY_ID_DTH import com.navi.bbps.common.CATEGORY_ID_MOBILE_POSTPAID import com.navi.bbps.common.CATEGORY_ID_MOBILE_PREPAID import com.navi.bbps.common.CoinsSyncManager -import com.navi.bbps.common.DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR import com.navi.bbps.common.DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR_COMMA_TIME import com.navi.bbps.common.DISPLAYABLE_MOBILE_NUMBER_KEY import com.navi.bbps.common.NaviBbpsAnalytics import com.navi.bbps.common.NaviBbpsScreen -import com.navi.bbps.common.SYMBOL_RUPEE import com.navi.bbps.common.TAG_BILL_FETCH_ERROR import com.navi.bbps.common.model.NaviBbpsVmData -import com.navi.bbps.common.model.config.ChipsConfigData import com.navi.bbps.common.model.config.NaviBbpsDefaultConfig import com.navi.bbps.common.repository.BbpsCommonRepository -import com.navi.bbps.common.session.DuplicatePaymentBottomSheetViewTracker import com.navi.bbps.common.session.NaviBbpsSessionHelper import com.navi.bbps.common.session.ZeroPlatformFeeBottomSheetViewTracker import com.navi.bbps.common.usecase.FetchBillHandler -import com.navi.bbps.common.usecase.FindLastOrderWithSuccessfulPaymentUseCase import com.navi.bbps.common.usecase.NaviBbpsConfigUseCase import com.navi.bbps.common.usecase.RewardNudgeUseCase import com.navi.bbps.common.utils.NaviBbpsCommonUtils import com.navi.bbps.common.utils.NaviBbpsCommonUtils.evaluateMvelExpression import com.navi.bbps.common.utils.NaviBbpsCommonUtils.getBbpsMetricInfo import com.navi.bbps.common.utils.NaviBbpsCommonUtils.getValidatedAmountNumber -import com.navi.bbps.common.utils.NaviBbpsCommonUtils.isCategoryOfTypeAmountChipsRequired import com.navi.bbps.common.utils.NaviBbpsDateUtils import com.navi.bbps.common.utils.getDefaultConfig import com.navi.bbps.common.utils.getDisplayableAmount @@ -70,7 +60,6 @@ import com.navi.bbps.feature.category.model.view.BillCategoryEntity import com.navi.bbps.feature.contactlist.model.view.PhoneContactEntity import com.navi.bbps.feature.customerinput.model.network.DeviceDetails import com.navi.bbps.feature.customerinput.model.view.BillDetailsEntity -import com.navi.bbps.feature.customerinput.model.view.BillPeriod import com.navi.bbps.feature.customerinput.model.view.BillerAdditionalParamsEntity import com.navi.bbps.feature.customerinput.model.view.BillerDetailsEntity import com.navi.bbps.feature.destinations.BbpsPostPaymentScreenDestination @@ -78,10 +67,12 @@ import com.navi.bbps.feature.destinations.BbpsTransactionDetailsScreenDestinatio import com.navi.bbps.feature.destinations.PrepaidRechargeScreenDestination import com.navi.bbps.feature.mybills.MyBillsRepository import com.navi.bbps.feature.mybills.model.view.MyBillEntity +import com.navi.bbps.feature.paybill.helper.BbpsArcHelper +import com.navi.bbps.feature.paybill.helper.NotifyDuplicatePaymentHandler +import com.navi.bbps.feature.paybill.helper.PayBillAmountChipsHelper import com.navi.bbps.feature.paybill.model.network.PayBillRequest import com.navi.bbps.feature.paybill.model.network.PayBillResponse import com.navi.bbps.feature.paybill.model.network.PaymentAmountExactness -import com.navi.bbps.feature.paybill.model.view.AmountChipEntity import com.navi.bbps.feature.paybill.model.view.CoinUtilisationProperties import com.navi.bbps.feature.paybill.model.view.CoinUtilisationPropertiesV2 import com.navi.bbps.feature.paybill.model.view.CreditCardAmountType @@ -107,13 +98,10 @@ import com.navi.common.utils.Constants import com.navi.common.utils.Constants.LITMUS_EXPERIMENT_NAVIPAY_NAVI_POWER_PLAY import com.navi.common.utils.TemporaryStorageHelper import com.navi.common.utils.toJsonObject -import com.navi.pay.tstore.list.model.view.OrderStatusOfView import com.navi.payment.nativepayment.utils.NaviPaymentRewardsEventBus import com.navi.payment.nativepayment.utils.getDiscountAdjustedAmount import com.navi.payment.paymentscreen.utils.PaymentNavigator import com.navi.payment.tstore.repository.TStoreOrderHandler -import com.navi.payments.shared.feature.arc.constant.ARC_NUDGE_RESPONSE_CACHE_KEY -import com.navi.payments.shared.feature.arc.model.network.ArcNudgeResponse import com.navi.uitron.utils.isNotNullAndNotEmpty import com.navi.uitron.utils.orVal import com.ramcosta.composedestinations.spec.Direction @@ -134,7 +122,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.joda.time.DateTime import org.json.JSONObject typealias OrderReferenceId = String @@ -158,22 +145,21 @@ constructor( private val resProvider: ResourceProvider, private val rewardsNudgeEntityFetchUseCase: RewardNudgeUseCase, private val naviPaymentRewardEventBus: NaviPaymentRewardsEventBus, - private val naviCacheRepository: NaviCacheRepository, private val naviBbpsCommonRepository: BbpsCommonRepository, private val resourceProvider: ResourceProvider, private val litmusExperimentsUseCase: LitmusExperimentsUseCase, - private val findLastOrderWithSuccessfulPaymentUseCase: - FindLastOrderWithSuccessfulPaymentUseCase, - private val duplicatePaymentBottomSheetViewTracker: DuplicatePaymentBottomSheetViewTracker, private val zeroPlatformFeeBottomSheetViewTracker: ZeroPlatformFeeBottomSheetViewTracker, + val payBillAmountChipsHelper: PayBillAmountChipsHelper, + val bbpsArcHelper: BbpsArcHelper, + val notifyDuplicatePaymentHandler: NotifyDuplicatePaymentHandler, ) : NaviBbpsBaseVM( naviBbpsVmData = NaviBbpsVmData(screen = NaviBbpsScreen.NAVI_BBPS_PAY_BILL_SCREEN) ) { companion object { private const val CC_MIN_DUE_AMOUNT_KEY = "MinimumDueAmount" - private const val BILLER_MIN_ACCEPTED_AMOUNT = "Biller_Min_Accepted_Amount" - private const val BILLER_MAX_ACCEPTED_AMOUNT = "Biller_Max_Accepted_Amount" + const val BILLER_MIN_ACCEPTED_AMOUNT = "Biller_Min_Accepted_Amount" + const val BILLER_MAX_ACCEPTED_AMOUNT = "Biller_Max_Accepted_Amount" } private val naviBbpsAnalytics: NaviBbpsAnalytics.PayBill = NaviBbpsAnalytics.INSTANCE.PayBill() @@ -202,13 +188,10 @@ constructor( private val _showErrorText = MutableStateFlow(false) val showErrorText = _showErrorText.asStateFlow() - private val _amountChipEntityList = MutableStateFlow>(emptyList()) - val amountChipEntityList = _amountChipEntityList.asStateFlow() - private val _shouldAutoFocusOnAmount = MutableStateFlow(false) val shouldAutoFocusOnAmount = _shouldAutoFocusOnAmount.asStateFlow() - val paymentAmountExactness = MutableStateFlow(PaymentAmountExactness.EXACT) + private val paymentAmountExactness = MutableStateFlow(PaymentAmountExactness.EXACT) // ignore this for case when isAdhoc is true, for others use it to show error text private val _isAdhoc = MutableStateFlow(false) @@ -284,21 +267,11 @@ constructor( private val _isConsentViewVisible = MutableStateFlow(false) val isConsentViewVisible = _isConsentViewVisible.asStateFlow() - private val _isArcProtected = MutableStateFlow(false) - val isArcProtected = _isArcProtected.asStateFlow() - - private val amountChips = MutableStateFlow(emptyList()) - private val _showBillDetailsUpdatedBottomSheet = MutableSharedFlow() val showBillDetailsUpdatedBottomSheet = _showBillDetailsUpdatedBottomSheet.asSharedFlow() - private val _showDuplicatePaymentBottomSheet = MutableSharedFlow() - val showDuplicatePaymentBottomSheet = _showDuplicatePaymentBottomSheet.asSharedFlow() - var isPayButtonClicked = false - var arcNudgeResponse: ArcNudgeResponse? = null - @Inject lateinit var paymentNavigator: PaymentNavigator val errorMessageId = @@ -336,13 +309,17 @@ constructor( private val _isLottieAnimationShown = MutableStateFlow(false) val isLottieAnimationShown = _isLottieAnimationShown.asStateFlow() - var isBillFetchFailed = false + private var isBillFetchFailed = false init { viewModelScope.launch(dispatcherProvider.io) { // Concurrent calls section launch { initConfig() } - launch { updateArcProtectedStatus() } + launch { + bbpsArcHelper.updateArcProtectedStatus( + billerId = billDetailsEntity.value?.billerId.orEmpty() + ) + } launch { getLitmusExperimentValues() } launch { fetchBillAndUpdateScreen() } launch { updateScreenState() } @@ -476,6 +453,30 @@ constructor( _isBillLoading.update { isLoading } } + private fun notifyDuplicatePayment() { + notifyDuplicatePaymentHandler.notifyDuplicatePayment( + coroutineScope = viewModelScope, + source = source, + initialSource = initialSource, + billDetailsEntity = billDetailsEntity.value, + onPayBillBottomSheetType = { payBillBottomSheetType -> + _payBillBottomSheetType.update { payBillBottomSheetType } + }, + navigateToOrderHistoryDetailsScreen = { orderReferenceId -> + viewModelScope.launch(dispatcherProvider.io) { + _navigateToOrderDetailsScreen.emit( + Pair(first = true, second = orderReferenceId) + ) + } + }, + navigateToBillHistoryScreen = { + viewModelScope.launch(dispatcherProvider.io) { + _navigateToBillHistoryScreen.emit(value = true) + } + }, + ) + } + private suspend fun fetchBillAndUpdateScreen() { if (billDetailsEntity.value == null) { if (!naviNetworkConnectivity.isInternetConnected()) { @@ -582,137 +583,28 @@ constructor( } } if ( - !isCategoryOfTypeAmountChipsRequired( + !payBillAmountChipsHelper.isCategoryOfTypeAmountChipsRequired( categoryId = billCategoryEntity?.categoryId.orEmpty() ) ) { updateShouldAutoFocusOnAmount(paymentAmount = paymentAmount.value) return } - updateAmountChips(chipsConfigData = naviBbpsDefaultConfig.value.chipsConfigData) - } - - private fun updateAmountChips(chipsConfigData: ChipsConfigData) { - - var minChipAmount = chipsConfigData.genericMinAmount - var maxChipAmount = chipsConfigData.genericMaxAmount - - when (paymentAmountExactness.value) { - PaymentAmountExactness.EXACT_AND_ABOVE -> { - initialPaymentAmount.value.toDoubleOrNull()?.let { - minChipAmount = - minChipAmount.coerceAtLeast(initialPaymentAmount.value.toDouble().toInt()) - } - } - PaymentAmountExactness.EXACT_AND_BELOW -> { - initialPaymentAmount.value.toDoubleOrNull()?.let { - maxChipAmount = - maxChipAmount.coerceAtMost(initialPaymentAmount.value.toDouble().toInt()) - } - } - else -> {} - } - - val billerLevelMinAmountEntity = - billerAdditionalParams.value.find { it.paramName == BILLER_MIN_ACCEPTED_AMOUNT } - val billerLevelMaxAmountEntity = - billerAdditionalParams.value.find { it.paramName == BILLER_MAX_ACCEPTED_AMOUNT } - - billerLevelMinAmountEntity?.value?.toIntOrNull()?.let { - minChipAmount = minChipAmount.coerceAtLeast(billerLevelMinAmountEntity.value.toInt()) - } - - billerLevelMaxAmountEntity?.value?.toIntOrNull()?.let { - maxChipAmount = maxChipAmount.coerceAtMost(billerLevelMaxAmountEntity.value.toInt()) - } - viewModelScope.launch(dispatcherProvider.io) { - val secondChipAmount = - calculateChipAmountFromFormula( - minChipAmount = minChipAmount.toInt(), - maxChipAmount = maxChipAmount.toInt(), - chipFormulaExpression = chipsConfigData.secondChipFormulaExpression, - ) - - if (secondChipAmount == ZERO_STRING) { - updateShouldAutoFocusOnAmount(paymentAmount = paymentAmount.value) - return@launch - } - - val thirdChipAmount = - calculateChipAmountFromFormula( - minChipAmount = minChipAmount.toInt(), - maxChipAmount = maxChipAmount.toInt(), - chipFormulaExpression = chipsConfigData.thirdChipFormulaExpression, - ) - - if (thirdChipAmount == ZERO_STRING) { - updateShouldAutoFocusOnAmount(paymentAmount = paymentAmount.value) - return@launch - } - - val shouldShowChip = - maxChipAmount - minChipAmount >= chipsConfigData.minimumRequiredDifference - - if (!shouldShowChip) { - updateShouldAutoFocusOnAmount(paymentAmount = paymentAmount.value) - return@launch - } - - amountChips.update { - listOf( - minChipAmount.toString(), - secondChipAmount, - thirdChipAmount, - maxChipAmount.toString(), - ) - } - - transformAmountChipsToAmountChipEntity(amountChips = amountChips.value) - - if ( - initialPaymentAmount.value == ZERO_STRING || - initialPaymentAmount.value == amountChipEntityList.value.first().amount - ) { - updatePaymentAmount(newAmountValue = amountChipEntityList.value.first().amount) - updateAmountChipEntity(amount = amountChipEntityList.value.first().amount) - } - } - } - - private suspend fun calculateChipAmountFromFormula( - minChipAmount: Int, - maxChipAmount: Int, - chipFormulaExpression: String, - ): String { - return evaluateMvelExpression( - key = chipFormulaExpression, - data = mapOf("minChipAmount" to minChipAmount, "maxChipAmount" to maxChipAmount), - defaultValue = ZERO_STRING, - ) - } - - private fun transformAmountChipsToAmountChipEntity(amountChips: List) { - _amountChipEntityList.update { - amountChips.map { amount -> AmountChipEntity(amount = amount, isSelected = false) } - } - } - - open fun updateAmountChipEntity(amount: String) { - amountChipEntityList.value.isNotNull().let { - updateAmountChipEntityList( - amountChipList = - amountChipEntityList.value.map { amountChipEntity -> - amountChipEntity.copy(isSelected = amountChipEntity.amount == amount) - } + payBillAmountChipsHelper.updateAmountChips( + chipsConfigData = naviBbpsDefaultConfig.value.chipsConfigData, + paymentAmountExactness = paymentAmountExactness.value, + initialPaymentAmount = initialPaymentAmount.value, + paymentAmount = paymentAmount.value, + billerAdditionalParams = billerAdditionalParams.value, + onUpdateShouldAutoFocusOnAmount = { paymentAmount -> + updateShouldAutoFocusOnAmount(paymentAmount = paymentAmount) + }, + onUpdatePaymentAmount = { amount -> updatePaymentAmount(newAmountValue = amount) }, ) } } - private fun updateAmountChipEntityList(amountChipList: List) { - _amountChipEntityList.update { amountChipList } - } - private fun updateCreditCardPaymentOptions(billDetailsEntity: BillDetailsEntity) { val creditCardPaymentOptions = mutableListOf() @@ -875,8 +767,12 @@ constructor( } } - if (isCategoryOfTypeAmountChipsRequired(billCategoryEntity?.categoryId.orEmpty())) { - updateAmountChipEntity(amount = newAmountValue) + if ( + payBillAmountChipsHelper.isCategoryOfTypeAmountChipsRequired( + billCategoryEntity?.categoryId.orEmpty() + ) + ) { + payBillAmountChipsHelper.updateSelectedChip(amount = newAmountValue) } } @@ -1326,7 +1222,7 @@ constructor( isIPLPowerPlayThemeExperimentEnabled = isIPLPowerPlayThemeExperimentEnabled, billCategoryEntity = billCategoryEntity, - showScratchAnimation = !upiRequestId.isNullOrEmpty(), + showScratchAnimation = upiRequestId.isNotEmpty(), ), second = true, ) @@ -1550,33 +1446,40 @@ constructor( fun onCreditCardPaymentOptionSelected(creditCardPaymentOption: CreditCardPaymentOption) { if (creditCardPaymentOption.isSelected) return - creditCardPaymentOptions.indexOf(creditCardPaymentOption).let { selectedIndex -> - creditCardPaymentOptions.forEachIndexed { index, option -> - creditCardPaymentOptions[index] = option.copy(isSelected = index == selectedIndex) - } - } + updateCreditCardPaymentOptionSelection(creditCardPaymentOption) when (creditCardPaymentOption.type) { CreditCardAmountType.TOTAL -> { - updatePaymentAmount( - newAmountValue = billDetailsEntity.value?.amount.orEmpty().getNormalisedAmount() - ) + val totalAmount = billDetailsEntity.value?.amount.orEmpty().getNormalisedAmount() + updatePaymentAmount(newAmountValue = totalAmount) } + CreditCardAmountType.MINIMUM -> { - val minAmountParamEntity = - billDetailsEntity.value?.billerAdditionalParams?.find { - it.paramName == CC_MIN_DUE_AMOUNT_KEY - } - updatePaymentAmount( - newAmountValue = minAmountParamEntity?.value.orEmpty().getNormalisedAmount() - ) + val minAmountParamEntity = findMinimumDueAmountParam() + val minimumAmount = minAmountParamEntity?.value.orEmpty().getNormalisedAmount() + updatePaymentAmount(newAmountValue = minimumAmount) } + CreditCardAmountType.OTHER -> { updatePaymentAmount(newAmountValue = "") } } } + private fun updateCreditCardPaymentOptionSelection(selectedOption: CreditCardPaymentOption) { + val selectedIndex = creditCardPaymentOptions.indexOf(selectedOption) + + creditCardPaymentOptions.forEachIndexed { index, option -> + creditCardPaymentOptions[index] = option.copy(isSelected = index == selectedIndex) + } + } + + private fun findMinimumDueAmountParam(): BillerAdditionalParamsEntity? { + return billDetailsEntity.value?.billerAdditionalParams?.find { + it.paramName == CC_MIN_DUE_AMOUNT_KEY + } + } + private fun updateBottomRewardsNudgeEntity() { viewModelScope.launch(dispatcherProvider.io) { val nudgeDetailEntity = rewardsNudgeEntityFetchUseCase.execute() @@ -1631,281 +1534,10 @@ constructor( } } - private fun updateArcProtectedStatus() { - viewModelScope.launch(Dispatchers.IO) { - arcNudgeResponse = - try { - gson.fromJson( - naviCacheRepository.get(key = ARC_NUDGE_RESPONSE_CACHE_KEY)?.value, - ArcNudgeResponse::class.java, - ) - } catch (_: Exception) { - null - } - - val isArcProtected = - arcNudgeResponse?.isArcProtected.orFalse() && - arcNudgeResponse - ?.blockedBillerIds - ?.contains(billDetailsEntity.value?.billerId) == false - _isArcProtected.update { isArcProtected } - } - } - suspend fun onSavedBillCtaClicked(apiUrl: String) { naviBbpsCommonRepository.getSavedBillsDetailsOnError( path = apiUrl, metricInfo = getBbpsMetricInfo(screenName = naviBbpsVmData.screen.screenName), ) } - - fun notifyDuplicatePayment() { - suspend fun notifySuccessBottomSheet( - billId: String, - billPeriod: String, - billDate: String, - orderTimestamp: DateTime, - orderReferenceId: String, - orderStatus: OrderStatusOfView, - orderAmount: String, - billerId: String, - formattedAmount: String, - formattedDate: String, - ) { - val currentBillGenerationDate: DateTime = - DateUtils.getDateTimeObjectFromDateTimeString( - dateTime = billDate, - format = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR, - ) - - val nextBillGenerationDate = - currentBillGenerationDate.apply { - when (billPeriod) { - BillPeriod.BILL_PERIOD_DAILY.name -> plusDays(1) - BillPeriod.BILL_PERIOD_WEEKLY.name -> plusWeeks(1) - BillPeriod.BILL_PERIOD_MONTHLY.name -> plusMonths(1) - BillPeriod.BILL_PERIOD_BIMONTHLY.name -> plusMonths(2) - BillPeriod.BILL_PERIOD_QUARTERLY.name -> plusMonths(3) - BillPeriod.BILL_PERIOD_HALFYEARLY.name -> plusMonths(6) - BillPeriod.BILL_PERIOD_YEARLY.name -> plusYears(1) - } - } - - val currentBillingCycle = currentBillGenerationDate.. - val billId = billDetails.billId - val billerId = billDetails.billerId - val billDate = billDetails.billDate - val billPeriod = billDetails.billPeriod - - if (duplicatePaymentBottomSheetViewTracker.shouldShowBottomSheet(billId = billId)) { - val latestOrder = findLastOrderWithSuccessfulPaymentUseCase.find(billId) - - latestOrder?.let { - val orderReferenceId = latestOrder.orderReferenceId - val orderStatus = latestOrder.orderStatusOfView - val orderTimestamp = latestOrder.orderTimestamp - val orderAmount = latestOrder.amount - - val formattedDate = - DateUtils.getFormattedDateTimeAsStringFromDateTimeObject( - dateTime = orderTimestamp, - format = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR, - ) - val formattedAmount = SYMBOL_RUPEE + orderAmount.getDisplayableAmount() - - when (orderStatus) { - OrderStatusOfView.Debit -> { - notifySuccessBottomSheet( - billId = billId, - billerId = billerId, - billDate = billDate, - billPeriod = billPeriod, - orderReferenceId = orderReferenceId, - orderStatus = orderStatus, - orderTimestamp = orderTimestamp, - orderAmount = orderAmount, - formattedDate = formattedDate, - formattedAmount = formattedAmount, - ) - } - OrderStatusOfView.Pending -> { - notifyPendingBottomSheet( - billId = billId, - billerId = billerId, - orderReferenceId = orderReferenceId, - orderStatus = orderStatus, - orderTimestamp = orderTimestamp, - orderAmount = orderAmount, - formattedDate = formattedDate, - formattedAmount = formattedAmount, - ) - } - else -> { - // Do nothing - } - } - } - } - } - } - } } diff --git a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/BbpsArcHelper.kt b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/BbpsArcHelper.kt new file mode 100644 index 0000000000..445c585e44 --- /dev/null +++ b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/BbpsArcHelper.kt @@ -0,0 +1,53 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.bbps.feature.paybill.helper + +import com.navi.adverse.flux.provider.GsonProvider.gson +import com.navi.base.cache.repository.NaviCacheRepository +import com.navi.base.utils.orFalse +import com.navi.payments.shared.feature.arc.constant.ARC_NUDGE_RESPONSE_CACHE_KEY +import com.navi.payments.shared.feature.arc.model.network.ArcNudgeResponse +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class BbpsArcHelper @Inject constructor(private val naviCacheRepository: NaviCacheRepository) { + private val _isArcProtected = MutableStateFlow(false) + val isArcProtected: StateFlow = _isArcProtected.asStateFlow() + + suspend fun updateArcProtectedStatus(billerId: String) { + val arcNudgeResponse = fetchArcNudgeResponse() + + val isProtected = determineArcProtectionStatus(arcNudgeResponse, billerId) + + _isArcProtected.update { isProtected } + } + + private suspend fun fetchArcNudgeResponse(): ArcNudgeResponse? { + return try { + naviCacheRepository.get(key = ARC_NUDGE_RESPONSE_CACHE_KEY)?.value?.let { cacheValue -> + gson.fromJson(cacheValue, ArcNudgeResponse::class.java) + } + } catch (_: Exception) { + null + } + } + + private fun determineArcProtectionStatus( + response: ArcNudgeResponse?, + billerId: String, + ): Boolean { + // A biller is ARC protected if: + // 1. ARC protection is globally enabled + // 2. The biller is not in the blocked billers list + return response?.isArcProtected.orFalse() && + response?.blockedBillerIds?.contains(billerId) == false + } +} diff --git a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/NotifyDuplicatePaymentHandler.kt b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/NotifyDuplicatePaymentHandler.kt new file mode 100644 index 0000000000..0a571fab37 --- /dev/null +++ b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/NotifyDuplicatePaymentHandler.kt @@ -0,0 +1,303 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.bbps.feature.paybill.helper + +import com.navi.base.utils.DateUtils +import com.navi.base.utils.ResourceProvider +import com.navi.bbps.R +import com.navi.bbps.common.DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR +import com.navi.bbps.common.NaviBbpsAnalytics +import com.navi.bbps.common.SYMBOL_RUPEE +import com.navi.bbps.common.session.DuplicatePaymentBottomSheetViewTracker +import com.navi.bbps.common.session.NaviBbpsSessionHelper +import com.navi.bbps.common.usecase.FindLastOrderWithSuccessfulPaymentUseCase +import com.navi.bbps.common.utils.getDisplayableAmount +import com.navi.bbps.feature.customerinput.model.view.BillDetailsEntity +import com.navi.bbps.feature.customerinput.model.view.BillPeriod +import com.navi.bbps.feature.paybill.model.view.PayBillBottomSheetType +import com.navi.common.di.CoroutineDispatcherProvider +import com.navi.common.utils.safeLaunch +import com.navi.pay.tstore.list.model.view.OrderStatusOfView +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import org.joda.time.DateTime + +/** + * Handler responsible for detecting and notifying users about duplicate bill payments. Shows + * appropriate bottom sheets based on the payment status and handles user actions. + */ +class NotifyDuplicatePaymentHandler +@Inject +constructor( + private val duplicatePaymentBottomSheetViewTracker: DuplicatePaymentBottomSheetViewTracker, + private val resourceProvider: ResourceProvider, + private val naviBbpsSessionHelper: NaviBbpsSessionHelper, + private val dispatcherProvider: CoroutineDispatcherProvider, + private val findLastOrderWithSuccessfulPaymentUseCase: FindLastOrderWithSuccessfulPaymentUseCase, +) { + private val naviBbpsAnalytics: NaviBbpsAnalytics.PayBill = NaviBbpsAnalytics.INSTANCE.PayBill() + + private val _showDuplicatePaymentBottomSheet = MutableSharedFlow() + val showDuplicatePaymentBottomSheet = _showDuplicatePaymentBottomSheet.asSharedFlow() + + fun notifyDuplicatePayment( + coroutineScope: CoroutineScope, + source: String, + initialSource: String, + billDetailsEntity: BillDetailsEntity?, + onPayBillBottomSheetType: (PayBillBottomSheetType) -> Unit, + navigateToOrderHistoryDetailsScreen: (String) -> Unit, + navigateToBillHistoryScreen: () -> Unit, + ) { + suspend fun notifySuccessBottomSheet( + billId: String, + billPeriod: String, + billDate: String, + orderTimestamp: DateTime, + orderReferenceId: String, + orderStatus: OrderStatusOfView, + orderAmount: String, + billerId: String, + formattedAmount: String, + formattedDate: String, + ) { + val currentBillGenerationDate = + DateUtils.getDateTimeObjectFromDateTimeString( + dateTime = billDate, + format = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR, + ) + + val nextBillGenerationDate = + currentBillGenerationDate.apply { + when (billPeriod) { + BillPeriod.BILL_PERIOD_DAILY.name -> plusDays(1) + BillPeriod.BILL_PERIOD_WEEKLY.name -> plusWeeks(1) + BillPeriod.BILL_PERIOD_MONTHLY.name -> plusMonths(1) + BillPeriod.BILL_PERIOD_BIMONTHLY.name -> plusMonths(2) + BillPeriod.BILL_PERIOD_QUARTERLY.name -> plusMonths(3) + BillPeriod.BILL_PERIOD_HALFYEARLY.name -> plusMonths(6) + BillPeriod.BILL_PERIOD_YEARLY.name -> plusYears(1) + } + } + + val currentBillingCycle = currentBillGenerationDate.. + val billId = billDetails.billId + val billerId = billDetails.billerId + val billDate = billDetails.billDate + val billPeriod = billDetails.billPeriod + + if (duplicatePaymentBottomSheetViewTracker.shouldShowBottomSheet(billId = billId)) { + val latestOrder = findLastOrderWithSuccessfulPaymentUseCase.find(billId) + + latestOrder?.let { + val orderReferenceId = latestOrder.orderReferenceId + val orderStatus = latestOrder.orderStatusOfView + val orderTimestamp = latestOrder.orderTimestamp + val orderAmount = latestOrder.amount + + val formattedDate = + DateUtils.getFormattedDateTimeAsStringFromDateTimeObject( + dateTime = orderTimestamp, + format = DATE_TIME_FORMAT_DATE_MONTH_NAME_YEAR, + ) + val formattedAmount = SYMBOL_RUPEE + orderAmount.getDisplayableAmount() + + when (orderStatus) { + OrderStatusOfView.Debit -> { + notifySuccessBottomSheet( + billId = billId, + billerId = billerId, + billDate = billDate, + billPeriod = billPeriod, + orderReferenceId = orderReferenceId, + orderStatus = orderStatus, + orderTimestamp = orderTimestamp, + orderAmount = orderAmount, + formattedDate = formattedDate, + formattedAmount = formattedAmount, + ) + } + OrderStatusOfView.Pending -> { + notifyPendingBottomSheet( + billId = billId, + billerId = billerId, + orderReferenceId = orderReferenceId, + orderStatus = orderStatus, + orderTimestamp = orderTimestamp, + orderAmount = orderAmount, + formattedDate = formattedDate, + formattedAmount = formattedAmount, + ) + } + else -> { + // Do nothing for other order statuses + } + } + } + } + } + } + } +} diff --git a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/PayBillAmountChipsHelper.kt b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/PayBillAmountChipsHelper.kt new file mode 100644 index 0000000000..696f05e0a8 --- /dev/null +++ b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/helper/PayBillAmountChipsHelper.kt @@ -0,0 +1,174 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.bbps.feature.paybill.helper + +import com.navi.base.utils.ZERO_STRING +import com.navi.bbps.common.CATEGORY_ID_DTH +import com.navi.bbps.common.CATEGORY_ID_FASTAG +import com.navi.bbps.common.model.config.ChipsConfigData +import com.navi.bbps.common.utils.NaviBbpsCommonUtils.evaluateMvelExpression +import com.navi.bbps.feature.customerinput.model.view.BillerAdditionalParamsEntity +import com.navi.bbps.feature.paybill.PayBillViewModel.Companion.BILLER_MAX_ACCEPTED_AMOUNT +import com.navi.bbps.feature.paybill.PayBillViewModel.Companion.BILLER_MIN_ACCEPTED_AMOUNT +import com.navi.bbps.feature.paybill.model.network.PaymentAmountExactness +import com.navi.bbps.feature.paybill.model.view.AmountChipEntity +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +/** + * Helper class that manages amount chips for bill payments Handles chip calculation, selection, and + * state management + */ +class PayBillAmountChipsHelper @Inject constructor() { + private val _amountChipEntityList = MutableStateFlow>(emptyList()) + val amountChipEntityList: StateFlow> = + _amountChipEntityList.asStateFlow() + + private val _availableAmounts = MutableStateFlow(emptyList()) + + suspend fun updateAmountChips( + chipsConfigData: ChipsConfigData, + paymentAmountExactness: PaymentAmountExactness, + initialPaymentAmount: String, + paymentAmount: String, + billerAdditionalParams: List, + onUpdateShouldAutoFocusOnAmount: (String) -> Unit, + onUpdatePaymentAmount: (String) -> Unit, + ): Boolean { + val bounds = + calculateAmountBounds( + chipsConfigData, + paymentAmountExactness, + initialPaymentAmount, + billerAdditionalParams, + ) + + if (!validateBounds(bounds, chipsConfigData.minimumRequiredDifference)) { + onUpdateShouldAutoFocusOnAmount(paymentAmount) + return false + } + + val secondAmount = + calculateChipAmountFromFormula( + bounds.first, + bounds.second, + chipsConfigData.secondChipFormulaExpression, + ) + if (secondAmount == ZERO_STRING) { + onUpdateShouldAutoFocusOnAmount(paymentAmount) + return false + } + + val thirdAmount = + calculateChipAmountFromFormula( + bounds.first, + bounds.second, + chipsConfigData.thirdChipFormulaExpression, + ) + if (thirdAmount == ZERO_STRING) { + onUpdateShouldAutoFocusOnAmount(paymentAmount) + return false + } + + _availableAmounts.update { + listOf(bounds.first.toString(), secondAmount, thirdAmount, bounds.second.toString()) + } + transformToChipEntities() + + if (shouldAutoSelectFirstChip(initialPaymentAmount)) { + val firstAmount = amountChipEntityList.value.firstOrNull()?.amount ?: return false + onUpdatePaymentAmount(firstAmount) + updateSelectedChip(firstAmount) + } + + return true + } + + fun updateSelectedChip(amount: String) { + _amountChipEntityList.update { currentList -> + currentList.map { chip -> chip.copy(isSelected = chip.amount == amount) } + } + } + + private fun calculateAmountBounds( + chipsConfigData: ChipsConfigData, + paymentAmountExactness: PaymentAmountExactness, + initialPaymentAmount: String, + billerAdditionalParams: List, + ): Pair { + var minAmount = chipsConfigData.genericMinAmount + var maxAmount = chipsConfigData.genericMaxAmount + + when (paymentAmountExactness) { + PaymentAmountExactness.EXACT_AND_ABOVE -> { + initialPaymentAmount.toDoubleOrNull()?.let { amount -> + minAmount = minAmount.coerceAtLeast(amount.toInt()) + } + } + PaymentAmountExactness.EXACT_AND_BELOW -> { + initialPaymentAmount.toDoubleOrNull()?.let { amount -> + maxAmount = maxAmount.coerceAtMost(amount.toInt()) + } + } + else -> { + // No adjustment needed + } + } + + billerAdditionalParams + .find { it.paramName == BILLER_MIN_ACCEPTED_AMOUNT } + ?.value + ?.toIntOrNull() + ?.let { billerMin -> minAmount = minAmount.coerceAtLeast(billerMin) } + + billerAdditionalParams + .find { it.paramName == BILLER_MAX_ACCEPTED_AMOUNT } + ?.value + ?.toIntOrNull() + ?.let { billerMax -> maxAmount = maxAmount.coerceAtMost(billerMax) } + + return minAmount to maxAmount + } + + private fun validateBounds(bounds: Pair, minimumDifference: Int): Boolean { + return bounds.second - bounds.first >= minimumDifference + } + + private suspend fun calculateChipAmountFromFormula( + minAmount: Int, + maxAmount: Int, + formula: String, + ): String { + return evaluateMvelExpression( + key = formula, + data = mapOf("minChipAmount" to minAmount, "maxChipAmount" to maxAmount), + defaultValue = ZERO_STRING, + ) + } + + private fun shouldAutoSelectFirstChip(initialPaymentAmount: String): Boolean { + return initialPaymentAmount == ZERO_STRING || + amountChipEntityList.value.firstOrNull()?.let { initialPaymentAmount == it.amount } + ?: false + } + + private fun transformToChipEntities() { + _amountChipEntityList.update { + _availableAmounts.value.map { amount -> + AmountChipEntity(amount = amount, isSelected = false) + } + } + } + + fun isCategoryOfTypeAmountChipsRequired(categoryId: String): Boolean { + return categoryId in listOf(CATEGORY_ID_DTH, CATEGORY_ID_FASTAG) + } +} diff --git a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/ui/PayBillScreen.kt b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/ui/PayBillScreen.kt index 83a2ea52aa..dd0062ffd1 100644 --- a/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/ui/PayBillScreen.kt +++ b/android/navi-bbps/src/main/kotlin/com/navi/bbps/feature/paybill/ui/PayBillScreen.kt @@ -298,7 +298,8 @@ fun PayBillScreen( val creditCardPaymentOptions = payBillViewModel.creditCardPaymentOptions val payBillBottomSheetType by payBillViewModel.payBillBottomSheetType.collectAsStateWithLifecycle() - val amountChipEntityList by payBillViewModel.amountChipEntityList.collectAsStateWithLifecycle() + val amountChipEntityList by + payBillViewModel.payBillAmountChipsHelper.amountChipEntityList.collectAsStateWithLifecycle() val offerDataList by payBillViewModel.offerDataList.collectAsStateWithLifecycle() val coinBurnData by payBillViewModel.coinBurnData.collectAsStateWithLifecycle() val amountAfterCoinDiscount by @@ -311,7 +312,8 @@ fun PayBillScreen( payBillViewModel.naviBbpsDefaultConfig.collectAsStateWithLifecycle() val isConsentViewVisible by payBillViewModel.isConsentViewVisible.collectAsStateWithLifecycle() val isConsentProvided by payBillViewModel.isConsentProvided.collectAsStateWithLifecycle() - val isArcProtected by payBillViewModel.isArcProtected.collectAsStateWithLifecycle() + val isArcProtected by + payBillViewModel.bbpsArcHelper.isArcProtected.collectAsStateWithLifecycle() val sortedOfferList by remember(offerDataList, paymentAmount) { @@ -576,14 +578,14 @@ fun PayBillScreen( } LaunchedEffect(Unit) { - payBillViewModel.showDuplicatePaymentBottomSheet.collectLatest { - showDuplicatePaymentBottomSheet -> - if (showDuplicatePaymentBottomSheet) { - openSheet() - } else { - closeSheet() + payBillViewModel.notifyDuplicatePaymentHandler.showDuplicatePaymentBottomSheet + .collectLatest { showDuplicatePaymentBottomSheet -> + if (showDuplicatePaymentBottomSheet) { + openSheet() + } else { + closeSheet() + } } - } } LaunchedEffect(bottomSheetState.isVisible) {