diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 67980f4eb0a..6869e8b8388 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,6 +3,7 @@ 19.8 ----- - [*] Stats: The Google Campaign analytics card now has a call to action to create a paid campaign if there are no campaign analytics for the selected time period. [https://github.com/woocommerce/woocommerce-android/pull/12161] +- [**] Added support for partial refunds. [https://github.com/woocommerce/woocommerce-android/pull/12164] 19.7 ----- diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Refund.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Refund.kt index 1ffa8fda548..1d74193212a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Refund.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Refund.kt @@ -1,11 +1,14 @@ package com.woocommerce.android.model import android.os.Parcelable +import com.woocommerce.android.extensions.sumByBigDecimal import com.woocommerce.android.ui.products.ProductHelper import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.wordpress.android.fluxc.model.refunds.WCRefundModel -import org.wordpress.android.fluxc.model.refunds.WCRefundModel.* +import org.wordpress.android.fluxc.model.refunds.WCRefundModel.WCRefundFeeLine +import org.wordpress.android.fluxc.model.refunds.WCRefundModel.WCRefundItem +import org.wordpress.android.fluxc.model.refunds.WCRefundModel.WCRefundShippingLine import java.math.BigDecimal import java.math.RoundingMode.HALF_UP import java.util.Date @@ -117,6 +120,10 @@ fun WCRefundFeeLine.toAppModel(): Refund.FeeLine { ) } +fun List.getRefundedProductsAmount() = filter { it.items.isNotEmpty() }.sumByBigDecimal { + it.items.sumByBigDecimal { item -> item.total } +} + fun List.getNonRefundedProducts( products: List ): List { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/refunds/IssueRefundViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/refunds/IssueRefundViewModel.kt index 352a93535bd..55cf6071ada 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/refunds/IssueRefundViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/refunds/IssueRefundViewModel.kt @@ -8,6 +8,7 @@ import com.woocommerce.android.R import com.woocommerce.android.WooException import com.woocommerce.android.analytics.AnalyticsEvent.CREATE_ORDER_REFUND_ITEM_QUANTITY_DIALOG_OPENED import com.woocommerce.android.analytics.AnalyticsEvent.CREATE_ORDER_REFUND_NEXT_BUTTON_TAPPED +import com.woocommerce.android.analytics.AnalyticsEvent.CREATE_ORDER_REFUND_PRODUCT_AMOUNT_DIALOG_OPENED import com.woocommerce.android.analytics.AnalyticsEvent.CREATE_ORDER_REFUND_SELECT_ALL_ITEMS_BUTTON_TAPPED import com.woocommerce.android.analytics.AnalyticsEvent.CREATE_ORDER_REFUND_SUMMARY_REFUND_BUTTON_TAPPED import com.woocommerce.android.analytics.AnalyticsEvent.ORDER_NOTE_ADD_FAILED @@ -28,6 +29,7 @@ import com.woocommerce.android.model.OrderNote import com.woocommerce.android.model.PaymentGateway import com.woocommerce.android.model.Refund import com.woocommerce.android.model.getMaxRefundQuantities +import com.woocommerce.android.model.getRefundedProductsAmount import com.woocommerce.android.model.toAppModel import com.woocommerce.android.tools.NetworkStatus import com.woocommerce.android.tools.SelectedSite @@ -38,6 +40,7 @@ import com.woocommerce.android.ui.payments.refunds.IssueRefundViewModel.InputVal import com.woocommerce.android.ui.payments.refunds.IssueRefundViewModel.IssueRefundEvent.HideValidationError import com.woocommerce.android.ui.payments.refunds.IssueRefundViewModel.IssueRefundEvent.OpenUrl import com.woocommerce.android.ui.payments.refunds.IssueRefundViewModel.IssueRefundEvent.ShowNumberPicker +import com.woocommerce.android.ui.payments.refunds.IssueRefundViewModel.IssueRefundEvent.ShowRefundAmountDialog import com.woocommerce.android.ui.payments.refunds.IssueRefundViewModel.IssueRefundEvent.ShowRefundConfirmation import com.woocommerce.android.ui.payments.refunds.IssueRefundViewModel.IssueRefundEvent.ShowRefundSummary import com.woocommerce.android.ui.payments.refunds.IssueRefundViewModel.IssueRefundEvent.ShowValidationError @@ -179,6 +182,7 @@ class IssueRefundViewModel @Inject constructor( private val refundableFeeLineIds: List /* Fees lines that haven't been refunded */ private val maxRefund: BigDecimal + private val availableRefundForProducts: BigDecimal private val maxQuantities: Map private val formatCurrency: (BigDecimal) -> String private val gateway: PaymentGateway @@ -202,6 +206,7 @@ class IssueRefundViewModel @Inject constructor( refunds = refundStore.getAllRefunds(selectedSite.get(), arguments.orderId).map { it.toAppModel() } formatCurrency = currencyFormatter.buildBigDecimalFormatter(order.currency) maxRefund = order.total - order.refundTotal + availableRefundForProducts = calculateProductTotal(order) - refunds.getRefundedProductsAmount() maxQuantities = refunds.getMaxRefundQuantities(order.items) .map { (id, quantity) -> id to quantity } .toMap() @@ -494,6 +499,7 @@ class IssueRefundViewModel @Inject constructor( refundStore.createItemsRefund( selectedSite.get(), order.id, + commonState.refundTotal, refundSummaryState.refundReason ?: "", true, gateway.supportsRefunds, @@ -614,6 +620,24 @@ class IssueRefundViewModel @Inject constructor( refundSummaryState = refundSummaryState.copy(isSummaryTextTooLong = currLength > maxLength) } + fun onProductRefundAmountTapped() { + triggerEvent( + ShowRefundAmountDialog( + refundByItemsState.productsRefund, + availableRefundForProducts, + resourceProvider.getString( + R.string.order_refunds_available_for_refund, + formatCurrency(availableRefundForProducts) + ) + ) + ) + + analyticsTrackerWrapper.track( + CREATE_ORDER_REFUND_PRODUCT_AMOUNT_DIALOG_OPENED, + mapOf(AnalyticsTracker.KEY_ORDER_ID to order.id) + ) + } + fun onProductsRefundAmountChanged(newAmount: BigDecimal) { refundByItemsState = refundByItemsState.copy( productsRefund = newAmount, @@ -628,7 +652,7 @@ class IssueRefundViewModel @Inject constructor( selectedQuantities[uniqueId] = newQuantity val (subtotal, taxes) = newItems.calculateTotals() - val productsRefund = min(max(subtotal + taxes, BigDecimal.ZERO), maxRefund) + val productsRefund = min(max(subtotal + taxes, BigDecimal.ZERO), availableRefundForProducts) val selectButtonTitle = if (areAllItemsSelected) { resourceProvider.getString(R.string.order_refunds_items_select_none) @@ -867,6 +891,8 @@ class IssueRefundViewModel @Inject constructor( return availableFeeLines } + private fun calculateProductTotal(order: Order) = order.items.sumByBigDecimal { it.subtotal + it.totalTax } + private fun calculatePartialShippingSubtotal(selectedShippingLinesId: List): BigDecimal { return order.shippingLines .filter { it.itemId in selectedShippingLinesId } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/refunds/RefundByItemsFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/refunds/RefundByItemsFragment.kt index 72d62a64a63..87a3a1df2b4 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/refunds/RefundByItemsFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/refunds/RefundByItemsFragment.kt @@ -113,6 +113,9 @@ class RefundByItemsFragment : viewModel.onFeesRefundMainSwitchChanged(isChecked) binding.issueRefundFeesSection.root.isVisible = isChecked } + productsBinding.issueRefundProductsTotal.setOnClickListener { + viewModel.onProductRefundAmountTapped() + } } private fun setupObservers() { diff --git a/WooCommerce/src/main/res/layout/refund_by_items_products.xml b/WooCommerce/src/main/res/layout/refund_by_items_products.xml index 13cc361410c..3de20b516ea 100644 --- a/WooCommerce/src/main/res/layout/refund_by_items_products.xml +++ b/WooCommerce/src/main/res/layout/refund_by_items_products.xml @@ -108,11 +108,13 @@