Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
04a4934
Allow creating refund for single quantity items
samiuelson Dec 17, 2025
32e6f56
Allow creating refund for items of multiple quantity
samiuelson Dec 19, 2025
0f10987
Clean up code: move logic to use case class
samiuelson Dec 19, 2025
67f6a7e
Add unit tests
samiuelson Dec 19, 2025
4c31f74
Merge branch 'trunk' into woomob-1855-create-refund
samiuelson Dec 19, 2025
6e6169a
Merge branch 'trunk' into woomob-1855-create-refund
samiuelson Dec 19, 2025
1f7d928
Update tests
samiuelson Dec 19, 2025
538e44b
Allow refunding multiple quantity
samiuelson Dec 22, 2025
4fc7bda
Update tests
samiuelson Dec 23, 2025
fd9b139
Refactor onUIEvent method to simplify state handling in WooPosRefundV…
samiuelson Dec 23, 2025
2d1d819
Refactor onUIEvent method to handle dialog dismissal logic and preven…
samiuelson Dec 23, 2025
6a83e99
Extract calculation logic to external use case classes
samiuelson Dec 23, 2025
eec9c49
Handle empty item lists in refund calculations to prevent errors
samiuelson Dec 23, 2025
ebf7dcd
Merge branch 'trunk' into woomob-1855-create-refund
samiuelson Dec 23, 2025
882b916
Revert optional refundTotal and refundTax
samiuelson Dec 23, 2025
6874490
Refactor dialog dismissal handling and UI event processing in WooPosR…
samiuelson Dec 23, 2025
a1dc668
Fix refund tax calculation to use BigDecimal comparison for accuracy
samiuelson Dec 23, 2025
2699b4c
Prevent processing refunds when already in processing state
samiuelson Dec 23, 2025
0709744
Add tests
samiuelson Dec 23, 2025
bd8e0ac
Simplify refund subtotal calculation by removing unnecessary checks f…
samiuelson Dec 23, 2025
8eb7f5b
Prevent dialog dismissal during refund processing to ensure user sees…
samiuelson Dec 23, 2025
2f50bb6
Notify UI on dialog dismissal to manage refund state transitions
samiuelson Dec 23, 2025
6e9c2ed
Log error when currentOrder is null during refund processing
samiuelson Dec 23, 2025
b767f93
Merge branch 'trunk' into woomob-1855-create-refund
samiuelson Dec 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.woocommerce.android.ui.woopos.orders

import java.math.BigDecimal
import javax.inject.Inject

class WooPosCalculateRefundSubtotal @Inject constructor() {
operator fun invoke(refundableItems: List<WooPosRefundableItem>): BigDecimal {
return refundableItems
.groupBy { it.orderItemId }
.entries
.sumOf { (_, items) ->
val quantity = items.size.toBigDecimal()
quantity.multiply(items.first().unitPrice)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.woocommerce.android.ui.woopos.orders

import com.woocommerce.android.model.Order
import java.math.BigDecimal
import java.math.RoundingMode
import javax.inject.Inject

class WooPosCalculateRefundTax @Inject constructor() {
operator fun invoke(
refundableItems: List<WooPosRefundableItem>,
order: Order
): BigDecimal {
return refundableItems
.groupBy { it.orderItemId }
.entries
.sumOf { (orderItemId, items) ->
calculateTaxForItem(orderItemId, items.size, order)
}
}

private fun calculateTaxForItem(
orderItemId: Long,
refundQuantity: Int,
order: Order
): BigDecimal {
val originalItem = order.items.find { it.itemId == orderItemId }

if (originalItem == null || originalItem.quantity == 0f) {
return BigDecimal.ZERO
}

return if (refundQuantity.toBigDecimal().compareTo(originalItem.quantity.toBigDecimal()) == 0) {
originalItem.totalTax
} else {
val singleItemTax = originalItem.totalTax.divide(
originalItem.quantity.toBigDecimal(),
2,
RoundingMode.HALF_UP
)
refundQuantity.toBigDecimal().multiply(singleItemTax)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.woocommerce.android.ui.woopos.orders

import com.woocommerce.android.model.Order
import org.wordpress.android.fluxc.model.refunds.RefundRequestItem
import org.wordpress.android.fluxc.model.refunds.RefundRequestTax
import java.math.BigDecimal
import java.math.RoundingMode
import javax.inject.Inject

class WooPosGroupRefundItems @Inject constructor() {
operator fun invoke(
refundableItems: List<WooPosRefundableItem>,
order: Order
): List<RefundRequestItem> {
return refundableItems
.groupBy { it.orderItemId }
.map { (orderItemId, items) ->
val originalItem = order.items.find { it.itemId == orderItemId }
val refundQuantity = items.size

RefundRequestItem(
itemId = orderItemId,
quantity = refundQuantity,
refundTotal = calculateRefundTotal(items, refundQuantity),
refundTax = calculateRefundTaxes(originalItem, refundQuantity)
)
}
}

private fun calculateRefundTotal(
items: List<WooPosRefundableItem>,
quantity: Int
): BigDecimal {
return items.first().unitPrice.multiply(quantity.toBigDecimal())
}

private fun calculateRefundTaxes(
originalItem: Order.Item?,
quantity: Int
): List<RefundRequestTax> {
if (originalItem == null || originalItem.quantity == 0f) {
return emptyList()
}

val refundQuantity = quantity.toBigDecimal()

return originalItem.taxes.map { tax ->
val singleItemTax = tax.taxAmount.divide(
originalItem.quantity.toBigDecimal(),
2,
RoundingMode.HALF_UP
)
RefundRequestTax(
taxRateId = tax.rateId,
refundTotal = refundQuantity.multiply(singleItemTax)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,19 @@ fun WooPosIssueRefundDialog(
}
val state by viewModel.state.collectAsStateWithLifecycle()

val handleDismiss = {
if (viewModel.onDismissRequest()) {
viewModel.onUIEvent(WooPosRefundUIEvent.DialogDismissed)
onDismissRequest()
}
}

WooPosDialogWrapper(
isVisible = true,
dialogBackgroundContentDescription = stringResource(
R.string.woopos_orders_issue_refund_content_description
),
onDismissRequest = {
viewModel.onUIEvent(WooPosRefundUIEvent.DialogDismissed)
onDismissRequest()
}
onDismissRequest = handleDismiss
) {
when (val currentState = state) {
is WooPosRefundState.Loading -> {
Expand All @@ -81,7 +85,7 @@ fun WooPosIssueRefundDialog(
WooPosRefundState.Content.RefundStep.SelectItems -> {
SelectItemsContent(
state = currentState,
onDismissRequest = onDismissRequest,
onDismissRequest = handleDismiss,
onContinue = {
viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked)
}
Expand All @@ -91,7 +95,7 @@ fun WooPosIssueRefundDialog(
WooPosRefundState.Content.RefundStep.ReviewRefund -> {
ReviewRefundContent(
state = currentState,
onDismissRequest = onDismissRequest,
onDismissRequest = handleDismiss,
onContinue = {
viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToConfirmRefundClicked)
},
Expand All @@ -104,28 +108,49 @@ fun WooPosIssueRefundDialog(
WooPosRefundState.Content.RefundStep.ConfirmRefund -> {
ConfirmRefundContent(
state = currentState,
onDismissRequest = onDismissRequest,
isProcessing = false,
onDismissRequest = handleDismiss,
onConfirm = {
viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundConfirmed)
onDismissRequest()
},
onBack = {
viewModel.onUIEvent(WooPosRefundUIEvent.BackToReviewClicked)
}
)
}
WooPosRefundState.Content.RefundStep.Processing -> {
ConfirmRefundContent(
state = currentState,
isProcessing = true,
onDismissRequest = {},
onConfirm = {},
onBack = {}
)
}
}
}

is WooPosRefundState.Error -> {
ErrorContent(
message = currentState.message,
onDismissRequest = onDismissRequest
onDismissRequest = handleDismiss
)
}

is WooPosRefundState.NoRefundableItems -> {
NoItemsContent(onDismissRequest = onDismissRequest)
NoItemsContent(onDismissRequest = handleDismiss)
}
is WooPosRefundState.RefundSuccess -> {
RefundSuccessContent(
state = currentState,
onDismissRequest = handleDismiss
)
}
is WooPosRefundState.RefundError -> {
ErrorContent(
message = currentState.message,
onDismissRequest = handleDismiss
)
}
}
}
Expand Down Expand Up @@ -197,6 +222,40 @@ private fun NoItemsContent(onDismissRequest: () -> Unit) {
}
}

@Composable
private fun RefundSuccessContent(
state: WooPosRefundState.RefundSuccess,
onDismissRequest: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(WooPosSpacing.XLarge.value),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
WooPosText(
text = stringResource(R.string.order_refunds_amount_refund_successful),
style = WooPosTypography.Heading,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.size(WooPosSpacing.Medium.value))
WooPosText(
text = "${state.refundedAmount} refunded for ${state.orderNumber}",
style = WooPosTypography.BodyLarge,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.size(WooPosSpacing.Large.value))
WooPosButton(
text = stringResource(R.string.close),
onClick = onDismissRequest
)
}
}

@Composable
private fun SelectItemsContent(
state: WooPosRefundState.Content,
Expand Down Expand Up @@ -543,6 +602,7 @@ private fun Divider(modifier: Modifier = Modifier) {
@Composable
private fun ConfirmRefundContent(
state: WooPosRefundState.Content,
isProcessing: Boolean,
onDismissRequest: () -> Unit,
onConfirm: () -> Unit,
onBack: () -> Unit
Expand All @@ -562,6 +622,7 @@ private fun ConfirmRefundContent(
)

ConfirmRefundButtons(
isProcessing = isProcessing,
onConfirm = onConfirm,
onBack = onBack
)
Expand Down Expand Up @@ -616,6 +677,7 @@ private fun ConfirmRefundMessage(message: String) {

@Composable
private fun ConfirmRefundButtons(
isProcessing: Boolean,
onConfirm: () -> Unit,
onBack: () -> Unit
) {
Expand All @@ -625,16 +687,29 @@ private fun ConfirmRefundButtons(
.padding(WooPosSpacing.XLarge.value),
verticalArrangement = Arrangement.spacedBy(WooPosSpacing.Medium.value)
) {
WooPosButton(
text = stringResource(R.string.woopos_orders_yes_proceed),
onClick = onConfirm,
modifier = Modifier.fillMaxWidth()
)
WooPosOutlinedButton(
text = stringResource(R.string.back),
onClick = onBack,
modifier = Modifier.fillMaxWidth()
)
if (isProcessing) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = WooPosSpacing.Medium.value),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp)
)
}
} else {
WooPosButton(
text = stringResource(R.string.woopos_orders_yes_proceed),
onClick = onConfirm,
modifier = Modifier.fillMaxWidth()
)
WooPosOutlinedButton(
text = stringResource(R.string.back),
onClick = onBack,
modifier = Modifier.fillMaxWidth()
)
}
}
}

Expand Down Expand Up @@ -825,6 +900,7 @@ fun ConfirmRefundContentPreview() {
WooPosTheme {
ConfirmRefundContent(
state = state,
isProcessing = false,
onDismissRequest = {},
onConfirm = {},
onBack = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ sealed class WooPosRefundState {

@Immutable
data object ConfirmRefund : RefundStep()

@Immutable
data object Processing : RefundStep()
}
}

Expand All @@ -44,4 +47,16 @@ sealed class WooPosRefundState {

@Immutable
data object NoRefundableItems : WooPosRefundState()

@Immutable
data class RefundSuccess(
val orderId: Long,
val orderNumber: String,
val refundedAmount: String
) : WooPosRefundState()

@Immutable
data class RefundError(
val message: String
) : WooPosRefundState()
}
Loading