diff --git a/build.gradle.kts b/build.gradle.kts index 65e3651..f7073af 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,6 +49,10 @@ dependencies { // Mollie implementation("be.woutschoovaerts:mollie:4.3.0") + // Slack + implementation("com.slack.api:slack-api-model-kotlin-extension:1.42.0") + implementation("com.slack.api:slack-api-client-kotlin-extension:1.42.0") + testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("io.mockk:mockk:1.13.11") } diff --git a/src/main/kotlin/com/carbonara/core/address/Address.kt b/src/main/kotlin/com/carbonara/core/address/Address.kt index 4443f1f..9c4ab75 100644 --- a/src/main/kotlin/com/carbonara/core/address/Address.kt +++ b/src/main/kotlin/com/carbonara/core/address/Address.kt @@ -12,5 +12,13 @@ data class Address( fun addressPropertiesComplete(): Boolean { return street != null && streetNumber != null && postCode != null && city != null && country != null } + + override fun toString(): String { + return "$street $streetNumber, $postCode $city" + } + + fun createGoogleMapsLink(): String { + return "https://www.google.com/maps/place/?q=place_id:$googlePlaceId" + } } diff --git a/src/main/kotlin/com/carbonara/core/order/OrderService.kt b/src/main/kotlin/com/carbonara/core/order/OrderService.kt index 18ce818..1ed4f5c 100644 --- a/src/main/kotlin/com/carbonara/core/order/OrderService.kt +++ b/src/main/kotlin/com/carbonara/core/order/OrderService.kt @@ -6,6 +6,7 @@ import com.carbonara.core.payment.MolliePaymentService import com.carbonara.core.payment.PaymentException import com.carbonara.core.product.ProductDao import com.carbonara.core.product.ProductService +import com.carbonara.core.slack.SlackMessageService import kotlinx.coroutines.reactor.awaitSingleOrNull import mu.KotlinLogging import org.bson.types.ObjectId @@ -16,7 +17,8 @@ import java.time.OffsetDateTime class OrderService( private val orderRepository: OrderRepository, private val productService: ProductService, - private val molliePaymentService: MolliePaymentService + private val molliePaymentService: MolliePaymentService, + private val slackMessageService: SlackMessageService ) { suspend fun createOrder( @@ -96,7 +98,13 @@ class OrderService( if (order.paymentDetails.internalPaymentStatus != InternalPaymentStatus.PAID) { updateOrderToPaid(order, paymentStatus) - // TODO: trigger delivery + slackMessageService.sendNewOrderMessage( + 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/payment/MolliePaymentWebhookController.kt b/src/main/kotlin/com/carbonara/core/payment/MolliePaymentWebhookController.kt index e30da89..e34ab1b 100644 --- a/src/main/kotlin/com/carbonara/core/payment/MolliePaymentWebhookController.kt +++ b/src/main/kotlin/com/carbonara/core/payment/MolliePaymentWebhookController.kt @@ -2,6 +2,7 @@ package com.carbonara.core.payment import com.carbonara.core.order.OrderService import mu.KotlinLogging +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RestController @@ -12,10 +13,11 @@ class MolliePaymentWebhookController( // Potential dos attack endpoint, introduce rate limiting - @PostMapping("/mollie-payment-status", "application/x-www-form-urlencoded") - suspend fun handleMollieWebhook(requestBody: MollieWebhookRequestBody) { + @PostMapping("/mollie-payment-status", consumes = ["application/x-www-form-urlencoded"]) + suspend fun handleMollieWebhook(requestBody: MollieWebhookRequestBody): ResponseEntity { log.info("Webhook received for paymentId: {}", requestBody.id) orderService.handleOrderPayment(requestBody.id) + return ResponseEntity.ok().build() } companion object { diff --git a/src/main/kotlin/com/carbonara/core/slack/SlackException.kt b/src/main/kotlin/com/carbonara/core/slack/SlackException.kt new file mode 100644 index 0000000..c2a1e81 --- /dev/null +++ b/src/main/kotlin/com/carbonara/core/slack/SlackException.kt @@ -0,0 +1,3 @@ +package com.carbonara.core.slack + +class SlackException(message: String) : RuntimeException(message) diff --git a/src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt b/src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt new file mode 100644 index 0000000..f2db45f --- /dev/null +++ b/src/main/kotlin/com/carbonara/core/slack/SlackMessageService.kt @@ -0,0 +1,76 @@ +package com.carbonara.core.slack + +import com.slack.api.Slack +import com.slack.api.methods.kotlin_extension.request.chat.blocks +import mu.KotlinLogging +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service + +@Service +class SlackMessageService { + + @Value("\${slack.token}") + lateinit var slackToken: String + + @Value("\${slack.channel}") + lateinit var slackChannel: String + + fun sendNewOrderMessage( + customerName: String, + orderId: String, + address: String, + googleMapsLink: String, + productNames: List + ) { + val slack = Slack.getInstance() + val response = slack.methods(slackToken).chatPostMessage { req -> req + .channel(slackChannel) + .blocks { + section { + fields { + markdownText("*Customer Name:*\n$customerName") + markdownText("*OrderId:*\n$orderId") + } + } + section { + fields { + markdownText("*Address:*\n$address\n$googleMapsLink") + markdownText("*Products:*\n${productNames.joinToString(", ")}") + } + } + actions { + button { + text("ACCEPT", emoji = true) + style("primary") + value("processing_order") + } + button { + text("DELIVERY_IN_PROGRESS", emoji = true) + style("primary") + value("delivery_in_progress") + } + button { + text("DELIVERED", emoji = true) + style("primary") + value("delivered") + } + button { + text("CANCELLED", emoji = true) + style("danger") + value("cancelled") + } + } + divider() + } + } + + if (!response.isOk) { + log.error("Slack API error: ${response.error}") + throw SlackException("Failed to send slack message for orderId: $orderId. Error: ${response.error}") + } + } + + companion object { + private val log = KotlinLogging.logger {} + } +} diff --git a/src/main/kotlin/com/carbonara/core/delivery/SlackDeliveryWebhookController.kt b/src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt similarity index 92% rename from src/main/kotlin/com/carbonara/core/delivery/SlackDeliveryWebhookController.kt rename to src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt index 73cd8e7..3282590 100644 --- a/src/main/kotlin/com/carbonara/core/delivery/SlackDeliveryWebhookController.kt +++ b/src/main/kotlin/com/carbonara/core/slack/SlackWebhookController.kt @@ -1,4 +1,4 @@ -package com.carbonara.core.delivery +package com.carbonara.core.slack import com.carbonara.core.order.OrderService import mu.KotlinLogging @@ -12,6 +12,8 @@ class SlackDeliveryWebhookController( // Potential dos attack endpoint, introduce rate limiting + // TODO: Handle webhook + @PostMapping("/slack-delivery-status", "application/x-www-form-urlencoded") suspend fun handleSlackWebhook(requestBody: SlackWebhookRequestBody) { log.info("--Start Slack--") diff --git a/src/main/resources/application-staging.properties b/src/main/resources/application-staging.properties index b6e1ac4..ae31f61 100644 --- a/src/main/resources/application-staging.properties +++ b/src/main/resources/application-staging.properties @@ -16,3 +16,6 @@ mollie.redirectUrl=carbonara://order-status mollie.paymentWebhookUrl=https://carbonara-core-mkvkriomda-ew.a.run.app/mollie-payment-status google.apiKey=${sm://projects/897213585789/secrets/google-apiKey} + +slack.token=${sm://projects/897213585789/secrets/slack-token} +slack.channel=C07KMKK3ZDX diff --git a/src/test/kotlin/com/carbonara/core/address/AddressTest.kt b/src/test/kotlin/com/carbonara/core/address/AddressTest.kt index 48f3912..05a3455 100644 --- a/src/test/kotlin/com/carbonara/core/address/AddressTest.kt +++ b/src/test/kotlin/com/carbonara/core/address/AddressTest.kt @@ -2,19 +2,36 @@ package com.carbonara.core.address import com.carbonara.core.serviceAvailability.ServiceAvailabilityServiceTest.Companion.GOOGLE_PLACE_ID import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test class AddressTest { + @Nested + inner class AddressPropertiesCompleteTests { + + @Test + fun `Happy case - address complete`() { + assertEquals(true, ADDRESS.addressPropertiesComplete()) + } + + @Test + fun `Happy case - address incomplete`() { + val address = ADDRESS.copy(streetNumber = null) + assertEquals(false, address.addressPropertiesComplete()) + } + } + @Test - fun `Happy case - address complete`() { - assertEquals(true, ADDRESS.addressPropertiesComplete()) + fun `Happy case - address to string`() { + val result = ADDRESS.toString() + assertEquals("Baker Street 221b, 12345 London", result) } @Test - fun `Happy case - address incomplete`() { - val address = ADDRESS.copy(streetNumber = null) - assertEquals(false, address.addressPropertiesComplete()) + fun `Happy case - googleMapsLink`() { + val result = ADDRESS.createGoogleMapsLink() + assertEquals("https://www.google.com/maps/place/?q=place_id:$GOOGLE_PLACE_ID", result) } companion object { diff --git a/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt b/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt index e667ae5..12862ef 100644 --- a/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt +++ b/src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt @@ -8,6 +8,7 @@ import com.carbonara.core.payment.MolliePaymentService import com.carbonara.core.payment.PaymentException import com.carbonara.core.product.ProductDao import com.carbonara.core.product.ProductService +import com.carbonara.core.slack.SlackMessageService import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -31,16 +32,19 @@ class OrderServiceTest { private lateinit var orderRepository: OrderRepository private lateinit var productService: ProductService private lateinit var molliePaymentService: MolliePaymentService + private lateinit var slackMessageService: SlackMessageService @BeforeEach fun init() { orderRepository = mockk() productService = mockk() molliePaymentService = mockk() - orderService = OrderService(orderRepository, productService, molliePaymentService) + slackMessageService = mockk() + orderService = OrderService(orderRepository, productService, molliePaymentService, slackMessageService) mockkStatic(OffsetDateTime::class) every { OffsetDateTime.now() } returns TIME + every { slackMessageService.sendNewOrderMessage(any(), any(), any(), any(), any()) } returns Unit } @Nested