diff --git a/src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt b/src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt index 68932be..075a22c 100644 --- a/src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt +++ b/src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt @@ -3,6 +3,7 @@ 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 com.slack.api.methods.response.chat.ChatUpdateResponse import mu.KotlinLogging import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service @@ -72,13 +73,33 @@ class SlackMessageService { } } - // TODO: Update depending on actual status - fun updateOrderMessageToAccepted( + fun updateOrderMessage( params: SlackMessageParams ) { + val slackResponse = when(params.orderStatus) { + OrderStatus.RIDER_ASSIGNED -> updateOrderMessageToAccepted(params) + OrderStatus.DELIVERY_IN_PROGRESS -> updateOrderMessageToDeliveryInProgress(params) + OrderStatus.DELIVERED -> updateOrderMessageToDelivered(params) + OrderStatus.CANCELLED -> updateOrderMessageToCancelled(params) + OrderStatus.FINDING_AVAILABLE_RIDER -> updateOrderMessageToUnassigned(params) + else -> throw IllegalArgumentException("Cannot update slack message based on order status ${params.orderStatus}") + } + + if (slackResponse == null) { + log.error("Slack API error: Slack response null") + throw SlackException("Failed to update slack message for orderId: ${params.orderId}. Error: Slack response null") + } else if (!slackResponse.isOk) { + log.error("Slack API error: ${slackResponse.error}") + throw SlackException("Failed to update slack message for orderId: ${params.orderId}. Error: ${slackResponse.error}") + } + } + + private fun updateOrderMessageToAccepted( + params: SlackMessageParams + ): ChatUpdateResponse? { val slack = Slack.getInstance() - val response = slack.methods(slackToken).chatUpdate { req -> req + return slack.methods(slackToken).chatUpdate { req -> req .channel(slackChannel) .ts(params.timeStamp) .blocks { @@ -94,6 +115,11 @@ class SlackMessageService { markdownText("*Products:*\n${params.productNames.joinToString(", ")}") } } + section { + fields { + markdownText("*Rider:*\n<@${params.slackUserId}>") + } + } actions { button { text("ACCEPT", emoji = true) @@ -116,17 +142,245 @@ class SlackMessageService { value(params.orderId) actionId("cancelled") } + button { + text("UNASSIGN", emoji = true) + value(params.orderId) + actionId("unassign") + } } 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}") + private fun updateOrderMessageToDeliveryInProgress( + params: SlackMessageParams + ): ChatUpdateResponse? { + + val slack = Slack.getInstance() + return 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(", ")}") + } + } + section { + fields { + markdownText("*Rider:*\n<@${params.slackUserId}>") + } + } + actions { + button { + text("ACCEPT", emoji = true) + value(params.orderId) + actionId("accept") + } + button { + text("DELIVERY IN PROGRESS", emoji = true) + style("primary") + 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") + } + button { + text("UNASSIGN", emoji = true) + value(params.orderId) + actionId("unassign") + } + } + divider() + } + } + } + + private fun updateOrderMessageToDelivered( + params: SlackMessageParams + ): ChatUpdateResponse? { + + val slack = Slack.getInstance() + return 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(", ")}") + } + } + section { + fields { + markdownText("*Rider:*\n<@${params.slackUserId}>") + } + } + actions { + button { + text("ACCEPT", emoji = true) + value(params.orderId) + actionId("accept") + } + button { + text("DELIVERY IN PROGRESS", emoji = true) + value(params.orderId) + actionId("delivery_in_progress") + } + button { + text("DELIVERED", emoji = true) + style("primary") + value(params.orderId) + actionId("delivered") + } + button { + text("CANCELLED", emoji = true) + value(params.orderId) + actionId("cancelled") + } + button { + text("UNASSIGN", emoji = true) + value(params.orderId) + actionId("unassign") + } + } + divider() + } + } + } + + private fun updateOrderMessageToCancelled( + params: SlackMessageParams + ): ChatUpdateResponse? { + + val slack = Slack.getInstance() + return 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(", ")}") + } + } + section { + fields { + markdownText("*Rider:*\n<@${params.slackUserId}>") + } + } + actions { + button { + text("ACCEPT", emoji = true) + 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) + style("danger") + value(params.orderId) + actionId("cancelled") + } + button { + text("UNASSIGN", emoji = true) + value(params.orderId) + actionId("unassign") + } + } + divider() + } } } + fun updateOrderMessageToUnassigned( + params: SlackMessageParams + ): ChatUpdateResponse? { + + val slack = Slack.getInstance() + return 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) + style("primary") + value(params.orderId) + actionId("delivery_in_progress") + } + button { + text("DELIVERED", emoji = true) + style("primary") + value(params.orderId) + actionId("delivered") + } + button { + text("CANCELLED", emoji = true) + style("danger") + value(params.orderId) + actionId("cancelled") + } + } + divider() + } + } + } companion object { private val log = KotlinLogging.logger {} @@ -140,5 +394,6 @@ data class SlackMessageParams( val googleMapsLink: String, val productNames: List, val timeStamp: String? = null, - val orderStatus: OrderStatus? = null + val orderStatus: OrderStatus? = null, + val slackUserId: String? = null ) diff --git a/src/main/kotlin/com/carbonara/core/slack/SlackService.kt b/src/main/kotlin/com/carbonara/core/slack/SlackService.kt index 5b1661f..ae1459f 100644 --- a/src/main/kotlin/com/carbonara/core/slack/SlackService.kt +++ b/src/main/kotlin/com/carbonara/core/slack/SlackService.kt @@ -13,14 +13,15 @@ class SlackService( suspend fun handleOrderStatusUpdate( orderId: String, slackOrderStatus: String, - messageTimestamp: String + messageTimestamp: String, + slackUserId: String ) { val orderStatus = mapSlackOrderStatusToOrderStatus(slackOrderStatus) val order = orderService.updateOrderStatus( orderId = orderId, orderStatus = orderStatus ) - slackMessageService.updateOrderMessageToAccepted( + slackMessageService.updateOrderMessage( SlackMessageParams( customerName = order.userName, orderId = orderId, @@ -28,7 +29,8 @@ class SlackService( googleMapsLink = order.deliveryAddress.createGoogleMapsLink(), productNames = order.products.map { it.productName }, timeStamp = messageTimestamp, - orderStatus = orderStatus + orderStatus = orderStatus, + slackUserId = slackUserId ) ) } @@ -41,6 +43,7 @@ class SlackService( "delivery_in_progress" -> OrderStatus.DELIVERY_IN_PROGRESS "delivered" -> OrderStatus.DELIVERED "cancelled" -> OrderStatus.CANCELLED + "unassign" -> OrderStatus.FINDING_AVAILABLE_RIDER else -> throw IllegalArgumentException("Invalid slack order status: $slackOrderStatus") } } diff --git a/src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt b/src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt index 15ab0ed..6ff71b9 100644 --- a/src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt +++ b/src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt @@ -2,6 +2,7 @@ package com.carbonara.core.slack import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import mu.KotlinLogging import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RestController @@ -15,11 +16,15 @@ class SlackDeliveryWebhookController( suspend fun handleSlackWebhook(requestBody: SlackWebhookRequestBody): ResponseEntity { val slackPayload = objectMapper.readValue(requestBody.payload, SlackPayload::class.java) + log.info("Received slack webhook for orderId=${slackPayload.actions.first().value} " + + "and userName=${slackPayload.user.username}") + slackPayload.actions.forEach { action -> slackService.handleOrderStatusUpdate( orderId = action.value, slackOrderStatus = action.action_id, - messageTimestamp = slackPayload.message.ts + messageTimestamp = slackPayload.message.ts, + slackUserId = slackPayload.user.id ) } @@ -28,6 +33,7 @@ class SlackDeliveryWebhookController( companion object { private val objectMapper = jacksonObjectMapper() + private val log = KotlinLogging.logger {} } } @@ -38,12 +44,13 @@ data class SlackWebhookRequestBody( @JsonIgnoreProperties(ignoreUnknown = true) data class SlackPayload( val actions: List, - val message: SlackMessage + val message: SlackMessage, + val user: SlackUser // Not sure if this is here ) @JsonIgnoreProperties(ignoreUnknown = true) data class SlackAction( - val action_id: String, + val action_id: String, // Storing orderId in here val value: String, ) @@ -51,3 +58,9 @@ data class SlackAction( data class SlackMessage( val ts: String ) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class SlackUser( + val id: String, + val username: String +) diff --git a/src/test/kotlin/com/carbonara/core/slack/SlackServiceTests.kt b/src/test/kotlin/com/carbonara/core/slack/SlackServiceTests.kt index a79c8aa..41cf1c7 100644 --- a/src/test/kotlin/com/carbonara/core/slack/SlackServiceTests.kt +++ b/src/test/kotlin/com/carbonara/core/slack/SlackServiceTests.kt @@ -31,7 +31,7 @@ class SlackServiceTests { OrderStatusUpdateScenario("delivered", OrderStatus.DELIVERED), OrderStatusUpdateScenario("cancelled", OrderStatus.CANCELLED) ).map { scenario -> - DynamicTest.dynamicTest("Happy case for order status update with status ${scenario.orderType}") { + DynamicTest.dynamicTest("Happy case for order status update with status ${scenario.orderStatus}") { val orderDao = createOrderDao(orderStatus = scenario.expectedOrderStatus) val slackMessageParams = SlackMessageParams( customerName = orderDao.userName, @@ -40,23 +40,29 @@ class SlackServiceTests { googleMapsLink = orderDao.deliveryAddress.createGoogleMapsLink(), productNames = orderDao.products.map { it.productName }, orderStatus = scenario.expectedOrderStatus, - timeStamp = "1726842841" + timeStamp = "1726842841", + slackUserId = "sherlock.holmes" ) coEvery { orderService.updateOrderStatus(any(), any()) } returns orderDao - coEvery { slackMessageService.updateOrderMessageToAccepted(any()) } returns Unit + coEvery { slackMessageService.updateOrderMessage(any()) } returns Unit runBlocking { - slackService.handleOrderStatusUpdate(orderDao.orderId.toString(), scenario.orderType, "1726842841") + slackService.handleOrderStatusUpdate( + orderId = orderDao.orderId.toString(), + slackOrderStatus = scenario.orderStatus, + messageTimestamp = "1726842841", + slackUserId = "sherlock.holmes" + ) } coVerify { orderService.updateOrderStatus(orderDao.orderId.toString(), scenario.expectedOrderStatus) } - coVerify { slackMessageService.updateOrderMessageToAccepted(slackMessageParams) } + coVerify { slackMessageService.updateOrderMessage(slackMessageParams) } } } } data class OrderStatusUpdateScenario( - val orderType: String, + val orderStatus: String, val expectedOrderStatus: OrderStatus )