diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/BookingMapper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/BookingMapper.kt index 382f980eb81e..ccc3e9b30bbd 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/BookingMapper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/BookingMapper.kt @@ -1,8 +1,10 @@ package com.woocommerce.android.ui.bookings +import com.woocommerce.android.R import com.woocommerce.android.extensions.isNotEqualTo import com.woocommerce.android.model.Address import com.woocommerce.android.model.GetLocations +import com.woocommerce.android.model.UiString import com.woocommerce.android.ui.bookings.compose.BookingAppointmentDetailsModel import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetailsModel @@ -10,6 +12,7 @@ import com.woocommerce.android.ui.bookings.compose.BookingPaymentDetailsModel import com.woocommerce.android.ui.bookings.compose.BookingStaffMemberStatus import com.woocommerce.android.ui.bookings.compose.BookingStatus import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel +import com.woocommerce.android.ui.bookings.details.CancelStatus import com.woocommerce.android.ui.bookings.list.BookingListItem import com.woocommerce.android.util.CurrencyFormatter import kotlinx.coroutines.Dispatchers @@ -56,7 +59,8 @@ class BookingMapper @Inject constructor( } fun Booking.toAppointmentDetailsModel( - staffMemberStatus: BookingStaffMemberStatus? + staffMemberStatus: BookingStaffMemberStatus?, + cancelStatus: CancelStatus, ): BookingAppointmentDetailsModel { val durationMinutes = Duration.between(start, end).toMinutes() return BookingAppointmentDetailsModel( @@ -66,7 +70,8 @@ class BookingMapper @Inject constructor( // TODO replace mocked values when available from API location = "238 Willow Creek Drive, Montgomery AL 36109", duration = "$durationMinutes min", - price = currencyFormatter.formatCurrency(cost, currency) + price = currencyFormatter.formatCurrency(cost, currency), + cancelStatus = cancelStatus, ) } @@ -109,6 +114,23 @@ class BookingMapper @Inject constructor( return "${billingFirstName.orEmpty()} ${billingLastName.orEmpty()}".trim().ifEmpty { null } } + fun buildCancelDialogMessage(booking: Booking): UiString { + val customerName = booking.order.customerInfo?.fullName()?.let { UiString.UiStringText(it) } + ?: UiString.UiStringRes(R.string.customer_detail_guest_customer) + val serviceName = booking.order.productInfo?.name ?: "-" + val date = detailsDateFormatter.format(booking.start) + val time = timeRangeFormatter.format(booking.start) + return UiString.UiStringRes( + R.string.booking_cancel_dialog_message, + listOf( + customerName, + UiString.UiStringText(serviceName), + UiString.UiStringText(date), + UiString.UiStringText(time) + ) + ) + } + private suspend fun BookingCustomerInfo.address(): Address? { val countryCode = billingCountry ?: return null val (country, state) = withContext(Dispatchers.IO) { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingAppointmentDetails.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingAppointmentDetails.kt index 3e5ce617f3f1..537932d8e0c0 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingAppointmentDetails.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingAppointmentDetails.kt @@ -8,8 +8,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,6 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.woocommerce.android.R +import com.woocommerce.android.ui.bookings.details.CancelStatus import com.woocommerce.android.ui.compose.animations.SkeletonView import com.woocommerce.android.ui.compose.component.WCOutlinedButton import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews @@ -87,9 +91,17 @@ fun BookingAppointmentDetails( colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colorScheme.onSurface ), - onClick = onCancelBooking + onClick = onCancelBooking, + enabled = model.cancelButtonEnabled, ) { - Text(text = stringResource(R.string.booking_details_cancel_booking_button)) + if (model.cancelInProgressShown) { + CircularProgressIndicator( + color = LocalContentColor.current, + modifier = Modifier.size(24.dp) + ) + } else { + Text(text = stringResource(R.string.booking_details_cancel_booking_button)) + } } HorizontalDivider(thickness = 0.5.dp) } @@ -139,8 +151,12 @@ data class BookingAppointmentDetailsModel( val staff: BookingStaffMemberStatus?, val location: String, val duration: String, - val price: String -) + val price: String, + val cancelStatus: CancelStatus, +) { + val cancelButtonEnabled: Boolean = cancelStatus != CancelStatus.InProgress + val cancelInProgressShown: Boolean = cancelStatus == CancelStatus.InProgress +} sealed interface BookingStaffMemberStatus { data object Loading : BookingStaffMemberStatus @@ -159,7 +175,8 @@ private fun BookingAppointmentDetailsPreview() { staff = BookingStaffMemberStatus.Loading, location = "238 Willow Creek Drive, Montgomery AL 36109", duration = "60 min", - price = "$55.00" + price = "$55.00", + cancelStatus = CancelStatus.Idle, ), onCancelBooking = {}, modifier = Modifier.fillMaxWidth() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingAttendanceStatusBottomSheet.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingAttendanceStatusBottomSheet.kt index ebb8952dd194..1c883cdf234a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingAttendanceStatusBottomSheet.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/compose/BookingAttendanceStatusBottomSheet.kt @@ -70,7 +70,11 @@ private fun BookingAttendanceStatusSelection( color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 22.dp) ) - BookingAttendanceStatus.entries.forEachIndexed { index, status -> + listOf( + BookingAttendanceStatus.BOOKED, + BookingAttendanceStatus.CHECKED_IN, + BookingAttendanceStatus.NO_SHOW, + ).forEachIndexed { index, status -> AttendanceStatusRow( status = status, onClick = { onSelect(status) } @@ -92,12 +96,14 @@ private fun AttendanceStatusRow( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - Icon( - painter = painterResource(status.iconRes), - contentDescription = status.text(), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp) - ) + status.iconRes?.let { iconRes -> + Icon( + painter = painterResource(iconRes), + contentDescription = status.text(), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp) + ) + } Column(modifier = Modifier.weight(1f)) { Text( text = status.text(), @@ -118,16 +124,16 @@ private fun AttendanceStatusRow( private fun BookingAttendanceStatus.description(): String = when (this) { BookingAttendanceStatus.BOOKED -> R.string.booking_attendance_status_booked_desc BookingAttendanceStatus.CHECKED_IN -> R.string.booking_attendance_status_checked_in_desc - BookingAttendanceStatus.CANCELLED -> R.string.booking_attendance_status_cancelled_desc BookingAttendanceStatus.NO_SHOW -> R.string.booking_attendance_status_no_show_desc -}.let { stringResource(it) } + else -> null +}?.let { stringResource(it) } ?: "" -private val BookingAttendanceStatus.iconRes: Int +private val BookingAttendanceStatus.iconRes: Int? get() = when (this) { BookingAttendanceStatus.BOOKED -> R.drawable.ic_attendance_booked BookingAttendanceStatus.CHECKED_IN -> R.drawable.ic_attendance_checked_in - BookingAttendanceStatus.CANCELLED -> R.drawable.ic_attendance_cancelled BookingAttendanceStatus.NO_SHOW -> R.drawable.ic_attendance_no_show + else -> null } @LightDarkThemePreviews diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsScreen.kt index 2c956602e349..aaf256de255e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsScreen.kt @@ -40,6 +40,7 @@ import com.woocommerce.android.ui.bookings.compose.BookingStaffMemberStatus import com.woocommerce.android.ui.bookings.compose.BookingStatus import com.woocommerce.android.ui.bookings.compose.BookingSummary import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel +import com.woocommerce.android.ui.compose.Render import com.woocommerce.android.ui.compose.animations.SkeletonView import com.woocommerce.android.ui.compose.component.Toolbar import com.woocommerce.android.ui.compose.component.WCPullToRefreshBox @@ -106,14 +107,15 @@ fun BookingDetailsScreen( } } } - } - if (showAttendanceSheet.value) { - BookingAttendanceStatusBottomSheet( - onSelect = { status -> - viewState.onAttendanceStatusSelected(status) - }, - onDismiss = { showAttendanceSheet.value = false } - ) + if (showAttendanceSheet.value) { + BookingAttendanceStatusBottomSheet( + onSelect = { status -> + viewState.onAttendanceStatusSelected(status) + }, + onDismiss = { showAttendanceSheet.value = false } + ) + } + viewState.dialogState?.Render() } } @@ -229,7 +231,8 @@ private fun BookingDetailsPreview() { staff = BookingStaffMemberStatus.Loaded("Marianne Renoir"), location = "238 Willow Creek Drive, Montgomery AL 36109", duration = "60 min", - price = "$55.00" + price = "$55.00", + cancelStatus = CancelStatus.Idle, ), bookingCustomerDetails = BookingCustomerDetailsModel( name = "Margarita Nikolaevna", diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt index 36c612b80bad..cfd2c404bbb1 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import com.woocommerce.android.R +import com.woocommerce.android.model.UiString import com.woocommerce.android.tools.NetworkStatus import com.woocommerce.android.ui.bookings.Booking import com.woocommerce.android.ui.bookings.BookingMapper @@ -12,6 +13,7 @@ import com.woocommerce.android.ui.bookings.BookingResource import com.woocommerce.android.ui.bookings.BookingsRepository import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus import com.woocommerce.android.ui.bookings.compose.BookingStaffMemberStatus +import com.woocommerce.android.ui.compose.DialogState import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.viewmodel.ResourceProvider import com.woocommerce.android.viewmodel.ScopedViewModel @@ -20,6 +22,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -28,6 +31,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch +import java.time.Duration import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @@ -54,25 +58,62 @@ class BookingDetailsViewModel @Inject constructor( // Temporary, the booking status should come from the stored object private val bookingAttendanceStatus = MutableStateFlow(null) - val state: LiveData = combine( + private val cancelStatusState = MutableStateFlow(CancelStatus.Idle) + private val showCancelBookingDialog = MutableStateFlow(false) + + private val cancelBookingDialogState = combine( + booking, + showCancelBookingDialog, + ) { booking, showCancelBooking -> + if (showCancelBooking && booking != null) { + val message = bookingMapper.buildCancelDialogMessage(booking) + DialogState( + title = UiString.UiStringRes(R.string.booking_cancel_dialog_title), + message = message, + positiveButton = DialogState.DialogButton( + text = UiString.UiStringRes(R.string.booking_cancel_dialog_confirm), + onClick = ::onConfirmCancelBooking + ), + negativeButton = DialogState.DialogButton( + text = UiString.UiStringRes(R.string.booking_cancel_dialog_keep), + onClick = ::onDismissCancelDialog + ), + ) + } else { + null + } + } + + private val bookingUiStateFlow = combine( booking, bookingAttendanceStatus, loadingState, - resource - ) { booking, attendanceStatus, loadingState, resource -> + resource, + cancelStatusState, + ) { booking, attendanceStatus, loadingState, resource, cancelStatus -> + if (booking != null) { + bookingMapper.buildBookingUiState(booking, attendanceStatus, resource, loadingState, cancelStatus) + } else { + null + } + } + + val state: LiveData = combine( + booking, + bookingUiStateFlow, + loadingState, + cancelBookingDialogState, + ) { booking, bookingUiState, loadingState, cancelBookingDialog -> with(bookingMapper) { BookingDetailsViewState( toolbarTitle = booking?.id?.value?.let { id -> resourceProvider.getString(R.string.booking_details_title, id) } ?: "", - bookingUiState = if (booking != null) { - buildBookingUiState(booking, resource, attendanceStatus, loadingState) - } else { - null - }, - loadingState = loadingState, + bookingUiState = bookingUiState, onCancelBooking = ::onCancelBooking, onAttendanceStatusSelected = ::onAttendanceStatusSelected, + dialogState = cancelBookingDialog, + loadingState = loadingState, onRefresh = ::fetchBooking, ) } @@ -116,14 +157,27 @@ class BookingDetailsViewModel @Inject constructor( } private fun onCancelBooking() { - // TODO Add logic to Cancel booking + showCancelBookingDialog.value = true + } + + private fun onDismissCancelDialog() { + showCancelBookingDialog.value = false + } + + private fun onConfirmCancelBooking() = launch { + // TODO Add logic to Cancel booking action + showCancelBookingDialog.value = false + cancelStatusState.value = CancelStatus.InProgress + delay(Duration.ofSeconds(1).toMillis()) + cancelStatusState.value = CancelStatus.Idle } private suspend fun BookingMapper.buildBookingUiState( booking: Booking, - resource: BookingResource?, attendanceStatus: BookingAttendanceStatus?, - loadingState: BookingDetailsLoadingState + resource: BookingResource?, + loadingState: BookingDetailsLoadingState, + cancelStatus: CancelStatus, ): BookingUiState = BookingUiState( orderId = booking.orderId, bookingSummary = booking.toBookingSummaryModel().let { @@ -138,7 +192,8 @@ class BookingDetailsViewModel @Inject constructor( resourceId = booking.resourceId, resource = resource, loadingState = loadingState - ) + ), + cancelStatus = cancelStatus ), bookingCustomerDetails = booking.order.customerInfo.toCustomerDetailsModel(), bookingPaymentDetails = booking.order.paymentInfo?.toPaymentDetailsModel(booking.currency) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewState.kt index b8ee59551ceb..8f57c73d1c4e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewState.kt @@ -5,6 +5,7 @@ import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus import com.woocommerce.android.ui.bookings.compose.BookingCustomerDetailsModel import com.woocommerce.android.ui.bookings.compose.BookingPaymentDetailsModel import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel +import com.woocommerce.android.ui.compose.DialogState sealed interface BookingDetailsLoadingState { data object Idle : BookingDetailsLoadingState @@ -18,6 +19,7 @@ data class BookingDetailsViewState( val loadingState: BookingDetailsLoadingState = BookingDetailsLoadingState.Idle, val onCancelBooking: () -> Unit = {}, val onAttendanceStatusSelected: (BookingAttendanceStatus) -> Unit = { _ -> }, + val dialogState: DialogState? = null, val onRefresh: () -> Unit = {}, ) { val shouldShowSkeleton: Boolean = bookingUiState == null && loadingState == BookingDetailsLoadingState.Refreshing @@ -30,3 +32,8 @@ data class BookingUiState( val bookingCustomerDetails: BookingCustomerDetailsModel, val bookingPaymentDetails: BookingPaymentDetailsModel?, ) + +sealed interface CancelStatus { + data object Idle : CancelStatus + data object InProgress : CancelStatus +} diff --git a/WooCommerce/src/main/res/drawable/ic_attendance_cancelled.xml b/WooCommerce/src/main/res/drawable/ic_attendance_cancelled.xml deleted file mode 100644 index 058c63641342..000000000000 --- a/WooCommerce/src/main/res/drawable/ic_attendance_cancelled.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index d383dfcd3998..e7e0d642b324 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -4228,7 +4228,6 @@ Update attendance status The appointment is scheduled but hasn’t happened yet. The customer arrived and the session took place as planned. - The client will no longer be able to attend. The client missed the appointment without canceling in advance. PAYMENT Service @@ -4262,4 +4261,8 @@ Automattic logo Back icon App icon + Cancel booking + %1$s will no longer be able to attend “%2$s” on %3$s at %4$s. + No, keep it + Yes, cancel it diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/BookingMapperTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/BookingMapperTest.kt index 0cea01bf4a90..ee1081feb77f 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/BookingMapperTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/BookingMapperTest.kt @@ -1,9 +1,12 @@ package com.woocommerce.android.ui.bookings +import com.woocommerce.android.R import com.woocommerce.android.model.GetLocations +import com.woocommerce.android.model.UiString import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus import com.woocommerce.android.ui.bookings.compose.BookingStaffMemberStatus import com.woocommerce.android.ui.bookings.compose.BookingStatus +import com.woocommerce.android.ui.bookings.details.CancelStatus import com.woocommerce.android.util.CurrencyFormatter import com.woocommerce.android.viewmodel.BaseUnitTest import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -98,7 +101,7 @@ class BookingMapperTest : BaseUnitTest() { val expectedTime = "${timeFormatter.format(start)} - ${timeFormatter.format(end)}" // WHEN - val model = mapper.run { booking.toAppointmentDetailsModel(staffMemberStatus) } + val model = mapper.run { booking.toAppointmentDetailsModel(staffMemberStatus, CancelStatus.Idle) } // THEN assertThat(model.date).isEqualTo(expectedDate) @@ -107,6 +110,7 @@ class BookingMapperTest : BaseUnitTest() { assertThat(model.location).isEqualTo("238 Willow Creek Drive, Montgomery AL 36109") assertThat(model.duration).isEqualTo("90 min") assertThat(model.price).isEqualTo("$55.00") + assertThat(model.cancelStatus).isEqualTo(CancelStatus.Idle) } @Test @@ -169,12 +173,70 @@ class BookingMapperTest : BaseUnitTest() { assertThat(model.total).isEqualTo("$110.00") } + @Test + fun `given booking, when building cancel dialog message, then formats using booking details`() { + // GIVEN + val start = Instant.parse("2025-09-12T16:00:00Z") + val booking = sampleBooking(start = start, end = start.plus(Duration.ofHours(1))) + val expectedDate = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withZone(ZoneOffset.UTC).format(start) + val expectedTime = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneOffset.UTC).format(start) + + // WHEN + val message = mapper.buildCancelDialogMessage(booking) + + // THEN + val customerName = + "${booking.order.customerInfo?.billingFirstName} ${booking.order.customerInfo?.billingLastName}" + assertThat(message) + .isEqualTo( + UiString.UiStringRes( + R.string.booking_cancel_dialog_message, + listOf( + UiString.UiStringText(customerName), + UiString.UiStringText("${booking.order.productInfo?.name}"), + UiString.UiStringText(expectedDate), + UiString.UiStringText(expectedTime) + ) + ) + ) + } + + @Test + fun `given booking without customer, when building cancel dialog message, then falls back to guest`() { + // GIVEN + val start = Instant.parse("2025-09-12T16:00:00Z") + val booking = sampleBooking(start = start, customerInfo = null) + val expectedDate = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withZone(ZoneOffset.UTC).format(start) + val expectedTime = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneOffset.UTC).format(start) + + // WHEN + val message = mapper.buildCancelDialogMessage(booking) + + // THEN + assertThat(message) + .isEqualTo( + UiString.UiStringRes( + R.string.booking_cancel_dialog_message, + listOf( + UiString.UiStringRes(R.string.customer_detail_guest_customer), + UiString.UiStringText("${booking.order.productInfo?.name}"), + UiString.UiStringText(expectedDate), + UiString.UiStringText(expectedTime) + ) + ) + ) + } + private fun sampleBooking( status: BookingEntity.Status = BookingEntity.Status.Confirmed, start: Instant = Instant.parse("2025-07-05T11:00:00Z"), end: Instant = start.plus(Duration.ofHours(1)), cost: String = "0.00", - currency: String = "USD" + currency: String = "USD", + customerInfo: BookingCustomerInfo? = BookingCustomerInfo( + billingFirstName = "Margarita", + billingLastName = "Nikolaevna" + ) ): BookingEntity { return BookingEntity( id = LocalOrRemoteId.RemoteId(1L), @@ -199,10 +261,7 @@ class BookingMapperTest : BaseUnitTest() { order = BookingOrderInfo( status = "completed", productInfo = BookingProductInfo(name = "Women’s Haircut"), - customerInfo = BookingCustomerInfo( - billingFirstName = "Margarita", - billingLastName = "Nikolaevna" - ) + customerInfo = customerInfo, ) ) } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModelTest.kt index f6f3cde5873e..e092895cb05b 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModelTest.kt @@ -204,6 +204,57 @@ class BookingDetailsViewModelTest : BaseUnitTest() { .isEqualTo(BookingStaffMemberStatus.Unavailable) } + @Test + fun `when onCancelBooking called, then cancel dialog is shown with message`() = testBlocking { + // Given + val savedState = SavedStateHandle(mapOf("bookingId" to 111L)) + val viewModel = createViewModel(savedState) + val state = viewModel.state.getOrAwaitValue() + + // When + state.onCancelBooking() + + // Then + val updated = viewModel.state.getOrAwaitValue() + assertThat(updated.dialogState).isNotNull + } + + @Test + fun `given cancel dialog shown, when onDismissCancelDialog called, then dialog is hidden`() = testBlocking { + // Given + val savedState = SavedStateHandle(mapOf("bookingId" to 222L)) + val viewModel = createViewModel(savedState) + val state = viewModel.state.getOrAwaitValue() + state.onCancelBooking() + val stateWithDialog = viewModel.state.getOrAwaitValue() + assertThat(stateWithDialog.dialogState).isNotNull + + // When + stateWithDialog.dialogState?.negativeButton?.onClick() + + // Then + val updated = viewModel.state.getOrAwaitValue() + assertThat(updated.dialogState).isNull() + } + + @Test + fun `given cancel dialog shown, when onConfirmCancelBooking called, then dialog is hidden`() = testBlocking { + // Given + val savedState = SavedStateHandle(mapOf("bookingId" to 333L)) + val viewModel = createViewModel(savedState) + val state = viewModel.state.getOrAwaitValue() + state.onCancelBooking() + val stateWithDialog = viewModel.state.getOrAwaitValue() + assertThat(stateWithDialog.dialogState).isNotNull() + + // When + stateWithDialog.dialogState?.positiveButton?.onClick() + + // Then + val updated = viewModel.state.getOrAwaitValue() + assertThat(updated.dialogState).isNull() + } + private fun createViewModel( savedState: SavedStateHandle, ): BookingDetailsViewModel {