NTP-24858 | Frequent contact logic changes (#15016)

This commit is contained in:
Hardik Chaudhary
2025-03-07 19:53:13 +05:30
committed by GitHub
parent bfd831d324
commit 35869f006d
8 changed files with 163 additions and 125 deletions

View File

@@ -80,7 +80,6 @@ data class DefaultConfigContent(
@SerializedName("frequentTransactionMaxColumns") val frequentTransactionsMaxColumns: Int = 4,
@SerializedName("frequentTransactionTotalEntries")
val frequentTransactionsTotalEntries: Int = 12,
@SerializedName("frequentTransactionDays") val frequentTransactionsDays: Int = 180,
@SerializedName("frequentTransactionHeading")
val frequentTransactionHeading: String = "Pay again",
@SerializedName("upiAppLogoS3BaseUrl")

View File

@@ -7,48 +7,109 @@
package com.navi.pay.common.utils
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.repository.NaviCacheRepository
import com.navi.pay.management.common.sendmoney.model.network.TransactionInitiationType
import com.navi.pay.network.di.NaviPayGsonBuilder
import com.navi.pay.tstore.list.model.view.FrequentOrderEntity
import com.navi.pay.tstore.list.model.view.OrderEntity
import com.navi.pay.tstore.list.model.view.OrderStatusOfView
import com.navi.pay.tstore.list.model.view.toFrequentOrderEntityList
import com.navi.pay.tstore.list.repository.OrderRepository
import com.navi.pay.utils.NAVI_PAY_FREQUENT_ORDER_CACHE_KEY
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* Fetching all orders based on configurable number of days
*
* Filtering all successful outgoing pay to contact orders
*
* Grouping filtered orders based on vpa, sorting on basis of count and recency
*/
class FrequentOrdersHelper @Inject constructor(private val orderRepository: OrderRepository) {
class FrequentOrdersHelper
@Inject
constructor(
private val orderRepository: OrderRepository,
private val naviCacheRepository: NaviCacheRepository,
@NaviPayGsonBuilder private val gson: Gson,
) {
suspend fun getFrequentOrderList(
frequentOrdersTotalEntries: Int,
frequentOrdersDays: Int,
): List<OrderEntity> {
val latestOrderListFromDB =
orderRepository.findAllOrderEntityOfLast(totalDays = frequentOrdersDays)
suspend fun getFrequentOrderList(frequentOrdersTotalEntries: Int): List<FrequentOrderEntity> {
val latestCachedFrequentOrderListFromDB =
naviCacheRepository.get(key = NAVI_PAY_FREQUENT_ORDER_CACHE_KEY)?.value?.let {
gson.fromJson<List<FrequentOrderEntity>>(
it,
object : TypeToken<List<FrequentOrderEntity>>() {}.type,
)
} ?: emptyList()
if (latestOrderListFromDB.isEmpty()) return emptyList()
val filteredOrderList = filteredOrderEntityList(orderEntityList = latestOrderListFromDB)
val orderListGroupedByVpa = filteredOrderList.groupBy { it.orderDescription }
return orderListGroupedByVpa.entries
.sortedWith(
compareByDescending<Map.Entry<String, List<OrderEntity>>> { it.value.size }
.thenByDescending { it.value[0].orderTimestamp }
)
.take(frequentOrdersTotalEntries)
.map { it.value[0] }
CoroutineScope(Dispatchers.IO).launch {
processAndRefreshFrequentOrderCache(frequentOrdersTotalEntries)
}
return latestCachedFrequentOrderListFromDB
}
private fun filteredOrderEntityList(orderEntityList: List<OrderEntity>): List<OrderEntity> {
private suspend fun processAndRefreshFrequentOrderCache(frequentOrdersTotalEntries: Int) {
val aggregatedOrdersGroupedByVpa = fetchAndAggregateOrders(frequentOrdersTotalEntries)
val finalFrequentEntityList =
aggregatedOrdersGroupedByVpa
.getTopFrequentEntries(frequentOrdersTotalEntries)
.map { it.value.first() }
.toFrequentOrderEntityList()
naviCacheRepository.save(
NaviCacheEntity(
key = NAVI_PAY_FREQUENT_ORDER_CACHE_KEY,
value = gson.toJson(finalFrequentEntityList),
version = 1,
)
)
}
private suspend fun fetchAndAggregateOrders(
frequentOrdersTotalEntries: Int
): Map<String, MutableList<OrderEntity>> {
var offset = 0
val limit = 500
val aggregatedOrders = mutableMapOf<String, MutableList<OrderEntity>>()
do {
val ordersByOffset =
orderRepository.getOrderEntitiesByOffset(limit = limit, offset = offset)
if (ordersByOffset.isNotEmpty()) {
ordersByOffset
.filteredOrderEntityList()
.groupBy { it.orderDescription }
.getTopFrequentEntries(frequentOrdersTotalEntries)
.forEach { (vpa, orders) ->
aggregatedOrders.getOrPut(vpa) { mutableListOf() }.addAll(orders)
}
}
offset += limit
} while (ordersByOffset.isNotEmpty())
return aggregatedOrders
}
private fun Map<String, List<OrderEntity>>.getTopFrequentEntries(
frequentOrdersTotalEntries: Int
): List<Map.Entry<String, List<OrderEntity>>> {
return this.entries
.sortedWith(
compareByDescending<Map.Entry<String, List<OrderEntity>>> { it.value.size }
.thenByDescending { it.value.first().orderTimestamp }
)
.take(frequentOrdersTotalEntries)
}
private fun List<OrderEntity>.filteredOrderEntityList(): List<OrderEntity> {
val acceptedPaymentMode = listOf(TransactionInitiationType.PAY_TO_CONTACT.name)
val successfulOrderList =
orderEntityList.filter { it.orderStatusOfView == OrderStatusOfView.Debit }
val successfulOrderList = this.filter { it.orderStatusOfView == OrderStatusOfView.Debit }
val payToContactOrderList =
successfulOrderList.filter {

View File

@@ -101,7 +101,7 @@ import com.navi.pay.management.paytocontacts.viewmodel.PayToContactsUIState
import com.navi.pay.management.paytocontacts.viewmodel.PayToContactsViewModel
import com.navi.pay.permission.utils.PermissionKeys
import com.navi.pay.permission.utils.PermissionUtils
import com.navi.pay.tstore.list.model.view.OrderEntity
import com.navi.pay.tstore.list.model.view.FrequentOrderEntity
import com.navi.pay.utils.NAVI_PAY_LOADER
import com.navi.pay.utils.NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE
import com.navi.pay.utils.contactInitials
@@ -259,10 +259,10 @@ fun PayToContactsScreen(
payToContactsViewModel.initiatePaymentForNewContact()
}
val onFrequentOrderSelected: (OrderEntity) -> Unit = {
val onFrequentOrderSelected: (FrequentOrderEntity) -> Unit = {
keyboardController?.customHide(context = context, view = view)
focusManager.clearFocus()
payToContactsViewModel.initiatePaymentToFrequentOrder(orderEntity = it)
payToContactsViewModel.initiatePaymentToFrequentOrder(frequentOrderEntity = it)
naviPayAnalytics.onContactSelected(
source = NaviPayScreenType.NAVI_PAY_TO_CONTACTS.name,
inContactList = false,
@@ -364,10 +364,10 @@ fun RenderPayToContactsScreen(
onContactSelected: (PhoneContactEntity) -> Unit,
onNewContactSelected: (PhoneContactEntity) -> Unit,
onBackClick: () -> Unit,
frequentOrders: List<OrderEntity>,
frequentOrders: List<FrequentOrderEntity>,
frequentOrderMaxColumns: Int,
frequentOrdersHeading: String,
onFrequentOrderSelected: (OrderEntity) -> Unit,
onFrequentOrderSelected: (FrequentOrderEntity) -> Unit,
areAllPermissionsGranted: Boolean,
onPrimaryButtonClicked: () -> Unit,
hasNonZeroContacts: Boolean,
@@ -532,12 +532,12 @@ private fun PayToContactsScreenHeader(
private fun PayToContactScreenScaffoldContent(
modifier: Modifier,
searchQuery: String,
frequentOrders: List<OrderEntity>,
frequentOrders: List<FrequentOrderEntity>,
isNewContactVisible: Boolean,
contactList: List<PhoneContactEntity>,
onContactSelected: (PhoneContactEntity) -> Unit,
onNewContactSelected: (PhoneContactEntity) -> Unit,
onFrequentOrderSelected: (OrderEntity) -> Unit,
onFrequentOrderSelected: (FrequentOrderEntity) -> Unit,
frequentOrderMaxColumns: Int,
frequentOrdersHeading: String,
areAllPermissionsGranted: Boolean,
@@ -645,8 +645,8 @@ private fun PayToContactScreenScaffoldContent(
@Composable
private fun FrequentOrdersSection(
searchQuery: String,
frequentOrders: List<OrderEntity>,
onFrequentOrderSelected: (OrderEntity) -> Unit,
frequentOrders: List<FrequentOrderEntity>,
onFrequentOrderSelected: (FrequentOrderEntity) -> Unit,
frequentOrderMaxColumns: Int,
frequentOrdersHeading: String,
isSearchState: Boolean,
@@ -759,8 +759,8 @@ private fun PhoneContactView(
@Composable
fun FrequentOrdersSearchView(
index: Int,
frequentOrderEntity: OrderEntity,
onFrequentOrderSelected: (OrderEntity) -> Unit,
frequentOrderEntity: FrequentOrderEntity,
onFrequentOrderSelected: (FrequentOrderEntity) -> Unit,
showLoader: Boolean,
clickedContactPhoneNumber: String,
) {
@@ -776,25 +776,17 @@ fun FrequentOrdersSearchView(
) {
ContactIconView(
index = index,
contactInitials =
frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo
?.name
.orEmpty()
.contactInitials(),
contactInitials = frequentOrderEntity.payeeInfo?.name.orEmpty().contactInitials(),
phoneNumber =
getNormalisedPhoneNumber(
phoneNumber =
frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.mobNo
),
vpa = frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.vpa.orEmpty(),
getNormalisedPhoneNumber(phoneNumber = frequentOrderEntity.payeeInfo?.mobNo),
vpa = frequentOrderEntity.payeeInfo?.vpa.orEmpty(),
)
Spacer(modifier = Modifier.width(16.dp))
Column {
NaviText(
text =
frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.name.orEmpty(),
text = frequentOrderEntity.payeeInfo?.name.orEmpty(),
fontSize = 14.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
@@ -814,11 +806,7 @@ fun FrequentOrdersSearchView(
Spacer(modifier = Modifier.weight(1f))
Column(modifier = Modifier.width(24.dp)) {
if (
clickedContactPhoneNumber ==
frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.vpa &&
showLoader
) {
if (clickedContactPhoneNumber == frequentOrderEntity.payeeInfo?.vpa && showLoader) {
NaviPayLottieAnimation(
lottieFileName = NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE,
modifier = Modifier,
@@ -831,8 +819,8 @@ fun FrequentOrdersSearchView(
@Composable
fun FrequentOrdersItemView(
index: Int,
frequentOrderEntity: OrderEntity,
onFrequentOrderSelected: (OrderEntity) -> Unit,
frequentOrderEntity: FrequentOrderEntity,
onFrequentOrderSelected: (FrequentOrderEntity) -> Unit,
showLoader: Boolean,
clickedContactPhoneNumber: String,
) {
@@ -848,25 +836,15 @@ fun FrequentOrdersItemView(
) {
ContactIconView(
index = index,
contactInitials =
frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo
?.name
.orEmpty()
.contactInitials(),
contactInitials = frequentOrderEntity.payeeInfo?.name.orEmpty().contactInitials(),
phoneNumber =
getNormalisedPhoneNumber(
phoneNumber =
frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.mobNo
),
vpa = frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.vpa.orEmpty(),
getNormalisedPhoneNumber(phoneNumber = frequentOrderEntity.payeeInfo?.mobNo),
vpa = frequentOrderEntity.payeeInfo?.vpa.orEmpty(),
)
Spacer(modifier = Modifier.height(8.dp))
if (
clickedContactPhoneNumber ==
frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.vpa && showLoader
) {
if (clickedContactPhoneNumber == frequentOrderEntity.payeeInfo?.vpa && showLoader) {
NaviPayLottieAnimation(
modifier = Modifier.size(24.dp),
lottieFileName = NAVI_PAY_PURPLE_CTA_LOADER_LOTTIE,
@@ -874,8 +852,7 @@ fun FrequentOrdersItemView(
} else {
NaviText(
modifier = Modifier.padding(bottom = 8.dp),
text =
frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.name.orEmpty(),
text = frequentOrderEntity.payeeInfo?.name.orEmpty(),
fontSize = 12.sp,
fontFamily = naviFontFamily,
fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR),
@@ -889,16 +866,9 @@ fun FrequentOrdersItemView(
}
}
private fun frequentOrderItemDetail(frequentOrderEntity: OrderEntity) =
if (
frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo
?.mobNo
.orEmpty()
.isNotEmpty()
) {
getNormalisedPhoneNumber(
phoneNumber = frequentOrderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.mobNo
)
private fun frequentOrderItemDetail(frequentOrderEntity: FrequentOrderEntity) =
if (frequentOrderEntity.payeeInfo?.mobNo.orEmpty().isNotEmpty()) {
getNormalisedPhoneNumber(phoneNumber = frequentOrderEntity.payeeInfo?.mobNo)
} else {
frequentOrderEntity.orderDescription
}

View File

@@ -43,7 +43,7 @@ import com.navi.pay.management.paytocontacts.PhoneContactManager
import com.navi.pay.management.paytocontacts.model.network.PayToContactRequest
import com.navi.pay.management.paytocontacts.model.view.PayToContactsBottomSheetStateHolder
import com.navi.pay.management.paytocontacts.model.view.PhoneContactEntity
import com.navi.pay.tstore.list.model.view.OrderEntity
import com.navi.pay.tstore.list.model.view.FrequentOrderEntity
import com.navi.pay.utils.DEFAULT_CONFIG
import com.navi.pay.utils.INDIA_COUNTRY_CODE_WITH_PLUS
import com.navi.pay.utils.INVALID_VPA
@@ -122,7 +122,7 @@ constructor(
private val _activeQueryContactNumber = MutableStateFlow("")
val activeQueryContactNumber = _activeQueryContactNumber.asStateFlow()
private val frequentOrders = MutableStateFlow<List<OrderEntity>>(emptyList())
private val frequentOrders = MutableStateFlow<List<FrequentOrderEntity>>(emptyList())
private val _navigateToNextScreen = MutableSharedFlow<Direction>()
val navigateToNextScreen = _navigateToNextScreen.asSharedFlow()
@@ -155,9 +155,6 @@ constructor(
private val frequentOrdersTotalEntries =
MutableStateFlow(naviPayDefaultConfig.config.frequentTransactionsTotalEntries)
private val frequentOrdersDays =
MutableStateFlow(naviPayDefaultConfig.config.frequentTransactionsDays)
private val _frequentOrdersHeading =
MutableStateFlow(naviPayDefaultConfig.config.frequentTransactionHeading)
val frequentOrdersHeading = _frequentOrdersHeading.asStateFlow()
@@ -201,9 +198,7 @@ constructor(
combine(searchQuery, frequentOrders) { searchQuery, frequentOrders ->
val filteredFrequentOrder =
frequentOrders.filter { frequentOrder ->
val payeeInfo =
frequentOrder.naviPayTransactionDetailsMetadata.payeeInfo
?: return@filter false
val payeeInfo = frequentOrder.payeeInfo ?: return@filter false
payeeInfo.name.orEmpty().contains(searchQuery, ignoreCase = true) ||
phoneNumberContainSearchQuery(
phoneNumber = payeeInfo.mobNo.orEmpty(),
@@ -324,7 +319,6 @@ constructor(
updateFrequentOrderTotalEntries(
naviPayDefaultConfig.config.frequentTransactionsTotalEntries
)
updateFrequentOrderDays(naviPayDefaultConfig.config.frequentTransactionsDays)
updateFrequentOrderHeading(naviPayDefaultConfig.config.frequentTransactionHeading)
}
@@ -340,10 +334,6 @@ constructor(
frequentOrdersTotalEntries.update { totalEntries }
}
private fun updateFrequentOrderDays(numberOfDays: Int) {
frequentOrdersDays.update { numberOfDays }
}
private fun updateFrequentOrderHeading(heading: String) {
_frequentOrdersHeading.update { heading }
}
@@ -434,7 +424,7 @@ constructor(
}
}
fun initiatePaymentToFrequentOrder(orderEntity: OrderEntity) {
fun initiatePaymentToFrequentOrder(frequentOrderEntity: FrequentOrderEntity) {
paymentJob =
viewModelScope.launch(Dispatchers.IO) {
if (!naviPayNetworkConnectivity.isInternetConnected()) {
@@ -445,8 +435,7 @@ constructor(
updateShowLoader(showLoader = true)
updateActiveQueryContactNumber(
phoneNumber =
orderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.vpa.orEmpty()
phoneNumber = frequentOrderEntity.payeeInfo?.vpa.orEmpty()
)
val response =
@@ -454,31 +443,23 @@ constructor(
request =
ValidateVpaRequest(
deviceData = deviceInfoProvider.getDeviceData(),
vpa =
orderEntity.naviPayTransactionDetailsMetadata.payeeInfo
?.vpa
.orEmpty(),
vpa = frequentOrderEntity.payeeInfo?.vpa.orEmpty(),
merchantCustomerId = deviceInfoProvider.getMerchantCustomerId(),
),
screenName = screenName,
)
val payeeVpaToDisplay = orderEntity.orderDescription
val payeeVpaToDisplay = frequentOrderEntity.orderDescription
if (response.isSuccessWithData()) {
handleAPIResponseSuccessAndUpdateNextDestination(
response = response,
phoneNumber =
orderEntity.naviPayTransactionDetailsMetadata.payeeInfo
?.mobNo
.orEmpty(),
phoneNumber = frequentOrderEntity.payeeInfo?.mobNo.orEmpty(),
payeeVpaToDisplay = payeeVpaToDisplay,
payeeName =
orderEntity.naviPayTransactionDetailsMetadata.payeeInfo?.name.orEmpty(),
payeeName = frequentOrderEntity.payeeInfo?.name.orEmpty(),
transactionInitiationType =
getTransactionInitiationType(
transactionInitiationType =
orderEntity.naviPayTransactionDetailsMetadata.paymentMode
transactionInitiationType = frequentOrderEntity.paymentMode
),
)
} else {
@@ -607,10 +588,10 @@ constructor(
private suspend fun fetchFrequentOrders() {
val getFrequentOrderList =
frequentOrdersHelper.getFrequentOrderList(
frequentOrdersTotalEntries = frequentOrdersTotalEntries.value,
frequentOrdersDays = frequentOrdersDays.value,
frequentOrdersTotalEntries = frequentOrdersTotalEntries.value
)
updateFrequentOrderEntityList(getFrequentOrderList)
naviPayAnalytics.onSendToContactsLoaded(
naviPaySessionAttributes = getNaviPaySessionAttributes(),
isPermissionGranted = true,
@@ -628,8 +609,8 @@ constructor(
}
}
private fun updateFrequentOrderEntityList(orderEntityList: List<OrderEntity>) {
frequentOrders.update { orderEntityList }
private fun updateFrequentOrderEntityList(frequentOrderEntityList: List<FrequentOrderEntity>) {
frequentOrders.update { frequentOrderEntityList }
}
private fun updateShowLoader(showLoader: Boolean) {

View File

@@ -84,12 +84,9 @@ interface OrderDao {
suspend fun deleteAll()
@Query(
"SELECT * " +
"FROM $NAVI_PAY_DATABASE_T_STORE_ORDER_HISTORY_TABLE_NAME " +
" WHERE orderTimestamp >= :dateTime " +
"ORDER BY orderTimestamp DESC"
"SELECT * FROM $NAVI_PAY_DATABASE_T_STORE_ORDER_HISTORY_TABLE_NAME ORDER BY orderTimestamp DESC LIMIT :limit OFFSET :offset"
)
suspend fun findAllOrderEntityByOrderTimeStampGreaterThan(dateTime: DateTime): List<OrderEntity>
suspend fun getOrderEntitiesByOffset(limit: Int, offset: Int): List<OrderEntity>
@Query(
"SELECT " +

View File

@@ -0,0 +1,32 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.pay.tstore.list.model.view
import com.navi.pay.tstore.details.ui.upi.UserTxnInfo
import com.navi.pay.utils.parallelMap
import org.joda.time.DateTime
data class FrequentOrderEntity(
val orderTitle: String,
val orderDescription: String,
val orderTimestamp: DateTime,
val payeeInfo: UserTxnInfo?,
val paymentMode: String?,
)
suspend fun List<OrderEntity>.toFrequentOrderEntityList(): List<FrequentOrderEntity> {
return this.parallelMap { orderEntity ->
FrequentOrderEntity(
orderTitle = orderEntity.orderTitle,
orderDescription = orderEntity.orderDescription,
orderTimestamp = orderEntity.orderTimestamp,
payeeInfo = orderEntity.naviPayTransactionDetailsMetadata.payeeInfo,
paymentMode = orderEntity.naviPayTransactionDetailsMetadata.paymentMode,
)
}
}

View File

@@ -282,11 +282,8 @@ constructor(
return orderDao.getOldestOrderTimestamp()
}
suspend fun findAllOrderEntityOfLast(totalDays: Int): List<OrderEntity> {
val currentDateTime = DateTime.now().withZone(DateTimeZone.UTC)
val olderDateTime = currentDateTime.minusDays(totalDays)
return orderDao.findAllOrderEntityByOrderTimeStampGreaterThan(dateTime = olderDateTime)
suspend fun getOrderEntitiesByOffset(limit: Int, offset: Int): List<OrderEntity> {
return orderDao.getOrderEntitiesByOffset(limit = limit, offset = offset)
}
private suspend fun insertOrderEntityListDataIntoVpaTransactionInsightsDb(

View File

@@ -147,6 +147,7 @@ const val NAVI_PAY_SIM_PROVIDER_CACHE_KEY = "naviPaySimProviderKey"
const val NAVI_PAY_PACKAGE_NAME_CACHE_KEY = "naviPayPackageNameKey"
const val NAVI_PAY_SSID_CACHE_KEY = "naviPaySsidKey"
const val NAVI_PAY_DEVICE_ID_CACHE_KEY = "naviPayDeviceIdKey"
const val NAVI_PAY_FREQUENT_ORDER_CACHE_KEY = "naviPayFrequentOrderCacheKey"
// Sync DB keys
const val NAVI_PAY_SYNC_TABLE_TRANSACTION_HISTORY_KEY = "transactionHistoryKey"