From 1e6f2e94ada0c8b7cb42f76a1c0cf5925d599427 Mon Sep 17 00:00:00 2001 From: KjellBerlin Date: Tue, 27 Aug 2024 14:52:27 +0300 Subject: [PATCH 1/4] Introduce complex payment statuses --- .../carbonara/core/order/OrderRepository.kt | 6 ++- .../com/carbonara/core/order/OrderService.kt | 51 ++++++++++++++----- .../core/payment/MolliePaymentService.kt | 2 +- .../carbonara/core/payment/PaymentDetails.kt | 8 ++- .../carbonara/core/order/OrderServiceTest.kt | 50 ++++++++++++------ 5 files changed, 86 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/com/carbonara/core/order/OrderRepository.kt b/src/main/kotlin/com/carbonara/core/order/OrderRepository.kt index 2e55717..2bd077c 100644 --- a/src/main/kotlin/com/carbonara/core/order/OrderRepository.kt +++ b/src/main/kotlin/com/carbonara/core/order/OrderRepository.kt @@ -11,6 +11,8 @@ interface OrderRepository: ReactiveMongoRepository { @Query("{'paymentDetails.paymentId': ?0}") fun findFirstByPaymentId(paymentId: String): Mono - @Query("{'auth0UserId': ?0, 'paymentDetails.paid': true}") - fun findAllByAuth0UserIdAndPaid(auth0UserId: String): Flux + @Query("{'auth0UserId': ?0, 'paymentDetails.internalPaymentStatus': { \$in: ?1 }}") + fun findAllByAuth0UserIdAndPaymentStatuses(auth0UserId: String, paymentStatuses: List): Flux + + } diff --git a/src/main/kotlin/com/carbonara/core/order/OrderService.kt b/src/main/kotlin/com/carbonara/core/order/OrderService.kt index 7525833..4e383f8 100644 --- a/src/main/kotlin/com/carbonara/core/order/OrderService.kt +++ b/src/main/kotlin/com/carbonara/core/order/OrderService.kt @@ -1,6 +1,7 @@ package com.carbonara.core.order import be.woutschoovaerts.mollie.data.payment.PaymentStatus +import com.carbonara.core.payment.InternalPaymentStatus import com.carbonara.core.payment.MolliePaymentService import com.carbonara.core.payment.PaymentException import com.carbonara.core.product.ProductDao @@ -53,16 +54,12 @@ class OrderService( paymentId: String ) { val paymentStatus = molliePaymentService.getMolliePaymentStatus(paymentId) - if (paymentStatus == PaymentStatus.PAID) { - val order = retrieveOrderFromDatabase(paymentId) - if (!order.paymentDetails.paid) { - updateOrderToPaid(order, paymentStatus) - - // TODO: trigger delivery - } - } else { - log.info("Retrieved payment status={} for paymentId={}. Not processing order for now further", + when(paymentStatus) { + PaymentStatus.PAID -> handlePaidOrder(paymentId, paymentStatus) + PaymentStatus.CANCELED -> handleUnpaidOrder(paymentId, paymentStatus) + PaymentStatus.FAILED -> handleUnpaidOrder(paymentId, paymentStatus) + else -> log.info("Retrieved payment status={} for paymentId={}. Not processing order for now further", paymentStatus, paymentId) } } @@ -70,8 +67,10 @@ class OrderService( suspend fun getPaidOrdersByAuth0UserId( auth0UserId: String ): List { - return orderRepository.findAllByAuth0UserIdAndPaid(auth0UserId) - .collectList().awaitSingleOrNull()?.map { it.toOrderDto() } ?: emptyList() + return orderRepository.findAllByAuth0UserIdAndPaymentStatuses( + auth0UserId = auth0UserId, + paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name) + ).collectList().awaitSingleOrNull()?.map { it.toOrderDto() } ?: emptyList() } private fun createPaymentDescription(products: List): String { @@ -93,10 +92,25 @@ class OrderService( } } + private suspend fun handlePaidOrder(paymentId: String, paymentStatus: PaymentStatus) { + val order = retrieveOrderFromDatabase(paymentId) + + if (order.paymentDetails.internalPaymentStatus != InternalPaymentStatus.PAID) { + updateOrderToPaid(order, paymentStatus) + + // TODO: trigger delivery + } + } + + private suspend fun handleUnpaidOrder(paymentId: String, paymentStatus: PaymentStatus) { + val order = retrieveOrderFromDatabase(paymentId) + updateOrderToFailed(order, paymentStatus) + } + private suspend fun updateOrderToPaid(order: OrderDao, paymentStatus: PaymentStatus) { log.info("Retrieved payment status={} for orderId={}, now processing order", paymentStatus, order.orderId) val updatedOrder = order.copy( - paymentDetails = order.paymentDetails.copy(paid = true), + paymentDetails = order.paymentDetails.copy(internalPaymentStatus = InternalPaymentStatus.PAID), updatedAt = OffsetDateTime.now().toString(), orderStatus = OrderStatus.PROCESSING_ORDER ) @@ -106,6 +120,19 @@ class OrderService( } } + private suspend fun updateOrderToFailed(order: OrderDao, paymentStatus: PaymentStatus) { + log.info("Retrieved payment status={} for orderId={}, now processing order", paymentStatus, order.orderId) + val updatedOrder = order.copy( + paymentDetails = order.paymentDetails.copy(internalPaymentStatus = InternalPaymentStatus.FAILED), + updatedAt = OffsetDateTime.now().toString(), + orderStatus = OrderStatus.CANCELLED + ) + orderRepository.save(updatedOrder).awaitSingleOrNull() ?: run { + log.error("Failed to update payment status to failed for orderId={}", order.orderId) + throw PaymentException("Failed to update payment status") + } + } + companion object { private val log = KotlinLogging.logger {} } diff --git a/src/main/kotlin/com/carbonara/core/payment/MolliePaymentService.kt b/src/main/kotlin/com/carbonara/core/payment/MolliePaymentService.kt index 13d3d78..955e265 100644 --- a/src/main/kotlin/com/carbonara/core/payment/MolliePaymentService.kt +++ b/src/main/kotlin/com/carbonara/core/payment/MolliePaymentService.kt @@ -51,7 +51,7 @@ class MolliePaymentService { PaymentDetails( paymentId = molliePayment.id, paymentRedirectLink = molliePayment.links.checkout.href, - paid = false + internalPaymentStatus = InternalPaymentStatus.PENDING ) } catch (ex: MollieException) { log.error("Failed to create payment for user={} and amount in cents={}, details={}", userId, amountInCents, ex.details) diff --git a/src/main/kotlin/com/carbonara/core/payment/PaymentDetails.kt b/src/main/kotlin/com/carbonara/core/payment/PaymentDetails.kt index fd9602d..0e67796 100644 --- a/src/main/kotlin/com/carbonara/core/payment/PaymentDetails.kt +++ b/src/main/kotlin/com/carbonara/core/payment/PaymentDetails.kt @@ -3,5 +3,11 @@ package com.carbonara.core.payment data class PaymentDetails( val paymentRedirectLink: String, val paymentId: String, - val paid: Boolean + val internalPaymentStatus: InternalPaymentStatus, ) + +enum class InternalPaymentStatus { + PENDING, + PAID, + FAILED +} diff --git a/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt b/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt index 6d3a3e6..b72ff00 100644 --- a/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt +++ b/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt @@ -2,6 +2,7 @@ package com.carbonara.core.order import be.woutschoovaerts.mollie.data.payment.PaymentStatus import com.carbonara.core.address.Address +import com.carbonara.core.payment.InternalPaymentStatus import com.carbonara.core.payment.PaymentDetails import com.carbonara.core.payment.MolliePaymentService import com.carbonara.core.payment.PaymentException @@ -49,7 +50,7 @@ class OrderServiceTest { fun `Happy case`() { coEvery { productService.getProductDaosByIds(any()) } returns listOf(TEST_PRODUCT) every { orderRepository.save(any()) } returns ORDER_DAO.toMono() - every { molliePaymentService.createMolliePaymentLink(any(), any(), any()) } returns MOLLIE_PAYMENT_DETAILS + every { molliePaymentService.createMolliePaymentLink(any(), any(), any()) } returns PAYMENT_DETAILS val result = runBlocking { orderService.createOrder(CREATE_ORDER_INPUT) } assertEquals(ORDER_DAO.toOrderDto(), result) @@ -68,7 +69,7 @@ class OrderServiceTest { fun `Null return form database`() { coEvery { productService.getProductDaosByIds(any()) } returns listOf(TEST_PRODUCT) every { orderRepository.save(any()) } returns null.toMono() - every { molliePaymentService.createMolliePaymentLink(any(), any(), any()) } returns MOLLIE_PAYMENT_DETAILS + every { molliePaymentService.createMolliePaymentLink(any(), any(), any()) } returns PAYMENT_DETAILS assertThrows { runBlocking { orderService.createOrder(CREATE_ORDER_INPUT) } @@ -105,13 +106,13 @@ class OrderServiceTest { fun `Status not paid`() { every { molliePaymentService.getMolliePaymentStatus(any()) } returns PaymentStatus.FAILED coEvery { orderRepository.findFirstByPaymentId(any()) } returns ORDER_DAO.toMono() - coEvery { orderRepository.save(any()) } returns ORDER_DAO_PAID.toMono() + coEvery { orderRepository.save(any()) } returns ORDER_DAO_PAYMENT_FAILED.toMono() runBlocking { orderService.handleOrderPayment(PAYMENT_ID) } verify(exactly = 1) { molliePaymentService.getMolliePaymentStatus(PAYMENT_ID) } - coVerify(exactly = 0) { orderRepository.findFirstByPaymentId(PAYMENT_ID)} - coVerify(exactly = 0) { orderRepository.save(ORDER_DAO_PAID) } + coVerify(exactly = 1) { orderRepository.findFirstByPaymentId(PAYMENT_ID)} + coVerify(exactly = 1) { orderRepository.save(ORDER_DAO_PAYMENT_FAILED) } } @Test @@ -144,26 +145,38 @@ class OrderServiceTest { } @Nested - inner class GetPaidOrdersByAuth0UserIdTests { + inner class GetPaidAndFailedOrdersByAuth0UserIdTests { @Test fun `Happy case`() { - coEvery { orderRepository.findAllByAuth0UserIdAndPaid(AUTH0_USER_ID) } returns listOf(ORDER_DAO).toFlux() + coEvery { orderRepository.findAllByAuth0UserIdAndPaymentStatuses( + auth0UserId = AUTH0_USER_ID, + paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name) + ) } returns listOf(ORDER_DAO_PAID, ORDER_DAO_PAYMENT_FAILED).toFlux() val result = runBlocking { orderService.getPaidOrdersByAuth0UserId(AUTH0_USER_ID) } - assertEquals(listOf(ORDER_DAO.toOrderDto()), result) + assertEquals(listOf(ORDER_DAO_PAID.toOrderDto(), ORDER_DAO_PAYMENT_FAILED.toOrderDto()), result) - coVerify(exactly = 1) { orderRepository.findAllByAuth0UserIdAndPaid(AUTH0_USER_ID) } + coVerify(exactly = 1) { orderRepository.findAllByAuth0UserIdAndPaymentStatuses( + auth0UserId = AUTH0_USER_ID, + paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name) + ) } } @Test fun `No orders found`() { - coEvery { orderRepository.findAllByAuth0UserIdAndPaid(AUTH0_USER_ID) } returns emptyList().toFlux() + coEvery { orderRepository.findAllByAuth0UserIdAndPaymentStatuses( + auth0UserId = AUTH0_USER_ID, + paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name) + ) } returns emptyList().toFlux() val result = runBlocking { orderService.getPaidOrdersByAuth0UserId(AUTH0_USER_ID) } assertEquals(emptyList(), result) - coVerify(exactly = 1) { orderRepository.findAllByAuth0UserIdAndPaid(AUTH0_USER_ID) } + coVerify(exactly = 1) { orderRepository.findAllByAuth0UserIdAndPaymentStatuses( + auth0UserId = AUTH0_USER_ID, + paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name) + ) } } } @@ -197,10 +210,10 @@ class OrderServiceTest { productsIds = listOf(PRODUCT_ID.toString()), additionalDetails = "No additional details" ) - val MOLLIE_PAYMENT_DETAILS = PaymentDetails( + val PAYMENT_DETAILS = PaymentDetails( paymentId = PAYMENT_ID, paymentRedirectLink = "https://example.com", - paid = false + internalPaymentStatus = InternalPaymentStatus.PENDING ) val ORDER_DAO = OrderDao( orderId = ObjectId(), @@ -209,11 +222,18 @@ class OrderServiceTest { deliveryAddress = CREATE_ORDER_INPUT.deliveryAddress, products = listOf(TEST_PRODUCT), additionalDetails = CREATE_ORDER_INPUT.additionalDetails, - paymentDetails = MOLLIE_PAYMENT_DETAILS, + paymentDetails = PAYMENT_DETAILS, orderStatus = OrderStatus.PROCESSING_ORDER, createdAt = TIME.toString(), updatedAt = TIME.toString() ) - val ORDER_DAO_PAID = ORDER_DAO.copy(paymentDetails = ORDER_DAO.paymentDetails.copy(paid = true)) + val ORDER_DAO_PAID = ORDER_DAO.copy( + paymentDetails = ORDER_DAO.paymentDetails.copy(internalPaymentStatus = InternalPaymentStatus.PAID) + ) + val ORDER_DAO_PAYMENT_FAILED = ORDER_DAO.copy( + paymentDetails = ORDER_DAO.paymentDetails.copy( + internalPaymentStatus = InternalPaymentStatus.FAILED), + orderStatus = OrderStatus.CANCELLED + ) } } From 63d16f68d3d2b172221df09e0549a472c6b284a7 Mon Sep 17 00:00:00 2001 From: KjellBerlin Date: Tue, 27 Aug 2024 14:53:54 +0300 Subject: [PATCH 2/4] Trigger delivery only when paid --- src/main/kotlin/com/carbonara/core/order/OrderService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/carbonara/core/order/OrderService.kt b/src/main/kotlin/com/carbonara/core/order/OrderService.kt index 4e383f8..570fb11 100644 --- a/src/main/kotlin/com/carbonara/core/order/OrderService.kt +++ b/src/main/kotlin/com/carbonara/core/order/OrderService.kt @@ -95,7 +95,7 @@ class OrderService( private suspend fun handlePaidOrder(paymentId: String, paymentStatus: PaymentStatus) { val order = retrieveOrderFromDatabase(paymentId) - if (order.paymentDetails.internalPaymentStatus != InternalPaymentStatus.PAID) { + if (order.paymentDetails.internalPaymentStatus == InternalPaymentStatus.PAID) { updateOrderToPaid(order, paymentStatus) // TODO: trigger delivery From ce310ef1817a5142f5f0fd6a16536b2505a43ea9 Mon Sep 17 00:00:00 2001 From: KjellBerlin Date: Tue, 27 Aug 2024 14:55:13 +0300 Subject: [PATCH 3/4] Move variable declaration into when clause --- src/main/kotlin/com/carbonara/core/order/OrderService.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/kotlin/com/carbonara/core/order/OrderService.kt b/src/main/kotlin/com/carbonara/core/order/OrderService.kt index 570fb11..2ab3f56 100644 --- a/src/main/kotlin/com/carbonara/core/order/OrderService.kt +++ b/src/main/kotlin/com/carbonara/core/order/OrderService.kt @@ -53,9 +53,7 @@ class OrderService( suspend fun handleOrderPayment( paymentId: String ) { - val paymentStatus = molliePaymentService.getMolliePaymentStatus(paymentId) - - when(paymentStatus) { + when(val paymentStatus = molliePaymentService.getMolliePaymentStatus(paymentId)) { PaymentStatus.PAID -> handlePaidOrder(paymentId, paymentStatus) PaymentStatus.CANCELED -> handleUnpaidOrder(paymentId, paymentStatus) PaymentStatus.FAILED -> handleUnpaidOrder(paymentId, paymentStatus) From ae798c96cf3c297183c6cad86012496e93cdcf79 Mon Sep 17 00:00:00 2001 From: KjellBerlin Date: Wed, 28 Aug 2024 12:23:51 +0300 Subject: [PATCH 4/4] Move variable declaration into when clause --- src/main/kotlin/com/carbonara/core/order/OrderQuery.kt | 2 +- src/main/kotlin/com/carbonara/core/order/OrderService.kt | 2 +- .../kotlin/com/carbonara/core/order/OrderServiceTest.kt | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/carbonara/core/order/OrderQuery.kt b/src/main/kotlin/com/carbonara/core/order/OrderQuery.kt index ca16209..3dfa68e 100644 --- a/src/main/kotlin/com/carbonara/core/order/OrderQuery.kt +++ b/src/main/kotlin/com/carbonara/core/order/OrderQuery.kt @@ -11,6 +11,6 @@ class OrderQuery( @GraphQLDescription("Get all paid orders of an user") suspend fun paidOrders(userId: String): List { - return orderService.getPaidOrdersByAuth0UserId(userId) + return orderService.getNonPendingOrdersByAuth0UserId(userId) } } diff --git a/src/main/kotlin/com/carbonara/core/order/OrderService.kt b/src/main/kotlin/com/carbonara/core/order/OrderService.kt index 2ab3f56..86e9071 100644 --- a/src/main/kotlin/com/carbonara/core/order/OrderService.kt +++ b/src/main/kotlin/com/carbonara/core/order/OrderService.kt @@ -62,7 +62,7 @@ class OrderService( } } - suspend fun getPaidOrdersByAuth0UserId( + suspend fun getNonPendingOrdersByAuth0UserId( auth0UserId: String ): List { return orderRepository.findAllByAuth0UserIdAndPaymentStatuses( diff --git a/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt b/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt index b72ff00..b683ee8 100644 --- a/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt +++ b/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt @@ -145,7 +145,7 @@ class OrderServiceTest { } @Nested - inner class GetPaidAndFailedOrdersByAuth0UserIdTests { + inner class GetNonPendingOrdersByAuth0UserIdTests { @Test fun `Happy case`() { @@ -154,7 +154,7 @@ class OrderServiceTest { paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name) ) } returns listOf(ORDER_DAO_PAID, ORDER_DAO_PAYMENT_FAILED).toFlux() - val result = runBlocking { orderService.getPaidOrdersByAuth0UserId(AUTH0_USER_ID) } + val result = runBlocking { orderService.getNonPendingOrdersByAuth0UserId(AUTH0_USER_ID) } assertEquals(listOf(ORDER_DAO_PAID.toOrderDto(), ORDER_DAO_PAYMENT_FAILED.toOrderDto()), result) coVerify(exactly = 1) { orderRepository.findAllByAuth0UserIdAndPaymentStatuses( @@ -170,7 +170,7 @@ class OrderServiceTest { paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name) ) } returns emptyList().toFlux() - val result = runBlocking { orderService.getPaidOrdersByAuth0UserId(AUTH0_USER_ID) } + val result = runBlocking { orderService.getNonPendingOrdersByAuth0UserId(AUTH0_USER_ID) } assertEquals(emptyList(), result) coVerify(exactly = 1) { orderRepository.findAllByAuth0UserIdAndPaymentStatuses(