From 9176078fa0e3ab318ee881cb5fc6d288c6c91524 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 17 Oct 2025 15:56:14 +0200 Subject: [PATCH 1/7] Refactor WooPosOrdersViewModel to use WooPosFormatPrice for currency formatting --- .../ui/woopos/orders/WooPosOrdersViewModel.kt | 40 ++++++------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt index aa5e24d5a01..031e6706eb9 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt @@ -4,13 +4,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.woocommerce.android.R import com.woocommerce.android.model.Order -import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchInputState import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchUIEvent import com.woocommerce.android.ui.woopos.common.data.WooPosGetProductById import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState import com.woocommerce.android.ui.woopos.util.ext.formatToMMMddYYYYAtHHmm +import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import com.woocommerce.android.viewmodel.ResourceProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import org.wordpress.android.fluxc.store.WooCommerceStore import java.math.BigDecimal import java.util.Locale import javax.inject.Inject @@ -27,11 +26,10 @@ import javax.inject.Inject @HiltViewModel class WooPosOrdersViewModel @Inject constructor( private val ordersDataSource: WooPosOrdersDataSource, - private val wooCommerceStore: WooCommerceStore, - private val selectedSite: SelectedSite, private val resourceProvider: ResourceProvider, private val locale: Locale, private val getProductById: WooPosGetProductById, + private val formatPrice: WooPosFormatPrice, ) : ViewModel() { private val _state = MutableStateFlow( @@ -326,14 +324,7 @@ class WooPosOrdersViewModel @Inject constructor( } } - private fun mapOrderItem(order: Order, selectedId: Long?): OrderItemViewState { - val formattedOrderTotals = wooCommerceStore.formatCurrencyForDisplay( - amount = order.total.toDouble(), - site = selectedSite.get(), - currencyCode = null, - applyDecimalFormatting = true - ) - + private suspend fun mapOrderItem(order: Order, selectedId: Long?): OrderItemViewState { val statusText = order.status.localizedLabel(resourceProvider, locale) return OrderItemViewState( @@ -342,7 +333,7 @@ class WooPosOrdersViewModel @Inject constructor( date = order.dateCreated.formatToMMMddYYYYAtHHmm( atWord = resourceProvider.getString(R.string.date_time_connector) ), - total = formattedOrderTotals, + total = formatPrice(order.total), customerEmail = order.customer?.email, isSelected = order.id == selectedId, status = PosOrderStatus( @@ -353,13 +344,6 @@ class WooPosOrdersViewModel @Inject constructor( } private suspend fun mapOrderDetails(order: Order): OrderDetailsViewState { - fun fmt(amount: BigDecimal) = wooCommerceStore.formatCurrencyForDisplay( - amount = amount.toDouble(), - site = selectedSite.get(), - currencyCode = null, - applyDecimalFormatting = true - ) - val statusText = order.status.localizedLabel(resourceProvider, locale) val status = PosOrderStatus( @@ -373,19 +357,19 @@ class WooPosOrdersViewModel @Inject constructor( OrderDetailsViewState.LineItemRow( id = item.itemId, name = item.name, - qtyAndUnitPrice = "${item.quantity.toInt()} x ${fmt(unitPrice)}", - lineTotal = fmt(item.total), + qtyAndUnitPrice = "${item.quantity.toInt()} x ${formatPrice(unitPrice)}", + lineTotal = formatPrice(item.total), imageUrl = product?.firstImageUrl ) } val discountCode = order.couponLines.firstOrNull()?.code val breakdown = OrderDetailsViewState.TotalsBreakdown( - products = fmt(order.productsTotal), - discount = order.discountTotal.takeIf { it != BigDecimal.ZERO }?.let { "-${fmt(it)}" }, + products = formatPrice(order.productsTotal), + discount = order.discountTotal.takeIf { it != BigDecimal.ZERO }?.let { "-${formatPrice(it)}" }, discountCode = discountCode, - taxes = fmt(order.totalTax), - shipping = order.shippingTotal.takeIf { it != BigDecimal.ZERO }?.let { fmt(it) } + taxes = formatPrice(order.totalTax), + shipping = order.shippingTotal.takeIf { it != BigDecimal.ZERO }?.let { formatPrice(it) } ) return OrderDetailsViewState( @@ -398,8 +382,8 @@ class WooPosOrdersViewModel @Inject constructor( status = status, lineItems = lineItems, breakdown = breakdown, - total = fmt(order.total), - totalPaid = fmt(order.total), + total = formatPrice(order.total), + totalPaid = formatPrice(order.total), paymentMethodTitle = order.paymentMethodTitle.takeIf { it.isNotBlank() } ) } From 02605bc727eac24230536b2569ff42b2e7698b34 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 17 Oct 2025 16:02:32 +0200 Subject: [PATCH 2/7] Refactor WooPosOrdersViewModelTest to use WooPosFormatPrice for currency formatting --- .../orders/WooPosOrdersViewModelTest.kt | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt index 2d265f7c03e..144a8e5d99d 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt @@ -2,7 +2,6 @@ package com.woocommerce.android.ui.woopos.orders import com.woocommerce.android.R import com.woocommerce.android.model.Order -import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.orders.OrderTestUtils import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchInputState import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchUIEvent @@ -10,6 +9,7 @@ import com.woocommerce.android.ui.woopos.common.data.WooPosGetProductById import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule +import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import com.woocommerce.android.viewmodel.ResourceProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow @@ -22,12 +22,9 @@ import org.junit.Rule import org.junit.Test import org.mockito.Mockito.mock import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.store.WooCommerceStore import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) @@ -41,20 +38,18 @@ class WooPosOrdersViewModelTest { private lateinit var viewModel: WooPosOrdersViewModel private fun order(id: Long = 1L): Order = OrderTestUtils.generateTestOrder(orderId = id) - private val wooStore: WooCommerceStore = org.mockito.kotlin.mock() - private val selectedSite: SelectedSite = org.mockito.kotlin.mock() private val resourceProvider: ResourceProvider = org.mockito.kotlin.mock() private val getProductById: WooPosGetProductById = org.mockito.kotlin.mock() + private val formatPrice: WooPosFormatPrice = org.mockito.kotlin.mock() private val providedLocale: Locale = Locale.US private fun createViewModel(): WooPosOrdersViewModel { return WooPosOrdersViewModel( ordersDataSource = dataSource, - wooCommerceStore = wooStore, - selectedSite = selectedSite, resourceProvider = resourceProvider, locale = providedLocale, - getProductById = getProductById + getProductById = getProductById, + formatPrice = formatPrice ) } @@ -62,17 +57,9 @@ class WooPosOrdersViewModelTest { fun setUp() { whenever(resourceProvider.getString(R.string.date_time_connector)).thenReturn("at") - whenever( - wooStore.formatCurrencyForDisplay( - amount = any(), - site = any(), - currencyCode = anyOrNull(), - applyDecimalFormatting = any() - ) - ).thenReturn("$0.00") - - val site: SiteModel = mock() - whenever(selectedSite.get()).thenReturn(site) + runBlocking { + whenever(formatPrice.invoke(any())).thenReturn("$0.00") + } whenever(dataSource.loadOrders()).thenReturn( flow { emit(LoadOrdersResult.SuccessCache(listOf(order(1), order(2)))) } From bac12159ccc638a43fd00a42f5870b36260ae822 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 17 Oct 2025 16:40:30 +0200 Subject: [PATCH 3/7] Add refund data to WooPosOrders model and screen --- .../data/WooPosGetOrderRefundsByOrderId.kt | 21 +++++++++++ .../ui/woopos/orders/WooPosOrdersDetails.kt | 36 +++++++++++++++++++ .../ui/woopos/orders/WooPosOrdersScreen.kt | 4 ++- .../ui/woopos/orders/WooPosOrdersState.kt | 4 ++- .../ui/woopos/orders/WooPosOrdersViewModel.kt | 16 ++++++++- WooCommerce/src/main/res/values/strings.xml | 2 ++ .../orders/WooPosOrdersViewModelTest.kt | 6 +++- 7 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/data/WooPosGetOrderRefundsByOrderId.kt diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/data/WooPosGetOrderRefundsByOrderId.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/data/WooPosGetOrderRefundsByOrderId.kt new file mode 100644 index 00000000000..a630e7989bb --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/data/WooPosGetOrderRefundsByOrderId.kt @@ -0,0 +1,21 @@ +package com.woocommerce.android.ui.woopos.common.data + +import com.woocommerce.android.model.Refund +import com.woocommerce.android.model.toAppModel +import com.woocommerce.android.tools.SelectedSite +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.wordpress.android.fluxc.store.WCRefundStore +import javax.inject.Inject + +class WooPosGetOrderRefundsByOrderId @Inject constructor( + private val refundStore: WCRefundStore, + private val selectedSite: SelectedSite +) { + suspend operator fun invoke(orderId: Long): List { + return withContext(Dispatchers.IO) { + refundStore.getAllRefunds(selectedSite.get(), orderId) + .map { it.toAppModel() } + } + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt index d2caf85ee3c..221bb4fd2a8 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt @@ -251,6 +251,42 @@ fun OrderDetails( ) } } + + if (details.breakdown.refunds.isNotEmpty()) { + Column { + details.breakdown.refunds.forEach { refundAmount -> + Row(Modifier.fillMaxWidth()) { + WooPosText( + text = stringResource(R.string.woopos_orders_details_refunded_label), + style = WooPosTypography.BodySmall, + modifier = Modifier.weight(1f) + ) + WooPosText( + text = refundAmount, + style = WooPosTypography.BodySmall + ) + } + Spacer(Modifier.height(WooPosSpacing.XSmall.value)) + } + } + } + + details.breakdown.netPayment?.let { netPayment -> + Spacer(Modifier.height(WooPosSpacing.Small.value)) + Row(Modifier.fillMaxWidth()) { + WooPosText( + text = stringResource(R.string.woopos_orders_details_net_payment_label), + style = WooPosTypography.BodySmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.weight(1f) + ) + WooPosText( + text = netPayment, + style = WooPosTypography.BodySmall, + fontWeight = FontWeight.Bold + ) + } + } } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt index 35b0dee7201..5a23915620a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt @@ -517,7 +517,9 @@ private fun sampleOrderDetails( discount = "-$5.00", discountCode = "8qew4mnq", taxes = "$0.00", - shipping = null + shipping = null, + refunds = emptyList(), + netPayment = null ), total = "$17.00", totalPaid = "$17.00", diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersState.kt index 1464eb2e5c9..4c5f142227c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersState.kt @@ -35,7 +35,9 @@ data class OrderDetailsViewState( val discount: String?, val discountCode: String?, val taxes: String, - val shipping: String? + val shipping: String?, + val refunds: List, + val netPayment: String? ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt index 031e6706eb9..a3997b31a7e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt @@ -6,6 +6,7 @@ import com.woocommerce.android.R import com.woocommerce.android.model.Order import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchInputState import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchUIEvent +import com.woocommerce.android.ui.woopos.common.data.WooPosGetOrderRefundsByOrderId import com.woocommerce.android.ui.woopos.common.data.WooPosGetProductById import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState @@ -30,6 +31,7 @@ class WooPosOrdersViewModel @Inject constructor( private val locale: Locale, private val getProductById: WooPosGetProductById, private val formatPrice: WooPosFormatPrice, + private val getOrderRefunds: WooPosGetOrderRefundsByOrderId, ) : ViewModel() { private val _state = MutableStateFlow( @@ -364,12 +366,24 @@ class WooPosOrdersViewModel @Inject constructor( } val discountCode = order.couponLines.firstOrNull()?.code + + val refunds = getOrderRefunds(order.id) + val refundAmounts = refunds.map { "-${formatPrice(it.amount)}" } + val totalRefunded = refunds.sumOf { it.amount } + val netPayment = if (totalRefunded > BigDecimal.ZERO) { + formatPrice(order.total - totalRefunded) + } else { + null + } + val breakdown = OrderDetailsViewState.TotalsBreakdown( products = formatPrice(order.productsTotal), discount = order.discountTotal.takeIf { it != BigDecimal.ZERO }?.let { "-${formatPrice(it)}" }, discountCode = discountCode, taxes = formatPrice(order.totalTax), - shipping = order.shippingTotal.takeIf { it != BigDecimal.ZERO }?.let { formatPrice(it) } + shipping = order.shippingTotal.takeIf { it != BigDecimal.ZERO }?.let { formatPrice(it) }, + refunds = refundAmounts, + netPayment = netPayment ) return OrderDetailsViewState( diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 88e42b3ca9d..b4c227ec7a8 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -3809,6 +3809,8 @@ Shipping Total Total paid + Refunded + Net Payment Order products list Order totals breakdown diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt index 144a8e5d99d..5db14c4a521 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt @@ -5,6 +5,7 @@ import com.woocommerce.android.model.Order import com.woocommerce.android.ui.orders.OrderTestUtils import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchInputState import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchUIEvent +import com.woocommerce.android.ui.woopos.common.data.WooPosGetOrderRefundsByOrderId import com.woocommerce.android.ui.woopos.common.data.WooPosGetProductById import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState @@ -41,6 +42,7 @@ class WooPosOrdersViewModelTest { private val resourceProvider: ResourceProvider = org.mockito.kotlin.mock() private val getProductById: WooPosGetProductById = org.mockito.kotlin.mock() private val formatPrice: WooPosFormatPrice = org.mockito.kotlin.mock() + private val getOrderRefunds: WooPosGetOrderRefundsByOrderId = org.mockito.kotlin.mock() private val providedLocale: Locale = Locale.US private fun createViewModel(): WooPosOrdersViewModel { @@ -49,7 +51,8 @@ class WooPosOrdersViewModelTest { resourceProvider = resourceProvider, locale = providedLocale, getProductById = getProductById, - formatPrice = formatPrice + formatPrice = formatPrice, + getOrderRefunds = getOrderRefunds ) } @@ -76,6 +79,7 @@ class WooPosOrdersViewModelTest { runBlocking { whenever(getProductById.invoke(any())).thenReturn(null) + whenever(getOrderRefunds.invoke(any())).thenReturn(emptyList()) } } From 024f87e3910b81f783c2d8676a22e9dc09a61b22 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 17 Oct 2025 16:41:52 +0200 Subject: [PATCH 4/7] Add refund data and net payment to WooPosOrdersScreen --- .../android/ui/woopos/orders/WooPosOrdersScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt index 5a23915620a..77c6b68088c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt @@ -518,8 +518,8 @@ private fun sampleOrderDetails( discountCode = "8qew4mnq", taxes = "$0.00", shipping = null, - refunds = emptyList(), - netPayment = null + refunds = listOf("-$3.00", "-$2.00"), + netPayment = "$12.00" ), total = "$17.00", totalPaid = "$17.00", From 3d846dd3410c67b4ede01147a691314cf14b44e2 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 17 Oct 2025 16:44:46 +0200 Subject: [PATCH 5/7] Add tests for refund data handling in WooPosOrdersViewModel --- .../orders/WooPosOrdersViewModelTest.kt | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt index 5db14c4a521..ddcb962b510 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModelTest.kt @@ -612,4 +612,69 @@ class WooPosOrdersViewModelTest { assertThat(content.items.keys.map { it.id }).containsExactly(300L, 400L) assertThat(content.selectedDetails.id).isEqualTo(300L) } + + @Test + fun `given order with no refunds, when mapped, then breakdown has empty refunds and null net payment`() = runTest { + // GIVEN + whenever(dataSource.loadOrders()).thenReturn( + flow { emit(LoadOrdersResult.SuccessRemote(listOf(order(1)))) } + ) + runBlocking { + whenever(getOrderRefunds.invoke(1L)).thenReturn(emptyList()) + } + + // WHEN + viewModel = createViewModel() + advanceUntilIdle() + + // THEN + val content = viewModel.state.value as WooPosOrdersState.Content + assertThat(content.selectedDetails.breakdown.refunds).isEmpty() + assertThat(content.selectedDetails.breakdown.netPayment).isNull() + } + + @Test + fun `given order with refunds, when mapped, then breakdown includes refund amounts and net payment`() = runTest { + // GIVEN + val testOrder = order(1) + whenever(dataSource.loadOrders()).thenReturn( + flow { emit(LoadOrdersResult.SuccessRemote(listOf(testOrder))) } + ) + + val refund1 = com.woocommerce.android.model.Refund( + id = 1, + dateCreated = java.util.Date(), + amount = java.math.BigDecimal("10.00"), + reason = null, + automaticGatewayRefund = false, + items = emptyList(), + shippingLines = emptyList(), + feeLines = emptyList() + ) + val refund2 = com.woocommerce.android.model.Refund( + id = 2, + dateCreated = java.util.Date(), + amount = java.math.BigDecimal("5.00"), + reason = null, + automaticGatewayRefund = false, + items = emptyList(), + shippingLines = emptyList(), + feeLines = emptyList() + ) + + runBlocking { + whenever(getOrderRefunds.invoke(1L)).thenReturn(listOf(refund1, refund2)) + whenever(formatPrice.invoke(java.math.BigDecimal("10.00"))).thenReturn("$10.00") + whenever(formatPrice.invoke(java.math.BigDecimal("5.00"))).thenReturn("$5.00") + } + + // WHEN + viewModel = createViewModel() + advanceUntilIdle() + + // THEN + val content = viewModel.state.value as WooPosOrdersState.Content + assertThat(content.selectedDetails.breakdown.refunds).containsExactly("-$10.00", "-$5.00") + assertThat(content.selectedDetails.breakdown.netPayment).isNotNull() + } } From 80eb57c6d91af9028e9b91ca3f6cd32a53bdd5e5 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 17 Oct 2025 17:06:10 +0200 Subject: [PATCH 6/7] Rename OrderDetails to WooPosOrderDetails and update layout for refund and net payment display --- .../ui/woopos/orders/WooPosOrdersDetails.kt | 144 ++++++++++++------ .../ui/woopos/orders/WooPosOrdersScreen.kt | 3 +- 2 files changed, 96 insertions(+), 51 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt index 221bb4fd2a8..683e6e17123 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import com.woocommerce.android.R +import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosButton import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosCard import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosText @@ -42,7 +43,7 @@ import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosThe import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTypography @Composable -fun OrderDetails( +fun WooPosOrderDetails( modifier: Modifier = Modifier, details: OrderDetailsViewState, onEmailReceiptButtonClicked: (Long) -> Unit = {} @@ -227,66 +228,75 @@ fun OrderDetails( fontWeight = FontWeight.Bold ) } - } - } - Column { - Row(Modifier.fillMaxWidth()) { - WooPosText( - text = stringResource(R.string.woopos_orders_details_total_paid_label), - style = WooPosTypography.BodySmall, - modifier = Modifier.weight(1f) - ) - WooPosText( - text = details.totalPaid, - style = WooPosTypography.BodySmall - ) - } - details.paymentMethodTitle?.let { - Spacer(Modifier.height(WooPosSpacing.XSmall.value)) - WooPosText( - text = it, - style = WooPosTypography.BodySmall, - color = WooPosTheme.colors.onSurfaceVariantHighest - ) - } - } + Row( + Modifier + .fillMaxWidth() + ) { + WooPosText( + text = stringResource(R.string.woopos_orders_details_total_paid_label), + style = WooPosTypography.BodySmall, + modifier = Modifier.weight(1f) + ) + WooPosText( + text = details.totalPaid, + style = WooPosTypography.BodySmall + ) + } + details.paymentMethodTitle?.let { + Spacer(Modifier.height(WooPosSpacing.XSmall.value)) + WooPosText( + text = it, + style = WooPosTypography.BodySmall, + color = WooPosTheme.colors.onSurfaceVariantHighest, + ) + } + + if (details.breakdown.refunds.isNotEmpty()) { + Spacer(Modifier.height(WooPosSpacing.Small.value)) + details.breakdown.refunds.forEachIndexed { index, refundAmount -> + Row( + Modifier + .fillMaxWidth() + ) { + WooPosText( + text = stringResource(R.string.woopos_orders_details_refunded_label), + style = WooPosTypography.BodySmall, + modifier = Modifier.weight(1f) + ) + WooPosText( + text = refundAmount, + style = WooPosTypography.BodySmall + ) + } + if (index < details.breakdown.refunds.size - 1) { + Spacer(Modifier.height(WooPosSpacing.XSmall.value)) + } + } + } - if (details.breakdown.refunds.isNotEmpty()) { - Column { - details.breakdown.refunds.forEach { refundAmount -> - Row(Modifier.fillMaxWidth()) { + details.breakdown.netPayment?.let { netPayment -> + Spacer(Modifier.height(WooPosSpacing.Small.value)) + Row( + Modifier + .fillMaxWidth() + .padding(bottom = WooPosSpacing.Medium.value) + ) { WooPosText( - text = stringResource(R.string.woopos_orders_details_refunded_label), + text = stringResource(R.string.woopos_orders_details_net_payment_label), style = WooPosTypography.BodySmall, + fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f) ) WooPosText( - text = refundAmount, - style = WooPosTypography.BodySmall + text = netPayment, + style = WooPosTypography.BodySmall, + fontWeight = FontWeight.Bold ) } - Spacer(Modifier.height(WooPosSpacing.XSmall.value)) } } } - - details.breakdown.netPayment?.let { netPayment -> - Spacer(Modifier.height(WooPosSpacing.Small.value)) - Row(Modifier.fillMaxWidth()) { - WooPosText( - text = stringResource(R.string.woopos_orders_details_net_payment_label), - style = WooPosTypography.BodySmall, - fontWeight = FontWeight.Bold, - modifier = Modifier.weight(1f) - ) - WooPosText( - text = netPayment, - style = WooPosTypography.BodySmall, - fontWeight = FontWeight.Bold - ) - } - } } } } @@ -317,3 +327,39 @@ private fun OrderLineItemImage(imageUrl: String?) { ) } } + +@WooPosPreview +@Composable +fun WooPosOrderDetailsPreview() { + val orderDetails = OrderDetailsViewState( + id = 1L, + number = "#014", + dateTime = "Aug 28, 2025 at 10:31 AM", + customerEmail = "johndoe@mail.com", + status = PosOrderStatus(text = "Completed", colorKey = OrderStatusColorKey.COMPLETED), + lineItems = listOf( + OrderDetailsViewState.LineItemRow(101, "Cup", "1 x $8.50", "$15.00", null), + OrderDetailsViewState.LineItemRow(102, "Coffee Container", "1 x $10.00", "$8.00", null), + OrderDetailsViewState.LineItemRow(103, "Paper Filter", "1 x $4.50", "$8.00", null) + ), + breakdown = OrderDetailsViewState.TotalsBreakdown( + products = "$23.00", + discount = "-$5.00", + discountCode = "8qew4mnq", + taxes = "$0.00", + shipping = null, + refunds = listOf("-$3.00", "-$2.00"), + netPayment = "$12.00" + ), + total = "$17.00", + totalPaid = "$17.00", + paymentMethodTitle = "WooCommerce In-Person Payments" + ) + + WooPosTheme { + WooPosOrderDetails( + details = orderDetails, + onEmailReceiptButtonClicked = {} + ) + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt index 77c6b68088c..42b600136b1 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -167,7 +166,7 @@ private fun OrdersContent( .background(MaterialTheme.colorScheme.surface) ) - OrderDetails( + WooPosOrderDetails( modifier = Modifier .weight(0.7f) .fillMaxHeight() From 8b8420af60e00cd0e9add3a2fb795027b08c3f5b Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 21 Oct 2025 09:10:22 +0200 Subject: [PATCH 7/7] Add unit tests for WooPosGetOrderRefundsByOrderId functionality --- .../WooPosGetOrderRefundsByOrderIdTest.kt | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/common/data/WooPosGetOrderRefundsByOrderIdTest.kt diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/common/data/WooPosGetOrderRefundsByOrderIdTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/common/data/WooPosGetOrderRefundsByOrderIdTest.kt new file mode 100644 index 00000000000..54be743dfdc --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/common/data/WooPosGetOrderRefundsByOrderIdTest.kt @@ -0,0 +1,102 @@ +package com.woocommerce.android.ui.woopos.common.data + +import com.woocommerce.android.tools.SelectedSite +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.refunds.WCRefundModel +import org.wordpress.android.fluxc.store.WCRefundStore +import java.math.BigDecimal +import java.util.Date + +class WooPosGetOrderRefundsByOrderIdTest { + private lateinit var refundStore: WCRefundStore + private lateinit var selectedSite: SelectedSite + private lateinit var sut: WooPosGetOrderRefundsByOrderId + private lateinit var site: SiteModel + + @Before + fun setup() { + refundStore = mock() + selectedSite = mock() + site = mock() + + whenever(selectedSite.get()).thenReturn(site) + + sut = WooPosGetOrderRefundsByOrderId( + refundStore = refundStore, + selectedSite = selectedSite + ) + } + + @Test + fun `given refunds exist, when invoke called, then returns mapped refunds`() = runTest { + // GIVEN + val orderId = 123L + val fluxCRefunds = listOf( + WCRefundModel( + id = 1L, + dateCreated = Date(), + amount = BigDecimal.TEN, + reason = "Test refund", + automaticGatewayRefund = true, + items = emptyList(), + shippingLineItems = emptyList(), + feeLineItems = emptyList() + ), + WCRefundModel( + id = 2L, + dateCreated = Date(), + amount = BigDecimal.valueOf(5), + reason = "Another refund", + automaticGatewayRefund = false, + items = emptyList(), + shippingLineItems = emptyList(), + feeLineItems = emptyList() + ) + ) + whenever(refundStore.getAllRefunds(site, orderId)).thenReturn(fluxCRefunds) + + // WHEN + val result = sut.invoke(orderId) + + // THEN + assertThat(result).hasSize(2) + assertThat(result[0].id).isEqualTo(1L) + assertThat(result[0].amount).isEqualTo(BigDecimal.TEN) + assertThat(result[1].id).isEqualTo(2L) + assertThat(result[1].amount).isEqualTo(BigDecimal.valueOf(5)) + } + + @Test + fun `given no refunds exist, when invoke called, then returns empty list`() = runTest { + // GIVEN + val orderId = 123L + whenever(refundStore.getAllRefunds(site, orderId)).thenReturn(emptyList()) + + // WHEN + val result = sut.invoke(orderId) + + // THEN + assertThat(result).isEmpty() + } + + @Test + fun `given orderId provided, when invoke called, then passes correct orderId to store`() = runTest { + // GIVEN + val orderId = 456L + whenever(refundStore.getAllRefunds(any(), any())).thenReturn(emptyList()) + + // WHEN + sut.invoke(orderId) + + // THEN + verify(refundStore).getAllRefunds(site, orderId) + } +}