Coverage Summary for Class: InAppProcessingManagerImpl (cloud.mindbox.mobile_sdk.inapp.domain)

Class Method, % Branch, % Line, % Instruction, %
InAppProcessingManagerImpl 100% (6/6) 67.2% (43/64) 90.9% (90/99) 81.6% (502/615)
InAppProcessingManagerImpl$chooseInAppToShow$1
InAppProcessingManagerImpl$chooseInAppToShow$2 100% (1/1) 100% (8/8) 100% (96/96)
InAppProcessingManagerImpl$chooseInAppToShow$2$1$1 100% (1/1) 100% (4/4) 100% (3/3) 100% (21/21)
InAppProcessingManagerImpl$chooseInAppToShow$2$imageJob$1 100% (1/1) 16.7% (1/6) 42.9% (6/14) 72.3% (73/101)
InAppProcessingManagerImpl$chooseInAppToShow$2$targetingJob$1 100% (1/1) 37.5% (6/16) 42.9% (12/28) 68.8% (110/160)
InAppProcessingManagerImpl$Companion
InAppProcessingManagerImpl$sendTargetedInApp$1
InAppProcessingManagerImpl$TargetingDataWrapper 100% (1/1) 100% (2/2) 100% (12/12)
Total 100% (11/11) 60% (54/90) 78.6% (121/154) 81% (814/1005)


 package cloud.mindbox.mobile_sdk.inapp.domain
 
 import cloud.mindbox.mobile_sdk.Mindbox.logI
 import cloud.mindbox.mobile_sdk.getErrorResponseBodyData
 import cloud.mindbox.mobile_sdk.getImageUrl
 import cloud.mindbox.mobile_sdk.inapp.domain.extensions.asVolleyError
 import cloud.mindbox.mobile_sdk.inapp.domain.extensions.getProductFromTargetingData
 import cloud.mindbox.mobile_sdk.inapp.domain.extensions.getVolleyErrorDetails
 import cloud.mindbox.mobile_sdk.inapp.domain.extensions.shouldTrackImageDownloadError
 import cloud.mindbox.mobile_sdk.inapp.domain.extensions.shouldTrackTargetingError
 import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.InAppContentFetcher
 import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.InAppFailureTracker
 import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.InAppProcessingManager
 import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.InAppGeoRepository
 import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.InAppRepository
 import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.InAppSegmentationRepository
 import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.InAppTargetingErrorRepository
 import cloud.mindbox.mobile_sdk.inapp.domain.models.*
 import cloud.mindbox.mobile_sdk.logger.*
 import cloud.mindbox.mobile_sdk.models.InAppEventType
 import cloud.mindbox.mobile_sdk.models.operation.request.FailureReason
 import kotlinx.coroutines.*
 
 internal class InAppProcessingManagerImpl(
     private val inAppGeoRepository: InAppGeoRepository,
     private val inAppSegmentationRepository: InAppSegmentationRepository,
     private val inAppTargetingErrorRepository: InAppTargetingErrorRepository,
     private val inAppContentFetcher: InAppContentFetcher,
     private val inAppRepository: InAppRepository,
     private val inAppFailureTracker: InAppFailureTracker
 ) : InAppProcessingManager {
 
     companion object {
         private const val RESPONSE_STATUS_CUSTOMER_SEGMENTS_REQUIRE_CUSTOMER =
             "CheckCustomerSegments requires customer"
     }
 
     override suspend fun chooseInAppToShow(
         inApps: List<InApp>,
         triggerEvent: InAppEventType,
     ): InApp? {
         for (inApp in inApps) {
             val data = getTargetingData(triggerEvent)
             var isTargetingErrorOccurred = false
             var isInAppContentFetched: Boolean? = null
             var targetingCheck = false
             var imageFetchError: Throwable? = null
             withContext(Dispatchers.IO) {
                 val imageJob =
                     launch(start = CoroutineStart.LAZY) {
                         runCatching {
                             isInAppContentFetched =
                                 inAppContentFetcher.fetchContent(
                                     inApp.id,
                                     inApp.form.variants.first()
                                 )
                         }.onFailure { throwable ->
                             when (throwable) {
                                 is CancellationException -> {
                                     inAppContentFetcher.cancelFetching(inApp.id)
                                     isInAppContentFetched = null
                                 }
 
                                 is InAppContentFetchingError -> {
                                     isInAppContentFetched = false
                                     imageFetchError = throwable
                                 }
                             }
                         }
                     }
                 val targetingJob = launch(start = CoroutineStart.LAZY) {
                     runCatching {
                         inApp.targeting.fetchTargetingInfo(data)
                         targetingCheck = inApp.targeting.checkTargeting(data)
                     }.onFailure { throwable ->
                         when (throwable) {
                             is GeoError -> {
                                 isTargetingErrorOccurred = true
                                 inAppGeoRepository.setGeoStatus(GeoFetchStatus.GEO_FETCH_ERROR)
                                 if (throwable.shouldTrackTargetingError()) {
                                     inAppTargetingErrorRepository.saveError(
                                         key = TargetingErrorKey.Geo,
                                         error = throwable
                                     )
                                 }
                                 MindboxLoggerImpl.e(this, "Error fetching geo", throwable)
                             }
 
                             is CustomerSegmentationError -> {
                                 isTargetingErrorOccurred = true
                                 inAppSegmentationRepository.setCustomerSegmentationStatus(
                                     CustomerSegmentationFetchStatus.SEGMENTATION_FETCH_ERROR
                                 )
                                 if (throwable.shouldTrackTargetingError()) {
                                     inAppTargetingErrorRepository.saveError(
                                         key = TargetingErrorKey.CustomerSegmentation,
                                         error = throwable
                                     )
                                 }
                                 handleCustomerSegmentationErrorLog(throwable)
                             }
 
                             else -> {
                                 MindboxLoggerImpl.e(this, throwable.message ?: "", throwable)
                                 inAppFailureTracker.sendFailure(
                                     inAppId = inApp.id,
                                     failureReason = FailureReason.UNKNOWN_ERROR,
                                     errorDetails = "Unknown exception when checking target ${throwable.message}. ${throwable.cause?.getVolleyErrorDetails() ?: "volleyError=null"}"
                                 )
                                 throw throwable
                             }
                         }
                     }
                 }
                 listOf(imageJob, targetingJob.apply {
                     invokeOnCompletion {
                         if (imageJob.isActive && !targetingCheck) {
                             imageJob.cancel()
                             mindboxLogD("Cancelling content loading since targeting is $targetingCheck")
                         }
                     }
                 }).onEach {
                     it.start()
                 }.joinAll()
             }
             mindboxLogD("loading and targeting fetching finished")
             if (isTargetingErrorOccurred) return chooseInAppToShow(inApps, triggerEvent)
             trackTargetingErrorIfAny(inApp, data)
             if (isInAppContentFetched == false && targetingCheck) {
                 imageFetchError?.takeIf { it.shouldTrackImageDownloadError() }?.let { error ->
                     inAppFailureTracker.collectFailure(
                         inAppId = inApp.id,
                         failureReason = FailureReason.IMAGE_DOWNLOAD_FAILED,
                         errorDetails = (error.message ?: "Image loading error") + "\n Url is ${inApp.form.variants.first().getImageUrl()}"
                     )
                 }
             }
             if (isInAppContentFetched == false) {
                 mindboxLogD("Skipping inApp with id = ${inApp.id} due to content fetching error.")
                 continue
             }
             mindboxLogD("Check ${inApp.targeting.type}: $targetingCheck")
             if (!targetingCheck) {
                 mindboxLogD("Skipping inApp with id = ${inApp.id} due to targeting is false")
             }
             if (targetingCheck) {
                 sendTargetedInApp(inApp, triggerEvent)
                 inAppRepository.saveTargetedInAppWithEvent(
                     inAppId = inApp.id,
                     triggerEvent.hashCode()
                 )
                 inAppFailureTracker.clearFailures()
                 return inApp
             }
         }
         inAppFailureTracker.sendCollectedFailures()
         return null
     }
 
     override suspend fun sendTargetedInApp(inApp: InApp, triggerEvent: InAppEventType) {
         var isTargetingErrorOccurred = false
         val data = getTargetingData(triggerEvent)
         runCatching {
             inApp.targeting.fetchTargetingInfo(data)
         }.onFailure { throwable ->
             when (throwable) {
                 is GeoError -> {
                     isTargetingErrorOccurred = true
                     inAppGeoRepository.setGeoStatus(GeoFetchStatus.GEO_FETCH_ERROR)
                     InAppProcessingManagerImpl.mindboxLogE("Error fetching geo", throwable)
                 }
 
                 is CustomerSegmentationError -> {
                     isTargetingErrorOccurred = true
                     inAppSegmentationRepository.setCustomerSegmentationStatus(
                         CustomerSegmentationFetchStatus.SEGMENTATION_FETCH_ERROR
                     )
                     handleCustomerSegmentationErrorLog(throwable)
                 }
 
                 else -> InAppProcessingManagerImpl.mindboxLogE("Error fetching segmentation", throwable)
             }
         }
         if (isTargetingErrorOccurred) return sendTargetedInApp(inApp, triggerEvent)
         if (inApp.targeting.checkTargeting(data)) {
             logI("InApp with id = ${inApp.id} sends targeting by event $triggerEvent")
             inAppRepository.sendUserTargeted(inAppId = inApp.id)
         }
     }
 
     private fun getTargetingData(triggerEvent: InAppEventType): TargetingData {
         val ordinalEvent = triggerEvent as? InAppEventType.OrdinalEvent
 
         return TargetingDataWrapper(
             triggerEvent.name,
             ordinalEvent?.body
         )
     }
 
     private fun handleCustomerSegmentationErrorLog(error: CustomerSegmentationError) {
         val volleyError = error.cause.asVolleyError()
         volleyError?.let {
             if ((volleyError.networkResponse?.statusCode == 400) && (volleyError.getErrorResponseBodyData()
                     .contains(RESPONSE_STATUS_CUSTOMER_SEGMENTS_REQUIRE_CUSTOMER))
             ) {
                 mindboxLogI("Cannot check customer segment. It's a new customer")
                 return
             }
         }
         mindboxLogW("Error fetching customer segmentations", error)
     }
 
     private fun trackTargetingErrorIfAny(inApp: InApp, data: TargetingData) {
         when {
             inApp.targeting.hasSegmentationNode() &&
                 inAppSegmentationRepository.getCustomerSegmentationFetched() == CustomerSegmentationFetchStatus.SEGMENTATION_FETCH_ERROR -> {
                 inAppTargetingErrorRepository.getError(TargetingErrorKey.CustomerSegmentation)
                     ?.let { errorDetails ->
                         inAppFailureTracker.collectFailure(
                             inAppId = inApp.id,
                             failureReason = FailureReason.CUSTOMER_SEGMENT_REQUEST_FAILED,
                             errorDetails = errorDetails
                         )
                     }
                 return
             }
 
             inApp.targeting.hasGeoNode() &&
                 inAppGeoRepository.getGeoFetchedStatus() == GeoFetchStatus.GEO_FETCH_ERROR -> {
                 inAppTargetingErrorRepository.getError(TargetingErrorKey.Geo)
                     ?.let { errorDetails ->
                         inAppFailureTracker.collectFailure(
                             inAppId = inApp.id,
                             failureReason = FailureReason.GEO_TARGETING_FAILED,
                             errorDetails = errorDetails
                         )
                     }
                 return
             }
 
             inApp.targeting.hasProductSegmentationNode() -> {
                 data.getProductFromTargetingData()?.let { product ->
                     inAppTargetingErrorRepository.getError(
                         TargetingErrorKey.ProductSegmentation(product)
                     )?.let { errorDetails ->
                         inAppFailureTracker.collectFailure(
                             inAppId = inApp.id,
                             failureReason = FailureReason.PRODUCT_SEGMENT_REQUEST_FAILED,
                             errorDetails = errorDetails
                         )
                     }
                 }
             }
         }
     }
 
     private class TargetingDataWrapper(
         override val triggerEventName: String,
         override val operationBody: String? = null,
     ) : TargetingData.OperationName, TargetingData.OperationBody
 }