From 0d2841e768525d6cb948e0185a7ba5f3d61b860f Mon Sep 17 00:00:00 2001 From: KjellBerlin Date: Fri, 20 Sep 2024 16:53:42 +0200 Subject: [PATCH] Update slack message after user interaction --- .../com/carbonara/core/helper/OrderFactory.kt | 71 ++++++++++++++ .../com/carbonara/core/order/OrderService.kt | 20 ++-- .../core/slack/SlackMessageService.kt | 95 +++++++++++++++---- .../com/carbonara/core/slack/SlackService.kt | 22 ++++- .../core/slack/SlackWebhookController.kt | 12 ++- .../carbonara/core/order/OrderServiceTest.kt | 7 +- .../carbonara/core/slack/SlackServiceTests.kt | 24 ++++- 7 files changed, 215 insertions(+), 36 deletions(-) create mode 100644 src/main/kotlin/com/carbonara/core/helper/OrderFactory.kt diff --git a/src/main/kotlin/com/carbonara/core/helper/OrderFactory.kt b/src/main/kotlin/com/carbonara/core/helper/OrderFactory.kt new file mode 100644 index 0000000..1d04880 --- /dev/null +++ b/src/main/kotlin/com/carbonara/core/helper/OrderFactory.kt @@ -0,0 +1,71 @@ +package com.carbonara.core.helper + +import com.carbonara.core.address.Address +import com.carbonara.core.order.OrderDao +import com.carbonara.core.order.OrderStatus +import com.carbonara.core.payment.InternalPaymentStatus +import com.carbonara.core.payment.PaymentDetails +import com.carbonara.core.product.ProductDao +import org.bson.types.ObjectId + +fun createOrderDao( + auth0UserId: String = "auth0Id1", + userName: String = "Mr Bean", + deliveryAddress: Address = createDeliveryAddress(), + products: List = listOf(createProduct()), + additionalDetails: String = "", + paymentDetails: PaymentDetails = createPaymentDetails(), + orderStatus: OrderStatus = OrderStatus.FINDING_AVAILABLE_RIDER, + createdAt: String = "2024-06-01T14:00:00.0+02:00", + updatedAt: String = "2024-06-01T14:00:00.0+02:00" +): OrderDao { + return OrderDao( + orderId = ObjectId(), + auth0UserId = auth0UserId, + userName = userName, + deliveryAddress = deliveryAddress, + products = products, + additionalDetails = additionalDetails, + paymentDetails = paymentDetails, + orderStatus = orderStatus, + createdAt = createdAt, + updatedAt = updatedAt + ) +} + +fun createProduct( + productId: ObjectId = ObjectId() +): ProductDao { + return ProductDao( + productId = productId, + productName = "test-product-1", + productPrice = 1000, + productPictureUrl = "https://example.com", + isActive = true, + shortProductDescription = "Short description", + longProductDescription = "Long description" + ) +} + +fun createDeliveryAddress(): Address { + return Address( + name = "John Watson", + street = "Baker Street", + streetNumber = "221B", + postCode = "123", + city = "London", + country = "Germany", + googlePlaceId = "sample_google_place_id" + ) +} + +fun createPaymentDetails( + paymentId: String = "tr_123", + internalPaymentStatus: InternalPaymentStatus = InternalPaymentStatus.PENDING +): PaymentDetails { + return PaymentDetails( + paymentId = paymentId, + paymentRedirectLink = "https://example.com", + internalPaymentStatus = internalPaymentStatus + ) +} diff --git a/src/main/kotlin/com/carbonara/core/order/OrderService.kt b/src/main/kotlin/com/carbonara/core/order/OrderService.kt index 21dee95..4746d8f 100644 --- a/src/main/kotlin/com/carbonara/core/order/OrderService.kt +++ b/src/main/kotlin/com/carbonara/core/order/OrderService.kt @@ -8,6 +8,7 @@ import com.carbonara.core.payment.InternalPaymentStatus import com.carbonara.core.payment.MolliePaymentService import com.carbonara.core.product.ProductDao import com.carbonara.core.product.ProductService +import com.carbonara.core.slack.SlackMessageParams import com.carbonara.core.slack.SlackMessageService import kotlinx.coroutines.reactor.awaitSingleOrNull import mu.KotlinLogging @@ -78,20 +79,21 @@ class OrderService( suspend fun updateOrderStatus( orderId: String, orderStatus: OrderStatus - ) { + ): OrderDao { val order = orderRepository.findById(ObjectId(orderId)).awaitSingleOrNull() ?: run { log.error("Failed to retrieve order for orderId=$orderId") throw OrderNotFoundException("Failed to retrieve order with orderId=$orderId") } - val updatedOrder = order.copy( + val orderUpdate = order.copy( orderStatus = orderStatus, updatedAt = OffsetDateTime.now().toString() ) - orderRepository.save(updatedOrder).awaitSingleOrNull() ?: run { + val updatedOrder = orderRepository.save(orderUpdate).awaitSingleOrNull() ?: run { log.error("Failed to update order status for orderId=$orderId") throw OrderUpdateException("Failed to update order status for orderId=$orderId") } log.info("Updated order status to $orderStatus for orderId=$orderId") + return updatedOrder } private fun createPaymentDescription(products: List): String { @@ -120,11 +122,13 @@ class OrderService( updateOrderToPaid(order, paymentStatus) slackMessageService.sendNewOrderMessage( - customerName = order.userName, - orderId = order.orderId.toString(), - address = order.deliveryAddress.toString(), - googleMapsLink = order.deliveryAddress.createGoogleMapsLink(), - productNames = order.products.map { it.productName } + SlackMessageParams( + customerName = order.userName, + orderId = order.orderId.toString(), + address = order.deliveryAddress.toString(), + googleMapsLink = order.deliveryAddress.createGoogleMapsLink(), + productNames = order.products.map { it.productName } + ) ) } } diff --git a/src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt b/src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt index d02fda7..68932be 100644 --- a/src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt +++ b/src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt @@ -1,5 +1,6 @@ package com.carbonara.core.slack +import com.carbonara.core.order.OrderStatus import com.slack.api.Slack import com.slack.api.methods.kotlin_extension.request.chat.blocks import mu.KotlinLogging @@ -10,17 +11,13 @@ import org.springframework.stereotype.Service class SlackMessageService { @Value("\${slack.token}") - lateinit var slackToken: String + private lateinit var slackToken: String @Value("\${slack.channel}") - lateinit var slackChannel: String + private lateinit var slackChannel: String fun sendNewOrderMessage( - customerName: String, - orderId: String, - address: String, - googleMapsLink: String, - productNames: List + params: SlackMessageParams ) { val slack = Slack.getInstance() @@ -29,39 +26,39 @@ class SlackMessageService { .blocks { section { fields { - markdownText("*Customer Name:*\n$customerName") - markdownText("*OrderId:*\n$orderId") + markdownText("*Customer Name:*\n${params.customerName}") + markdownText("*OrderId:*\n${params.orderId}") } } section { fields { - markdownText("*Address:*\n$address\n$googleMapsLink") - markdownText("*Products:*\n${productNames.joinToString(", ")}") + markdownText("*Address:*\n${params.address}\n${params.googleMapsLink}") + markdownText("*Products:*\n${params.productNames.joinToString(", ")}") } } actions { button { text("ACCEPT", emoji = true) style("primary") - value(orderId) + value(params.orderId) actionId("accept") } button { text("DELIVERY IN PROGRESS", emoji = true) style("primary") - value(orderId) + value(params.orderId) actionId("delivery_in_progress") } button { text("DELIVERED", emoji = true) style("primary") - value(orderId) + value(params.orderId) actionId("delivered") } button { text("CANCELLED", emoji = true) style("danger") - value(orderId) + value(params.orderId) actionId("cancelled") } } @@ -71,11 +68,77 @@ class SlackMessageService { if (!response.isOk) { log.error("Slack API error: ${response.error}") - throw SlackException("Failed to send slack message for orderId: $orderId. Error: ${response.error}") + throw SlackException("Failed to send slack message for orderId: ${params.orderId}. Error: ${response.error}") } } + // TODO: Update depending on actual status + fun updateOrderMessageToAccepted( + params: SlackMessageParams + ) { + + val slack = Slack.getInstance() + val response = slack.methods(slackToken).chatUpdate { req -> req + .channel(slackChannel) + .ts(params.timeStamp) + .blocks { + section { + fields { + markdownText("*Customer Name:*\n${params.customerName}") + markdownText("*OrderId:*\n${params.orderId}") + } + } + section { + fields { + markdownText("*Address:*\n${params.address}\n${params.googleMapsLink}") + markdownText("*Products:*\n${params.productNames.joinToString(", ")}") + } + } + actions { + button { + text("ACCEPT", emoji = true) + style("primary") + value(params.orderId) + actionId("accept") + } + button { + text("DELIVERY IN PROGRESS", emoji = true) + value(params.orderId) + actionId("delivery_in_progress") + } + button { + text("DELIVERED", emoji = true) + value(params.orderId) + actionId("delivered") + } + button { + text("CANCELLED", emoji = true) + value(params.orderId) + actionId("cancelled") + } + } + divider() + } + } + + if (!response.isOk) { + log.error("Slack API error: ${response.error}") + throw SlackException("Failed to update slack message for orderId: ${params.orderId}. Error: ${response.error}") + } + } + + companion object { private val log = KotlinLogging.logger {} } } + +data class SlackMessageParams( + val customerName: String, + val orderId: String, + val address: String, + val googleMapsLink: String, + val productNames: List, + val timeStamp: String? = null, + val orderStatus: OrderStatus? = null +) diff --git a/src/main/kotlin/com/carbonara/core/slack/SlackService.kt b/src/main/kotlin/com/carbonara/core/slack/SlackService.kt index c3a906b..5b1661f 100644 --- a/src/main/kotlin/com/carbonara/core/slack/SlackService.kt +++ b/src/main/kotlin/com/carbonara/core/slack/SlackService.kt @@ -6,16 +6,30 @@ import org.springframework.stereotype.Service @Service class SlackService( - private val orderService: OrderService + private val orderService: OrderService, + private val slackMessageService: SlackMessageService ) { suspend fun handleOrderStatusUpdate( orderId: String, - slackOrderStatus: String + slackOrderStatus: String, + messageTimestamp: String ) { - orderService.updateOrderStatus( + val orderStatus = mapSlackOrderStatusToOrderStatus(slackOrderStatus) + val order = orderService.updateOrderStatus( orderId = orderId, - orderStatus = mapSlackOrderStatusToOrderStatus(slackOrderStatus) + orderStatus = orderStatus + ) + slackMessageService.updateOrderMessageToAccepted( + SlackMessageParams( + customerName = order.userName, + orderId = orderId, + address = order.deliveryAddress.toString(), + googleMapsLink = order.deliveryAddress.createGoogleMapsLink(), + productNames = order.products.map { it.productName }, + timeStamp = messageTimestamp, + orderStatus = orderStatus + ) ) } diff --git a/src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt b/src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt index f0f0028..15ab0ed 100644 --- a/src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt +++ b/src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt @@ -18,7 +18,9 @@ class SlackDeliveryWebhookController( slackPayload.actions.forEach { action -> slackService.handleOrderStatusUpdate( orderId = action.value, - slackOrderStatus = action.action_id) + slackOrderStatus = action.action_id, + messageTimestamp = slackPayload.message.ts + ) } return ResponseEntity.ok().build() @@ -35,7 +37,8 @@ data class SlackWebhookRequestBody( @JsonIgnoreProperties(ignoreUnknown = true) data class SlackPayload( - val actions: List + val actions: List, + val message: SlackMessage ) @JsonIgnoreProperties(ignoreUnknown = true) @@ -43,3 +46,8 @@ data class SlackAction( val action_id: String, val value: String, ) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class SlackMessage( + val ts: String +) diff --git a/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt b/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt index 4b2ffde..0f24c7d 100644 --- a/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt +++ b/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt @@ -46,7 +46,7 @@ class OrderServiceTest { mockkStatic(OffsetDateTime::class) every { OffsetDateTime.now() } returns TIME - every { slackMessageService.sendNewOrderMessage(any(), any(), any(), any(), any()) } returns Unit + every { slackMessageService.sendNewOrderMessage(any()) } returns Unit } @Nested @@ -196,8 +196,10 @@ class OrderServiceTest { coEvery { orderRepository.findById(ORDER_DAO_PAID.orderId) } returns ORDER_DAO_PAID.toMono() coEvery { orderRepository.save(any()) } returns deliveredOrder.toMono() - runBlocking { orderService.updateOrderStatus(ORDER_DAO_PAID.orderId.toString(), OrderStatus.DELIVERED) } + val orderDao = + runBlocking { orderService.updateOrderStatus(ORDER_DAO_PAID.orderId.toString(), OrderStatus.DELIVERED) } + assertEquals(ORDER_DAO_DELIVERED, orderDao) coVerify(exactly = 1) { orderRepository.findById(ORDER_DAO_PAID.orderId) } coVerify(exactly = 1) { orderRepository.save(deliveredOrder) } } @@ -285,5 +287,6 @@ class OrderServiceTest { internalPaymentStatus = InternalPaymentStatus.FAILED), orderStatus = OrderStatus.PAYMENT_FAILED ) + val ORDER_DAO_DELIVERED = ORDER_DAO_PAID.copy(orderStatus = OrderStatus.DELIVERED) } } diff --git a/src/test/kotlin/com/carbonara/core/slack/SlackServiceTests.kt b/src/test/kotlin/com/carbonara/core/slack/SlackServiceTests.kt index 05653de..a79c8aa 100644 --- a/src/test/kotlin/com/carbonara/core/slack/SlackServiceTests.kt +++ b/src/test/kotlin/com/carbonara/core/slack/SlackServiceTests.kt @@ -1,5 +1,6 @@ package com.carbonara.core.slack +import com.carbonara.core.helper.createOrderDao import com.carbonara.core.order.OrderService import com.carbonara.core.order.OrderStatus import io.mockk.coEvery @@ -14,11 +15,13 @@ class SlackServiceTests { private lateinit var orderService: OrderService private lateinit var slackService: SlackService + private lateinit var slackMessageService: SlackMessageService @BeforeEach fun init() { orderService = mockk() - slackService = SlackService(orderService) + slackMessageService = mockk() + slackService = SlackService(orderService, slackMessageService) } @TestFactory @@ -29,13 +32,26 @@ class SlackServiceTests { OrderStatusUpdateScenario("cancelled", OrderStatus.CANCELLED) ).map { scenario -> DynamicTest.dynamicTest("Happy case for order status update with status ${scenario.orderType}") { - coEvery { orderService.updateOrderStatus(any(), any()) } returns Unit + val orderDao = createOrderDao(orderStatus = scenario.expectedOrderStatus) + val slackMessageParams = SlackMessageParams( + customerName = orderDao.userName, + orderId = orderDao.orderId.toString(), + address = orderDao.deliveryAddress.toString(), + googleMapsLink = orderDao.deliveryAddress.createGoogleMapsLink(), + productNames = orderDao.products.map { it.productName }, + orderStatus = scenario.expectedOrderStatus, + timeStamp = "1726842841" + ) + + coEvery { orderService.updateOrderStatus(any(), any()) } returns orderDao + coEvery { slackMessageService.updateOrderMessageToAccepted(any()) } returns Unit runBlocking { - slackService.handleOrderStatusUpdate("1", scenario.orderType) + slackService.handleOrderStatusUpdate(orderDao.orderId.toString(), scenario.orderType, "1726842841") } - coVerify { orderService.updateOrderStatus("1", scenario.expectedOrderStatus) } + coVerify { orderService.updateOrderStatus(orderDao.orderId.toString(), scenario.expectedOrderStatus) } + coVerify { slackMessageService.updateOrderMessageToAccepted(slackMessageParams) } } } }