From 8c107fa177f015a953f3d65fa3c256560c6e8454 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Thu, 5 Sep 2024 13:00:16 +0200 Subject: [PATCH] WR-377: Prevent duplicate order creation on request timeout --- shared/localization.yml | 3 +++ .../waiterrobot/shared/utils/UUID.android.kt | 5 +++++ .../waiterrobot/shared/core/data/api/ApiException.kt | 4 ++++ .../features/order/api/models/OrderRequestDto.kt | 3 ++- .../features/order/repository/OrderRepository.kt | 4 ++-- .../shared/features/order/viewmodel/OrderViewModel.kt | 10 +++++++++- .../waiterrobot/shared/utils/ExceptionMessageMapper.kt | 2 ++ .../datepollsystems/waiterrobot/shared/utils/UUID.kt | 3 +++ .../waiterrobot/shared/utils/UUID.ios.kt | 5 +++++ 9 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 shared/src/androidMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.android.kt create mode 100644 shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.kt create mode 100644 shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.ios.kt diff --git a/shared/localization.yml b/shared/localization.yml index 694b6a7..6ed736d 100644 --- a/shared/localization.yml +++ b/shared/localization.yml @@ -179,6 +179,9 @@ order: inputPlaceholder: de: 1x ohne Tomaten en: 1x without tomato + alreadyCreated: + en: Order was already sent. + de: Bestellung wurde bereits gesendet. couldNotFindProduct: title: de: Produkt nicht gefunden. diff --git a/shared/src/androidMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.android.kt b/shared/src/androidMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.android.kt new file mode 100644 index 0000000..8118ad0 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.android.kt @@ -0,0 +1,5 @@ +package org.datepollsystems.waiterrobot.shared.utils + +import java.util.UUID + +actual fun randomUUID(): String = UUID.randomUUID().toString() diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/data/api/ApiException.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/data/api/ApiException.kt index ed33f4c..deb05a3 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/data/api/ApiException.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/data/api/ApiException.kt @@ -85,4 +85,8 @@ internal sealed class ApiException : Exception() { @Serializable @SerialName("STRIPE_DISABLED") class StripeDisabled : ApiException() + + @Serializable + @SerialName("ORDER_ALREADY_SUBMITTED") + class OrderAlreadySubmitted : ApiException() } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/OrderRequestDto.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/OrderRequestDto.kt index 824bfbb..7bfcac4 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/OrderRequestDto.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/OrderRequestDto.kt @@ -6,7 +6,8 @@ import org.datepollsystems.waiterrobot.shared.core.data.api.RequestBodyDto @Serializable class OrderRequestDto( val tableId: Long, - val products: List + val products: List, + val clientOrderId: String, ) : RequestBodyDto { @Serializable class OrderItemDto( diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/repository/OrderRepository.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/repository/OrderRepository.kt index 4847f12..0473ca0 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/repository/OrderRepository.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/repository/OrderRepository.kt @@ -8,8 +8,8 @@ import org.datepollsystems.waiterrobot.shared.features.table.models.Table internal class OrderRepository(private val orderApi: OrderApi) : AbstractRepository() { - suspend fun sendOrder(table: Table, order: List) { + suspend fun sendOrder(table: Table, order: List, orderId: String) { val items = order.map { OrderRequestDto.OrderItemDto(it.product.id, it.amount, it.note) } - orderApi.sendOrder(OrderRequestDto(table.id, items)) + orderApi.sendOrder(OrderRequestDto(table.id, items, orderId)) } } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderViewModel.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderViewModel.kt index 556d2f9..82b55d5 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderViewModel.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderViewModel.kt @@ -16,6 +16,7 @@ import org.datepollsystems.waiterrobot.shared.generated.localization.L import org.datepollsystems.waiterrobot.shared.generated.localization.desc import org.datepollsystems.waiterrobot.shared.generated.localization.descOrderSent import org.datepollsystems.waiterrobot.shared.utils.extensions.emptyToNull +import org.datepollsystems.waiterrobot.shared.utils.randomUUID import org.orbitmvi.orbit.syntax.simple.SimpleContext import org.orbitmvi.orbit.syntax.simple.SimpleSyntax import org.orbitmvi.orbit.syntax.simple.intent @@ -29,6 +30,8 @@ class OrderViewModel internal constructor( private val initialItemId: Long? ) : AbstractViewModel(OrderState()) { + private var currentOrderId = randomUUID() + override suspend fun SimpleSyntax>.onCreate() { coroutineScope { launch { productRepository.listen() } @@ -99,7 +102,8 @@ class OrderViewModel internal constructor( } try { - orderRepository.sendOrder(table, order) + orderRepository.sendOrder(table, order, currentOrderId) + currentOrderId = randomUUID() reduce { state.copy(_currentOrder = Resource.Success(emptyMap())) } navigator.popUpTo(Screen.TableDetailScreen(table), inclusive = false) @@ -113,6 +117,10 @@ class OrderViewModel internal constructor( } else { reduce { stockToLow(stockToLowProduct, e.remaining) } } + } catch (_: ApiException.OrderAlreadySubmitted) { + logger.w("Order was already submitted") + reduce { state.copy(_currentOrder = Resource.Success(emptyMap())) } + navigator.popUpTo(Screen.TableDetailScreen(table), inclusive = false) } } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/ExceptionMessageMapper.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/ExceptionMessageMapper.kt index d7e4565..40d0df0 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/ExceptionMessageMapper.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/ExceptionMessageMapper.kt @@ -6,6 +6,7 @@ import org.datepollsystems.waiterrobot.shared.core.data.api.ApiException import org.datepollsystems.waiterrobot.shared.core.di.getLogger import org.datepollsystems.waiterrobot.shared.generated.localization.L import org.datepollsystems.waiterrobot.shared.generated.localization.accountNotActivated +import org.datepollsystems.waiterrobot.shared.generated.localization.alreadyCreated import org.datepollsystems.waiterrobot.shared.generated.localization.amountToLow import org.datepollsystems.waiterrobot.shared.generated.localization.desc import org.datepollsystems.waiterrobot.shared.generated.localization.disabled @@ -38,6 +39,7 @@ internal fun ApiException.getLocalizedUserMessage(): String = when (this) { is ApiException.StripeDisabled -> L.stripeInit.error.disabled() is ApiException.StripeNotActivated -> L.stripeInit.error.disabledForEvent() is ApiException.ProductStockToLow -> L.order.stockToLow.title() + is ApiException.OrderAlreadySubmitted -> L.order.alreadyCreated() // Unknown exceptions or exceptions that should normally not happen is ApiException.Generic, diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.kt new file mode 100644 index 0000000..64054d6 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.kt @@ -0,0 +1,3 @@ +package org.datepollsystems.waiterrobot.shared.utils + +expect fun randomUUID(): String diff --git a/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.ios.kt b/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.ios.kt new file mode 100644 index 0000000..4f223e7 --- /dev/null +++ b/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/UUID.ios.kt @@ -0,0 +1,5 @@ +package org.datepollsystems.waiterrobot.shared.utils + +import platform.Foundation.NSUUID + +actual fun randomUUID(): String = NSUUID().UUIDString