diff --git a/pom.xml b/pom.xml index 579e05e..4a5182d 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,17 @@ org.springframework.boot spring-boot-starter-web + + + io.awspring.cloud + spring-cloud-aws-starter-sns + 3.2.0-M1 + + + io.awspring.cloud + spring-cloud-aws-starter-sqs + 3.2.0-M1 + com.fasterxml.jackson.module jackson-module-kotlin diff --git a/src/main/kotlin/com/fiap/order/adapter/controller/OrderController.kt b/src/main/kotlin/com/fiap/order/adapter/controller/OrderController.kt index 7ae2d77..ef03d67 100644 --- a/src/main/kotlin/com/fiap/order/adapter/controller/OrderController.kt +++ b/src/main/kotlin/com/fiap/order/adapter/controller/OrderController.kt @@ -54,7 +54,7 @@ class OrderController( override fun create(orderRequest: OrderRequest): ResponseEntity = ResponseEntity.ok( - createOrderUseCase.create( + createOrderUseCase.requestCreate( customerId = getAuthenticatedCustomerId(), items = orderRequest.toOrderItemsDomain() ) diff --git a/src/main/kotlin/com/fiap/order/adapter/gateway/PaymentGateway.kt b/src/main/kotlin/com/fiap/order/adapter/gateway/PaymentGateway.kt index 074ed7b..fcba729 100644 --- a/src/main/kotlin/com/fiap/order/adapter/gateway/PaymentGateway.kt +++ b/src/main/kotlin/com/fiap/order/adapter/gateway/PaymentGateway.kt @@ -7,4 +7,6 @@ interface PaymentGateway { fun findByPaymentId(paymentId: String): PaymentResponse? fun requestPayment(order: Order): PaymentResponse + + fun notifyRequestPayment(order: Order) } diff --git a/src/main/kotlin/com/fiap/order/adapter/gateway/impl/PaymentGatewayImpl.kt b/src/main/kotlin/com/fiap/order/adapter/gateway/impl/PaymentGatewayImpl.kt index 7926534..7c99c6b 100644 --- a/src/main/kotlin/com/fiap/order/adapter/gateway/impl/PaymentGatewayImpl.kt +++ b/src/main/kotlin/com/fiap/order/adapter/gateway/impl/PaymentGatewayImpl.kt @@ -2,6 +2,7 @@ package com.fiap.order.adapter.gateway.impl import com.fiap.order.adapter.client.PaymentsApiClient import com.fiap.order.adapter.gateway.PaymentGateway +import com.fiap.order.adapter.messaging.sender.PaymentSender import com.fiap.order.domain.entities.Order import com.fiap.order.driver.web.request.PaymentOrderInfo import com.fiap.order.driver.web.request.PaymentOrderInfoLine @@ -9,7 +10,8 @@ import com.fiap.order.driver.web.request.PaymentRequest import com.fiap.order.driver.web.response.PaymentResponse class PaymentGatewayImpl( - private val paymentsApiClient: PaymentsApiClient + private val paymentsApiClient: PaymentsApiClient, + private val paymentSender: PaymentSender ) : PaymentGateway { override fun findByPaymentId(paymentId: String): PaymentResponse { @@ -37,6 +39,27 @@ class PaymentGatewayImpl( return paymentsApiClient.create(paymentHTTPRequest) } + override fun notifyRequestPayment(order: Order) { + paymentSender.requestPayment( + paymentRequest = PaymentRequest( + orderInfo = PaymentOrderInfo( + number = order.number!!, + orderedAt = order.orderedAt, + orderedBy = order.customer?.name ?: ANONYMOUS, + total = order.total, + lines = order.lines.map { orderLine -> + PaymentOrderInfoLine( + name = orderLine.name, + quantity = orderLine.quantity, + unitPrice = orderLine.unitPrice, + total = orderLine.total + ) + } + ) + ) + ) + } + companion object { const val ANONYMOUS = "Anonymous" } diff --git a/src/main/kotlin/com/fiap/order/adapter/messaging/config/MessagingConfig.kt b/src/main/kotlin/com/fiap/order/adapter/messaging/config/MessagingConfig.kt new file mode 100644 index 0000000..a9fe48f --- /dev/null +++ b/src/main/kotlin/com/fiap/order/adapter/messaging/config/MessagingConfig.kt @@ -0,0 +1,20 @@ +package com.fiap.order.adapter.messaging.config + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fiap.order.adapter.messaging.sender.PaymentSender +import com.fiap.order.adapter.messaging.sender.impl.PaymentSenderImpl +import io.awspring.cloud.sns.core.SnsTemplate +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class MessagingConfig { + + @Bean("PaymentSender") + fun createPaymentSender(snsTemplate: SnsTemplate, + @Value("\${topic.request-payment}") topicArn: String, + objectMapper: ObjectMapper) : PaymentSender { + return PaymentSenderImpl(snsTemplate, topicArn, objectMapper) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/fiap/order/adapter/messaging/sender/PaymentSender.kt b/src/main/kotlin/com/fiap/order/adapter/messaging/sender/PaymentSender.kt new file mode 100644 index 0000000..499fbd6 --- /dev/null +++ b/src/main/kotlin/com/fiap/order/adapter/messaging/sender/PaymentSender.kt @@ -0,0 +1,7 @@ +package com.fiap.order.adapter.messaging.sender + +import com.fiap.order.driver.web.request.PaymentRequest + +interface PaymentSender { + fun requestPayment(paymentRequest:PaymentRequest) +} \ No newline at end of file diff --git a/src/main/kotlin/com/fiap/order/adapter/messaging/sender/impl/PaymentSenderImpl.kt b/src/main/kotlin/com/fiap/order/adapter/messaging/sender/impl/PaymentSenderImpl.kt new file mode 100644 index 0000000..4bdc3ae --- /dev/null +++ b/src/main/kotlin/com/fiap/order/adapter/messaging/sender/impl/PaymentSenderImpl.kt @@ -0,0 +1,15 @@ +package com.fiap.order.adapter.messaging.sender.impl + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fiap.order.adapter.messaging.sender.PaymentSender +import com.fiap.order.driver.web.request.PaymentRequest +import io.awspring.cloud.sns.core.SnsTemplate +import org.springframework.messaging.support.GenericMessage + + +class PaymentSenderImpl(private val snsTemplate: SnsTemplate, private val topicName: String, private val mapper: ObjectMapper) : PaymentSender { + + override fun requestPayment(paymentRequest: PaymentRequest) { + snsTemplate.send(topicName, GenericMessage(mapper.writeValueAsString(paymentRequest))) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/fiap/order/domain/entities/Payment.kt b/src/main/kotlin/com/fiap/order/domain/entities/Payment.kt new file mode 100644 index 0000000..7f255b5 --- /dev/null +++ b/src/main/kotlin/com/fiap/order/domain/entities/Payment.kt @@ -0,0 +1,15 @@ +package com.fiap.order.domain.entities + +import com.fiap.order.domain.valueobjects.PaymentStatus +import java.time.LocalDateTime + +data class Payment( + val id: String, + val orderNumber: Long, + val externalOrderId: String, + val externalOrderGlobalId: String?, + val paymentInfo: String, + val createdAt: LocalDateTime, + val status: PaymentStatus, + val statusChangedAt: LocalDateTime, +) diff --git a/src/main/kotlin/com/fiap/order/driver/database/config/GatewayConfig.kt b/src/main/kotlin/com/fiap/order/driver/database/config/GatewayConfig.kt index 03ba5d6..0ca3e3a 100644 --- a/src/main/kotlin/com/fiap/order/driver/database/config/GatewayConfig.kt +++ b/src/main/kotlin/com/fiap/order/driver/database/config/GatewayConfig.kt @@ -15,6 +15,7 @@ import com.fiap.order.adapter.gateway.impl.PaymentGatewayImpl import com.fiap.order.adapter.gateway.impl.ProductGatewayImpl import com.fiap.order.adapter.gateway.impl.StockGatewayImpl import com.fiap.order.adapter.gateway.impl.TransactionalGatewayImpl +import com.fiap.order.adapter.messaging.sender.PaymentSender import com.fiap.order.driver.database.persistence.jpa.CustomerJpaRepository import com.fiap.order.driver.database.persistence.jpa.OrderJpaRepository import org.springframework.context.annotation.Bean @@ -51,7 +52,7 @@ class GatewayConfig { } @Bean("PaymentGateway") - fun createPaymentGateway(paymentsApiClient: PaymentsApiClient): PaymentGateway { - return PaymentGatewayImpl(paymentsApiClient) + fun createPaymentGateway(paymentsApiClient: PaymentsApiClient, paymentSender: PaymentSender): PaymentGateway { + return PaymentGatewayImpl(paymentsApiClient, paymentSender) } } diff --git a/src/main/kotlin/com/fiap/order/driver/messaging/consumer/OrderConsumer.kt b/src/main/kotlin/com/fiap/order/driver/messaging/consumer/OrderConsumer.kt new file mode 100644 index 0000000..b16a943 --- /dev/null +++ b/src/main/kotlin/com/fiap/order/driver/messaging/consumer/OrderConsumer.kt @@ -0,0 +1,32 @@ +package com.fiap.order.driver.messaging.consumer + +import com.fiap.order.domain.valueobjects.PaymentStatus +import com.fiap.order.driver.messaging.event.PaymentEvent +import com.fiap.order.driver.messaging.event.toDomain +import com.fiap.order.usecases.CreateOrderUseCase +import com.fiap.order.usecases.services.OrderService +import io.awspring.cloud.sqs.annotation.SqsListener +import org.slf4j.LoggerFactory +import org.springframework.messaging.MessageHeaders +import org.springframework.messaging.handler.annotation.Headers +import org.springframework.stereotype.Component + +@Component +class OrderConsumer( + private val createOrderUseCase: CreateOrderUseCase, +) { + + private val log = LoggerFactory.getLogger(javaClass) + + + @SqsListener("\${sqs.queues.request-payment-response}") + fun onMessage(message: PaymentEvent, @Headers headers: MessageHeaders) { + when (message.status) { + PaymentStatus.PENDING -> createOrderUseCase.acceptPending(message.toDomain()) + PaymentStatus.EXPIRED -> TODO() + PaymentStatus.FAILED -> TODO() + PaymentStatus.CONFIRMED -> TODO() + } + log.info(message.toString()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/fiap/order/driver/messaging/event/PaymentEvent.kt b/src/main/kotlin/com/fiap/order/driver/messaging/event/PaymentEvent.kt new file mode 100644 index 0000000..a156b9a --- /dev/null +++ b/src/main/kotlin/com/fiap/order/driver/messaging/event/PaymentEvent.kt @@ -0,0 +1,27 @@ +package com.fiap.order.driver.messaging.event + +import com.fiap.order.domain.entities.Payment +import com.fiap.order.domain.valueobjects.PaymentStatus +import java.time.LocalDateTime + +data class PaymentEvent( + val id: String, + val orderNumber: Long, + val externalOrderId: String, + val externalOrderGlobalId: String?, + val paymentInfo: String, + val createdAt: LocalDateTime, + val status: PaymentStatus, + val statusChangedAt: LocalDateTime, +) + +fun PaymentEvent.toDomain() = Payment( + id = id, + orderNumber = orderNumber, + externalOrderId = externalOrderId, + externalOrderGlobalId = externalOrderGlobalId, + paymentInfo = paymentInfo, + createdAt = createdAt, + status = status, + statusChangedAt = statusChangedAt, +) \ No newline at end of file diff --git a/src/main/kotlin/com/fiap/order/driver/web/response/PendingOrderResponse.kt b/src/main/kotlin/com/fiap/order/driver/web/response/PendingOrderResponse.kt index 78adbf1..9fe9b3a 100644 --- a/src/main/kotlin/com/fiap/order/driver/web/response/PendingOrderResponse.kt +++ b/src/main/kotlin/com/fiap/order/driver/web/response/PendingOrderResponse.kt @@ -1,8 +1,9 @@ package com.fiap.order.driver.web.response import com.fiap.order.domain.entities.Order +import com.fiap.order.domain.entities.Payment data class PendingOrderResponse( val order: Order, - val payment: PaymentResponse, + val payment: Payment?, ) diff --git a/src/main/kotlin/com/fiap/order/usecases/CreateOrderUseCase.kt b/src/main/kotlin/com/fiap/order/usecases/CreateOrderUseCase.kt index 86021b3..16eba83 100644 --- a/src/main/kotlin/com/fiap/order/usecases/CreateOrderUseCase.kt +++ b/src/main/kotlin/com/fiap/order/usecases/CreateOrderUseCase.kt @@ -1,9 +1,14 @@ package com.fiap.order.usecases +import com.fiap.order.domain.entities.Order import com.fiap.order.domain.entities.OrderItem +import com.fiap.order.domain.entities.Payment +import com.fiap.order.domain.valueobjects.PaymentStatus +import com.fiap.order.driver.messaging.event.PaymentEvent import com.fiap.order.driver.web.response.PendingOrderResponse import java.util.* interface CreateOrderUseCase { - fun create(customerId: UUID?, items: List): PendingOrderResponse + fun requestCreate(customerId: UUID?, items: List): PendingOrderResponse + fun acceptPending(payment: Payment) : PendingOrderResponse } diff --git a/src/main/kotlin/com/fiap/order/usecases/RequestPaymentUseCase.kt b/src/main/kotlin/com/fiap/order/usecases/RequestPaymentUseCase.kt index 994fd4c..53bdee1 100644 --- a/src/main/kotlin/com/fiap/order/usecases/RequestPaymentUseCase.kt +++ b/src/main/kotlin/com/fiap/order/usecases/RequestPaymentUseCase.kt @@ -4,5 +4,5 @@ import com.fiap.order.domain.entities.Order import com.fiap.order.driver.web.response.PaymentResponse interface RequestPaymentUseCase { - fun requestPayment(order: Order): PaymentResponse + fun requestPayment(order: Order) } diff --git a/src/main/kotlin/com/fiap/order/usecases/services/OrderService.kt b/src/main/kotlin/com/fiap/order/usecases/services/OrderService.kt index f20b3dc..241d2c3 100644 --- a/src/main/kotlin/com/fiap/order/usecases/services/OrderService.kt +++ b/src/main/kotlin/com/fiap/order/usecases/services/OrderService.kt @@ -5,6 +5,7 @@ import com.fiap.order.adapter.gateway.TransactionalGateway import com.fiap.order.domain.entities.Order import com.fiap.order.domain.entities.OrderItem import com.fiap.order.domain.entities.OrderLine +import com.fiap.order.domain.entities.Payment import com.fiap.order.domain.errors.ErrorType import com.fiap.order.domain.errors.SelfOrderManagementException import com.fiap.order.domain.valueobjects.OrderStatus @@ -56,7 +57,7 @@ open class OrderService( override fun findByStatusAndCustomerId(status: OrderStatus, customerId: UUID): List = orderGateway.findByStatusAndCustomerId(status, customerId) - override fun create( + override fun requestCreate( customerId: UUID?, items: List, ): PendingOrderResponse { @@ -87,7 +88,7 @@ open class OrderService( ) } - var order = orderGateway.upsert( + val order = orderGateway.upsert( Order( number = null, orderedAt = LocalDateTime.now(), @@ -98,24 +99,36 @@ open class OrderService( ) ) - order = orderGateway.upsert( - order.copy( - status = OrderStatus.PENDING, - lines = order.lines.map { i -> i.copy(orderNumber = order.number) }, - ) - ) - val payment = providePaymentRequestUseCase.requestPayment(order) + providePaymentRequestUseCase.requestPayment(order) - log.info("Created order $order and respective payment $payment") + log.info("Created order $order ") PendingOrderResponse( order = order, - payment = payment, + payment = null, ) } } + override fun acceptPending(payment: Payment): PendingOrderResponse { + return orderGateway.findByOrderNumber(payment.orderNumber)?.let { + orderGateway.upsert( + it.copy( + status = OrderStatus.PENDING, + lines = it.lines.map { i -> i.copy(orderNumber = it.number) }, + ) + ) + }?.let { orderUpdate -> + PendingOrderResponse(order = orderUpdate, payment = payment,).also { + log.info("Accepted order $orderUpdate") + } + } ?: throw SelfOrderManagementException( + errorType = ErrorType.ORDER_NOT_FOUND, + message = "Order ${payment.orderNumber} should exist in the database", + ) + } + override fun confirmOrder(orderNumber: Long): Order { return transactionalRepository.transaction { val order = getByOrderNumber(orderNumber) diff --git a/src/main/kotlin/com/fiap/order/usecases/services/PaymentService.kt b/src/main/kotlin/com/fiap/order/usecases/services/PaymentService.kt index 2738c1e..0e9eb28 100644 --- a/src/main/kotlin/com/fiap/order/usecases/services/PaymentService.kt +++ b/src/main/kotlin/com/fiap/order/usecases/services/PaymentService.kt @@ -12,8 +12,8 @@ class PaymentService( { private val log = LoggerFactory.getLogger(javaClass) - override fun requestPayment(order: Order): PaymentResponse { + override fun requestPayment(order: Order) { log.info("Requesting payment request for order $order") - return paymentGateway.requestPayment(order) + paymentGateway.notifyRequestPayment(order) } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a118cff..cf6c286 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,12 @@ spring: + cloud: + aws: + endpoint: http://localhost:4566 + credentials: + access-key: localstack + secret-key: localstack + region: + static: ${AWS_REGION} application: name: orders-api datasource: @@ -32,3 +40,11 @@ clients: url: ${STOCK_SERVICE_URL} payments-api: url: ${PAYMENTS_SERVICE_URL} + + +topic: + request-payment: arn:aws:sns:us-east-2:000000000000:request-payment_topic + +sqs: + queues: + request-payment-response: payment-response_queue \ No newline at end of file diff --git a/src/test/kotlin/com/fiap/order/adapter/controller/OrderControllerTest.kt b/src/test/kotlin/com/fiap/order/adapter/controller/OrderControllerTest.kt index 19f3f05..b8d71b7 100644 --- a/src/test/kotlin/com/fiap/order/adapter/controller/OrderControllerTest.kt +++ b/src/test/kotlin/com/fiap/order/adapter/controller/OrderControllerTest.kt @@ -123,13 +123,13 @@ class OrderControllerTest { val items = orderRequest.items.map { it.toOrderItem() } val pendingOrderResponse = createPendingOrderResponse() - every { createOrderUseCase.create(null, items) } returns pendingOrderResponse + every { createOrderUseCase.requestCreate(null, items) } returns pendingOrderResponse val result = orderController.create(orderRequest) assertThat(result.statusCode).isEqualTo(HttpStatus.OK) assertThat(result.body).isEqualTo(pendingOrderResponse) - verify(exactly = 1) { createOrderUseCase.create(null, items) } + verify(exactly = 1) { createOrderUseCase.requestCreate(null, items) } } @Test diff --git a/src/test/kotlin/com/fiap/order/adapter/gateway/impl/PaymentGatewayImplTest.kt b/src/test/kotlin/com/fiap/order/adapter/gateway/impl/PaymentGatewayImplTest.kt index dcbffff..3f02d41 100644 --- a/src/test/kotlin/com/fiap/order/adapter/gateway/impl/PaymentGatewayImplTest.kt +++ b/src/test/kotlin/com/fiap/order/adapter/gateway/impl/PaymentGatewayImplTest.kt @@ -1,6 +1,7 @@ package com.fiap.order.adapter.gateway.impl import com.fiap.order.adapter.client.PaymentsApiClient +import com.fiap.order.adapter.messaging.sender.PaymentSender import com.fiap.order.createOrder import com.fiap.order.createPaymentResponse import io.mockk.every @@ -13,10 +14,11 @@ import java.util.* class PaymentGatewayImplTest { private val paymentsApiClient = mockk() + private val paymentSender = mockk() private val paymentGatewayImpl = PaymentGatewayImpl( - paymentsApiClient, + paymentsApiClient, paymentSender ) @AfterEach diff --git a/src/test/kotlin/com/fiap/order/usecases/services/OrderServiceTest.kt b/src/test/kotlin/com/fiap/order/usecases/services/OrderServiceTest.kt index 0179c33..41b5039 100644 --- a/src/test/kotlin/com/fiap/order/usecases/services/OrderServiceTest.kt +++ b/src/test/kotlin/com/fiap/order/usecases/services/OrderServiceTest.kt @@ -165,7 +165,7 @@ class OrderServiceTest { justRun { adjustInventoryUseCase.decrementStockOfProducts(any()) } every { orderRepository.upsert(any()) } returns createOrder(status = OrderStatus.CREATED) - val result = orderService.create(null, items) + val result = orderService.requestCreate(null, items) assertThat(result).isNotNull() assertThat(result.order.number).isNotNull() @@ -177,7 +177,7 @@ class OrderServiceTest { fun `should throw an error when order is empty`() { val items = emptyList() - assertThatThrownBy { orderService.create(null, items) } + assertThatThrownBy { orderService.requestCreate(null, items) } .isInstanceOf(SelfOrderManagementException::class.java) .hasFieldOrPropertyWithValue("errorType", ErrorType.EMPTY_ORDER) }