diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt index 29ad53f7534..13e6485728e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt @@ -16,6 +16,8 @@ import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceive import com.woocommerce.android.ui.woopos.home.cart.WooPosCartStatus.CHECKOUT import com.woocommerce.android.ui.woopos.home.cart.WooPosCartStatus.EDITABLE import com.woocommerce.android.ui.woopos.home.cart.WooPosCartStatus.EMPTY +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import com.woocommerce.android.viewmodel.ResourceProvider import com.woocommerce.android.viewmodel.getStateFlow @@ -32,6 +34,7 @@ class WooPosCartViewModel @Inject constructor( private val getProductById: WooPosGetProductById, private val resourceProvider: ResourceProvider, private val formatPrice: WooPosFormatPrice, + private val analyticsTracker: WooPosAnalyticsTracker, savedState: SavedStateHandle, ) : ViewModel() { private val _state = savedState.getStateFlow( @@ -127,6 +130,7 @@ class WooPosCartViewModel @Inject constructor( ) ) } + analyticsTracker.track(WooPosAnalyticsEvent.Event.ItemAddedToCart) } is ParentToChildrenEvent.OrderSuccessfullyPaid -> { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt index e5e279f9a44..8092656641e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt @@ -12,6 +12,8 @@ import com.woocommerce.android.ui.woopos.home.ChildToParentEvent import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import com.woocommerce.android.util.WooLog import com.woocommerce.android.util.WooLog.T @@ -34,6 +36,7 @@ class WooPosTotalsViewModel @Inject constructor( private val cardReaderFacade: WooPosCardReaderFacade, private val totalsRepository: WooPosTotalsRepository, private val priceFormat: WooPosFormatPrice, + private val analyticsTracker: WooPosAnalyticsTracker, savedState: SavedStateHandle, ) : ViewModel() { @@ -129,12 +132,20 @@ class WooPosTotalsViewModel @Inject constructor( onSuccess = { order -> dataState.value = dataState.value.copy(orderId = order.id) uiState.value = buildWooPosTotalsViewState(order) + analyticsTracker.track(WooPosAnalyticsEvent.Event.OrderCreationSuccess) }, onFailure = { error -> WooLog.e(T.POS, "Order creation failed - $error") uiState.value = WooPosTotalsViewState.Error( resourceProvider.getString(R.string.woopos_totals_order_creation_error) ) + analyticsTracker.track( + WooPosAnalyticsEvent.Error.OrderCreationError( + errorContext = WooPosTotalsViewModel::class, + errorType = error::class.simpleName, + errorDescription = error.message + ) + ) } ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalytics.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt similarity index 58% rename from WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalytics.kt rename to WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt index d85ef37a60f..30b8f41588e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalytics.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt @@ -3,7 +3,7 @@ package com.woocommerce.android.ui.woopos.util.analytics import com.woocommerce.android.analytics.IAnalyticsEvent import kotlin.reflect.KClass -sealed class WooPosAnalytics : IAnalyticsEvent { +sealed class WooPosAnalyticsEvent : IAnalyticsEvent { override val siteless: Boolean = false override val isPosEvent: Boolean = true @@ -14,30 +14,33 @@ sealed class WooPosAnalytics : IAnalyticsEvent { _properties.putAll(additionalProperties) } - sealed class Error : WooPosAnalytics() { - abstract val errorContext: KClass + sealed class Error : WooPosAnalyticsEvent() { + abstract val errorContext: KClass abstract val errorType: String? abstract val errorDescription: String? - data class Test( - override val errorContext: KClass, + data class OrderCreationError( + override val errorContext: KClass, override val errorType: String?, override val errorDescription: String?, ) : Error() { - override val name: String = "WOO_POS_TEST_ERROR" + override val name: String = "order_creation_failed" } } - sealed class Event : WooPosAnalytics() { - data object Test : Event() { - override val name: String = "WOO_POS_TEST_EVENT" + sealed class Event : WooPosAnalyticsEvent() { + data object ItemAddedToCart : Event() { + override val name: String = "item_added_to_cart" + } + data object OrderCreationSuccess : Event() { + override val name: String = "order_creation_success" } } } internal fun IAnalyticsEvent.addProperties(additionalProperties: Map) { when (this) { - is WooPosAnalytics -> addProperties(additionalProperties) + is WooPosAnalyticsEvent -> addProperties(additionalProperties) else -> error("Cannot add properties to non-WooPosAnalytics event") } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsTracker.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsTracker.kt index cd53ae93015..35ee5f98fc2 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsTracker.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsTracker.kt @@ -14,14 +14,14 @@ class WooPosAnalyticsTracker @Inject constructor( withContext(Dispatchers.IO) { analytics.addProperties(commonPropertiesProvider.commonProperties) when (analytics) { - is WooPosAnalytics.Event -> { + is WooPosAnalyticsEvent.Event -> { analyticsTrackerWrapper.track( analytics, analytics.properties ) } - is WooPosAnalytics.Error -> { + is WooPosAnalyticsEvent.Error -> { analyticsTrackerWrapper.track( analytics, analytics.properties, diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt index a04323fb393..684fbe4d563 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt @@ -9,6 +9,8 @@ import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import com.woocommerce.android.util.captureValues import com.woocommerce.android.viewmodel.ResourceProvider @@ -19,6 +21,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Rule import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.math.BigDecimal import kotlin.test.Test @@ -52,6 +55,8 @@ class WooPosCartViewModelTest { onBlocking { invoke(eq(BigDecimal("10.0"))) }.thenReturn("10.0$") } + private val analyticsTracker: WooPosAnalyticsTracker = mock() + private val savedState: SavedStateHandle = SavedStateHandle() @Test @@ -359,6 +364,30 @@ class WooPosCartViewModelTest { assertThat(toolbar.isClearAllButtonVisible).isFalse() } + @Test + fun `when item added to cart, then should track analytics event`() = runTest { + // GIVEN + val product = ProductTestUtils.generateProduct( + productId = 23L, + productName = "title", + amount = "10.0" + ).copy(firstImageUrl = "url") + + val parentToChildrenEventsMutableFlow = MutableSharedFlow() + whenever(parentToChildrenEventReceiver.events).thenReturn(parentToChildrenEventsMutableFlow) + whenever(getProductById(eq(product.remoteId))).thenReturn(product) + val sut = createSut() + sut.state.captureValues() + + // WHEN + parentToChildrenEventsMutableFlow.emit( + ParentToChildrenEvent.ItemClickedInProductSelector(product.remoteId) + ) + + // THEN + verify(analyticsTracker).track(WooPosAnalyticsEvent.Event.ItemAddedToCart) + } + private fun createSut(): WooPosCartViewModel { return WooPosCartViewModel( childrenToParentEventSender, @@ -366,6 +395,7 @@ class WooPosCartViewModelTest { getProductById, resourceProvider, formatPrice, + analyticsTracker, savedState ) } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt index c1ec1e24bc9..02dd7c1f521 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt @@ -10,6 +10,8 @@ import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import com.woocommerce.android.viewmodel.ResourceProvider import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -49,6 +51,7 @@ class WooPosTotalsViewModelTest { } private val cardReaderFacade: WooPosCardReaderFacade = mock() + private val analyticsTracker: WooPosAnalyticsTracker = mock() private companion object { private const val EMPTY_ORDER_ID = -1L @@ -424,6 +427,75 @@ class WooPosTotalsViewModelTest { verify(cardReaderFacade, times(5)).collectPayment(any()) } + @Test + fun `when order is created, then should track order creation success`() { + val productIds = listOf(1L, 2L, 3L) + val parentToChildrenEventFlow = MutableStateFlow(ParentToChildrenEvent.CheckoutClicked(productIds)) + val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver = mock { + on { events }.thenReturn(parentToChildrenEventFlow) + } + val order = Order.getEmptyOrder( + dateCreated = Date(), + dateModified = Date() + ).copy( + totalTax = BigDecimal("2.00"), + items = listOf( + Order.Item.EMPTY.copy( + subtotal = BigDecimal("1.00"), + ), + Order.Item.EMPTY.copy( + subtotal = BigDecimal("1.00"), + ), + Order.Item.EMPTY.copy( + subtotal = BigDecimal("1.00"), + ) + ) + ) + val totalsRepository: WooPosTotalsRepository = mock { + onBlocking { createOrderWithProducts(productIds = productIds) }.thenReturn(Result.success(order)) + } + + createViewModel( + parentToChildrenEventReceiver = parentToChildrenEventReceiver, + totalsRepository = totalsRepository, + ) + } + + @Test + fun `when fails to create order, then should track order creation failure`() = runTest { + val productIds = listOf(1L, 2L, 3L) + val parentToChildrenEventFlow = MutableStateFlow(ParentToChildrenEvent.CheckoutClicked(productIds)) + val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver = mock { + on { events }.thenReturn(parentToChildrenEventFlow) + } + val errorMessage = "Order creation failed" + val totalsRepository: WooPosTotalsRepository = mock { + onBlocking { createOrderWithProducts(productIds = productIds) }.thenReturn( + Result.failure(Exception(errorMessage)) + ) + } + + val resourceProvider: ResourceProvider = mock { + on { getString(any()) }.thenReturn(errorMessage) + } + + createViewModel( + resourceProvider = resourceProvider, + parentToChildrenEventReceiver = parentToChildrenEventReceiver, + totalsRepository = totalsRepository, + ) + + verify( + analyticsTracker + ).track( + WooPosAnalyticsEvent.Error.OrderCreationError( + WooPosTotalsViewModel::class, + Exception::class.java.simpleName, + errorMessage + ) + ) + } + private fun createViewModel( resourceProvider: ResourceProvider = mock(), parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver = mock(), @@ -438,6 +510,7 @@ class WooPosTotalsViewModelTest { cardReaderFacade, totalsRepository, priceFormat, - savedState + analyticsTracker, + savedState, ) } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsTrackerTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEventTrackerTest.kt similarity index 91% rename from WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsTrackerTest.kt rename to WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEventTrackerTest.kt index ef867f6bc87..1fad253a81d 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsTrackerTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEventTrackerTest.kt @@ -9,7 +9,7 @@ import org.mockito.kotlin.whenever import kotlin.test.Test import kotlin.test.assertFails -class WooPosAnalyticsTrackerTest { +class WooPosAnalyticsEventTrackerTest { private val analyticsTrackerWrapper: AnalyticsTrackerWrapper = mock() private val commonPropertiesProvider: WooPosAnalyticsCommonPropertiesProvider = mock() @@ -21,7 +21,7 @@ class WooPosAnalyticsTrackerTest { @Test fun `given an event, when track is called, then it should track the event via wrapper`() = runTest { // GIVEN - val event = WooPosAnalytics.Event.Test + val event = WooPosAnalyticsEvent.Event.ItemAddedToCart // WHEN tracker.track(event) @@ -36,7 +36,7 @@ class WooPosAnalyticsTrackerTest { @Test fun `given an err, when track is called, then it should track the error via wrapper`() = runTest { // GIVEN - val error = WooPosAnalytics.Error.Test( + val error = WooPosAnalyticsEvent.Error.OrderCreationError( errorContext = Any::class, errorType = "test", errorDescription = "test", @@ -58,7 +58,7 @@ class WooPosAnalyticsTrackerTest { @Test fun `given an event and common properties, when track is called, then it should track the event with common properties`() = runTest { // GIVEN - val event = WooPosAnalytics.Event.Test + val event = WooPosAnalyticsEvent.Event.ItemAddedToCart val commonProperties = mapOf("test" to "test") whenever(commonPropertiesProvider.commonProperties).thenReturn(commonProperties) @@ -75,7 +75,7 @@ class WooPosAnalyticsTrackerTest { @Test fun `given an error and common properties, when track is called, then it should track the event with common properties`() = runTest { // GIVEN - val error = WooPosAnalytics.Error.Test( + val error = WooPosAnalyticsEvent.Error.OrderCreationError( errorContext = Any::class, errorType = "test", errorDescription = "test",