Skip to content

Commit

Permalink
Merge pull request #70 from KjellBerlin/retrieve-slack-actions
Browse files Browse the repository at this point in the history
Update slack message after user interaction
  • Loading branch information
KjellBerlin authored Sep 20, 2024
2 parents 71fde34 + 0d2841e commit e2d52e0
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 36 deletions.
71 changes: 71 additions & 0 deletions src/main/kotlin/com/carbonara/core/helper/OrderFactory.kt
Original file line number Diff line number Diff line change
@@ -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<ProductDao> = 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
)
}
20 changes: 12 additions & 8 deletions src/main/kotlin/com/carbonara/core/order/OrderService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ProductDao>): String {
Expand Down Expand Up @@ -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 }
)
)
}
}
Expand Down
95 changes: 79 additions & 16 deletions src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<String>
params: SlackMessageParams
) {

val slack = Slack.getInstance()
Expand All @@ -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")
}
}
Expand All @@ -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<String>,
val timeStamp: String? = null,
val orderStatus: OrderStatus? = null
)
22 changes: 18 additions & 4 deletions src/main/kotlin/com/carbonara/core/slack/SlackService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)
}

Expand Down
12 changes: 10 additions & 2 deletions src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -35,11 +37,17 @@ data class SlackWebhookRequestBody(

@JsonIgnoreProperties(ignoreUnknown = true)
data class SlackPayload(
val actions: List<SlackAction>
val actions: List<SlackAction>,
val message: SlackMessage
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class SlackAction(
val action_id: String,
val value: String,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class SlackMessage(
val ts: String
)
7 changes: 5 additions & 2 deletions src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) }
}
Expand Down Expand Up @@ -285,5 +287,6 @@ class OrderServiceTest {
internalPaymentStatus = InternalPaymentStatus.FAILED),
orderStatus = OrderStatus.PAYMENT_FAILED
)
val ORDER_DAO_DELIVERED = ORDER_DAO_PAID.copy(orderStatus = OrderStatus.DELIVERED)
}
}
Loading

0 comments on commit e2d52e0

Please sign in to comment.