diff --git a/src/main/kotlin/com/fiap/payments/PaymentsApiApp.kt b/src/main/kotlin/com/fiap/payments/PaymentsApiApp.kt index f474bf9..5404bd0 100644 --- a/src/main/kotlin/com/fiap/payments/PaymentsApiApp.kt +++ b/src/main/kotlin/com/fiap/payments/PaymentsApiApp.kt @@ -15,11 +15,9 @@ import org.springframework.cloud.openfeign.FeignAutoConfiguration @OpenAPIDefinition( info = Info( - title = "Self-Order Management API", + title = "Payments API", version = "1.0.0", - description = - "API de autoatendimento em restaurante como implementação do Tech Challenge" + - " referente à primeira fase do curso de pós-graduação em Arquitetura de Software pela FIAP.", + description = "Microsserviço de pagamentos", contact = Contact( name = "Grupo 15", diff --git a/src/main/kotlin/com/fiap/payments/adapter/controller/PaymentController.kt b/src/main/kotlin/com/fiap/payments/adapter/controller/PaymentController.kt index afa4e31..107e5f5 100644 --- a/src/main/kotlin/com/fiap/payments/adapter/controller/PaymentController.kt +++ b/src/main/kotlin/com/fiap/payments/adapter/controller/PaymentController.kt @@ -1,9 +1,9 @@ package com.fiap.payments.adapter.controller import com.fiap.payments.domain.entities.Payment -import com.fiap.payments.domain.entities.PaymentRequest import com.fiap.payments.driver.web.PaymentAPI -import com.fiap.payments.driver.web.request.OrderRequest +import com.fiap.payments.driver.web.request.PaymentHTTPRequest +import com.fiap.payments.usecases.ChangePaymentStatusUseCase import com.fiap.payments.usecases.LoadPaymentUseCase import com.fiap.payments.usecases.ProvidePaymentRequestUseCase import com.fiap.payments.usecases.SyncPaymentUseCase @@ -15,7 +15,8 @@ import org.springframework.web.bind.annotation.RestController class PaymentController( private val loadPaymentUseCase: LoadPaymentUseCase, private val syncPaymentUseCase: SyncPaymentUseCase, - private val providePaymentRequestUseCase: ProvidePaymentRequestUseCase + private val providePaymentRequestUseCase: ProvidePaymentRequestUseCase, + private val changePaymentStatusUseCase: ChangePaymentStatusUseCase, ) : PaymentAPI { private val log = LoggerFactory.getLogger(javaClass) @@ -23,26 +24,26 @@ class PaymentController( return ResponseEntity.ok(loadPaymentUseCase.findAll()) } - override fun getByOrderNumber(orderNumber: Long): ResponseEntity { - return ResponseEntity.ok(loadPaymentUseCase.getByOrderNumber(orderNumber)) + override fun getByPaymentId(id: String): ResponseEntity { + return ResponseEntity.ok(loadPaymentUseCase.getByPaymentId(id)) } /** * The server response is important to flag the provider for retries */ - override fun notify(orderNumber: Long, resourceId: String, topic: String): ResponseEntity { - // TODO: verify x-signature header by Mercado Pago - log.info("Notification received for order ${orderNumber}: type=${topic} externalId=${resourceId}") - + override fun notify(paymentId: String, resourceId: String, topic: String): ResponseEntity { + // TODO: verify x-signature header allowing only request from Mercado Pago + log.info("Notification received for payment [${paymentId}]: type=${topic} externalId=${resourceId}") + when (topic) { IPNType.MERCHANT_ORDER.ipnType -> { - syncPaymentUseCase.syncPayment(orderNumber, resourceId) + syncPaymentUseCase.syncPayment(paymentId, resourceId) return ResponseEntity.ok().build() } IPNType.PAYMENT.ipnType -> { - val payment = loadPaymentUseCase.getByOrderNumber(orderNumber) + val payment = loadPaymentUseCase.getByPaymentId(paymentId) payment.externalOrderGlobalId?.let { - syncPaymentUseCase.syncPayment(orderNumber, it) + syncPaymentUseCase.syncPayment(paymentId, it) return ResponseEntity.ok().build() } // returns server error because external order global ID was not previously saved, @@ -50,14 +51,26 @@ class PaymentController( return ResponseEntity.internalServerError().build() } else -> { - // returns bad request because application does not accept this kind of IPN types + // returns bad request because application does not accept this IPN type return ResponseEntity.badRequest().build() } } } - override fun create(order: OrderRequest): ResponseEntity { - return ResponseEntity.ok(providePaymentRequestUseCase.providePaymentRequest(order.toDomain())); + override fun create(paymentHTTPRequest: PaymentHTTPRequest): ResponseEntity { + return ResponseEntity.ok(providePaymentRequestUseCase.providePaymentRequest(paymentHTTPRequest)) + } + + override fun fail(paymentId: String): ResponseEntity { + return ResponseEntity.ok(changePaymentStatusUseCase.failPayment(paymentId)) + } + + override fun expire(paymentId: String): ResponseEntity { + return ResponseEntity.ok(changePaymentStatusUseCase.expirePayment(paymentId)) + } + + override fun confirm(paymentId: String): ResponseEntity { + return ResponseEntity.ok(changePaymentStatusUseCase.confirmPayment(paymentId)) } enum class IPNType(val ipnType: String) { diff --git a/src/main/kotlin/com/fiap/payments/adapter/controller/configuration/ControllerExceptionHandler.kt b/src/main/kotlin/com/fiap/payments/adapter/controller/configuration/ControllerExceptionHandler.kt index 9526a66..16aff45 100644 --- a/src/main/kotlin/com/fiap/payments/adapter/controller/configuration/ControllerExceptionHandler.kt +++ b/src/main/kotlin/com/fiap/payments/adapter/controller/configuration/ControllerExceptionHandler.kt @@ -13,49 +13,20 @@ class ControllerExceptionHandler { protected fun domainErrorHandler(domainException: PaymentsException): ResponseEntity { val apiErrorResponseEntity: ApiErrorResponseEntity = when (domainException.errorType) { - ErrorType.PRODUCT_ALREADY_EXISTS, - ErrorType.CUSTOMER_ALREADY_EXISTS, - ErrorType.STOCK_ALREADY_EXISTS, - ErrorType.PAYMENT_ALREADY_EXISTS, - ErrorType.INSUFFICIENT_STOCK, - -> - ApiErrorResponseEntity( - ApiError(domainException.errorType.name, domainException.message), - HttpStatus.UNPROCESSABLE_ENTITY, - ) - - ErrorType.CUSTOMER_NOT_FOUND, - ErrorType.PRODUCT_NOT_FOUND, - ErrorType.COMPONENT_NOT_FOUND, - ErrorType.STOCK_NOT_FOUND, - ErrorType.ORDER_NOT_FOUND, ErrorType.PAYMENT_NOT_FOUND, -> ApiErrorResponseEntity( ApiError(domainException.errorType.name, domainException.message), HttpStatus.NOT_FOUND, ) - - ErrorType.INVALID_ORDER_STATUS, - ErrorType.INVALID_ORDER_STATE_TRANSITION, - ErrorType.INVALID_PRODUCT_CATEGORY, - ErrorType.EMPTY_ORDER, - ErrorType.PRODUCT_NUMBER_IS_MANDATORY, - ErrorType.COMPONENT_NUMBER_IS_MANDATORY, + + ErrorType.INVALID_PAYMENT_STATE_TRANSITION, -> ApiErrorResponseEntity( ApiError(domainException.errorType.name, domainException.message), HttpStatus.BAD_REQUEST, ) - ErrorType.PAYMENT_NOT_CONFIRMED, - ErrorType.PAYMENT_REQUEST_NOT_ALLOWED, - -> - ApiErrorResponseEntity( - ApiError(domainException.errorType.name, domainException.message), - HttpStatus.PAYMENT_REQUIRED, - ) - else -> ApiErrorResponseEntity( ApiError(ErrorType.UNEXPECTED_ERROR.name, domainException.localizedMessage), diff --git a/src/main/kotlin/com/fiap/payments/adapter/controller/configuration/ServiceConfig.kt b/src/main/kotlin/com/fiap/payments/adapter/controller/configuration/ServiceConfig.kt index b50528b..9dad15b 100644 --- a/src/main/kotlin/com/fiap/payments/adapter/controller/configuration/ServiceConfig.kt +++ b/src/main/kotlin/com/fiap/payments/adapter/controller/configuration/ServiceConfig.kt @@ -4,43 +4,45 @@ import com.fiap.payments.PaymentsApiApp import com.fiap.payments.adapter.gateway.OrderGateway import com.fiap.payments.adapter.gateway.PaymentGateway import com.fiap.payments.adapter.gateway.PaymentProviderGateway +import com.fiap.payments.usecases.ChangePaymentStatusUseCase import com.fiap.payments.usecases.ConfirmOrderUseCase import com.fiap.payments.usecases.LoadPaymentUseCase +import com.fiap.payments.usecases.services.OrderService +import com.fiap.payments.usecases.services.PaymentService import com.fiap.payments.usecases.services.PaymentSyncService import org.springframework.context.annotation.Bean import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Configuration -import com.fiap.payments.usecases.services.PaymentService -import com.fiap.payments.usecases.services.OrderService @Configuration @ComponentScan(basePackageClasses = [PaymentsApiApp::class]) class ServiceConfig { - @Bean fun createPaymentService( paymentRepository: PaymentGateway, paymentProvider: PaymentProviderGateway, + confirmOrderUseCase: ConfirmOrderUseCase ): PaymentService { return PaymentService( paymentRepository, - paymentProvider + paymentProvider, + confirmOrderUseCase ) } - + @Bean fun paymentSyncService( loadPaymentUseCase: LoadPaymentUseCase, paymentGateway: PaymentGateway, paymentProvider: PaymentProviderGateway, - confirmOrderUseCase: ConfirmOrderUseCase + changePaymentStatusUseCase: ChangePaymentStatusUseCase, ): PaymentSyncService { return PaymentSyncService( loadPaymentUseCase, paymentGateway, paymentProvider, - confirmOrderUseCase + changePaymentStatusUseCase ) } diff --git a/src/main/kotlin/com/fiap/payments/adapter/gateway/OrderGateway.kt b/src/main/kotlin/com/fiap/payments/adapter/gateway/OrderGateway.kt index cce459a..10907ad 100644 --- a/src/main/kotlin/com/fiap/payments/adapter/gateway/OrderGateway.kt +++ b/src/main/kotlin/com/fiap/payments/adapter/gateway/OrderGateway.kt @@ -1,7 +1,5 @@ package com.fiap.payments.adapter.gateway -import com.fiap.payments.domain.entities.Order - interface OrderGateway { - fun confirmOrder(orderNumber: Long): Order -} \ No newline at end of file + fun confirmOrder(orderNumber: Long) +} diff --git a/src/main/kotlin/com/fiap/payments/adapter/gateway/PaymentGateway.kt b/src/main/kotlin/com/fiap/payments/adapter/gateway/PaymentGateway.kt index bd23fc2..e436c1b 100644 --- a/src/main/kotlin/com/fiap/payments/adapter/gateway/PaymentGateway.kt +++ b/src/main/kotlin/com/fiap/payments/adapter/gateway/PaymentGateway.kt @@ -3,11 +3,9 @@ package com.fiap.payments.adapter.gateway import com.fiap.payments.domain.entities.Payment interface PaymentGateway { - fun findByOrderNumber(orderNumber: Long): Payment? + fun findByPaymentId(id: String): Payment? fun findAll(): List - fun create(payment: Payment): Payment - - fun update(payment: Payment): Payment + fun upsert(payment: Payment): Payment } diff --git a/src/main/kotlin/com/fiap/payments/adapter/gateway/PaymentProviderGateway.kt b/src/main/kotlin/com/fiap/payments/adapter/gateway/PaymentProviderGateway.kt index 36e324c..75595e2 100644 --- a/src/main/kotlin/com/fiap/payments/adapter/gateway/PaymentProviderGateway.kt +++ b/src/main/kotlin/com/fiap/payments/adapter/gateway/PaymentProviderGateway.kt @@ -1,12 +1,11 @@ package com.fiap.payments.adapter.gateway -import com.fiap.payments.domain.entities.Order import com.fiap.payments.domain.entities.PaymentRequest import com.fiap.payments.domain.valueobjects.PaymentStatus - +import com.fiap.payments.driver.web.request.PaymentHTTPRequest interface PaymentProviderGateway { - fun createExternalOrder(order: Order): PaymentRequest + fun createExternalOrder(paymentId: String, paymentHTTPRequest: PaymentHTTPRequest): PaymentRequest fun checkExternalOrderStatus(externalOrderGlobalId: String): PaymentStatus } diff --git a/src/main/kotlin/com/fiap/payments/adapter/gateway/impl/OrderGatewayImpl.kt b/src/main/kotlin/com/fiap/payments/adapter/gateway/impl/OrderGatewayImpl.kt index 85156bc..bb1b8ef 100644 --- a/src/main/kotlin/com/fiap/payments/adapter/gateway/impl/OrderGatewayImpl.kt +++ b/src/main/kotlin/com/fiap/payments/adapter/gateway/impl/OrderGatewayImpl.kt @@ -2,12 +2,12 @@ package com.fiap.payments.adapter.gateway.impl import com.fiap.payments.adapter.gateway.OrderGateway import com.fiap.payments.client.OrderApiClient -import com.fiap.payments.domain.entities.Order -class OrderGatewayImpl(private val orderApiClient: OrderApiClient) : OrderGateway { +class OrderGatewayImpl( + private val orderApiClient: OrderApiClient +) : OrderGateway { - override fun confirmOrder(orderNumber: Long): Order { + override fun confirmOrder(orderNumber: Long) { return orderApiClient.confirm(orderNumber) } - -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/fiap/payments/adapter/gateway/impl/PaymentGatewayImpl.kt b/src/main/kotlin/com/fiap/payments/adapter/gateway/impl/PaymentGatewayImpl.kt index 8851751..0c294fa 100644 --- a/src/main/kotlin/com/fiap/payments/adapter/gateway/impl/PaymentGatewayImpl.kt +++ b/src/main/kotlin/com/fiap/payments/adapter/gateway/impl/PaymentGatewayImpl.kt @@ -2,53 +2,44 @@ package com.fiap.payments.adapter.gateway.impl import com.fiap.payments.adapter.gateway.PaymentGateway import com.fiap.payments.domain.entities.Payment -import com.fiap.payments.domain.errors.ErrorType -import com.fiap.payments.domain.errors.PaymentsException -import com.fiap.payments.driver.database.persistence.repository.PaymentDynamoRepository import com.fiap.payments.driver.database.persistence.mapper.PaymentMapper +import com.fiap.payments.driver.database.persistence.repository.PaymentDynamoRepository import org.mapstruct.factory.Mappers class PaymentGatewayImpl( - private val paymentJpaRepository: PaymentDynamoRepository, + private val paymentRepository: PaymentDynamoRepository, ) : PaymentGateway { private val mapper = Mappers.getMapper(PaymentMapper::class.java) - override fun findByOrderNumber(orderNumber: Long): Payment? { - return paymentJpaRepository.findById(orderNumber.toString()) + override fun findByPaymentId(id: String): Payment? { + return paymentRepository.findById(id) .map(mapper::toDomain) .orElse(null) } override fun findAll(): List { - return paymentJpaRepository.findAll() + return paymentRepository.findAll() .map(mapper::toDomain) } - override fun create(payment: Payment): Payment { - payment.orderNumber.let { - findByOrderNumber(it)?.let { - throw PaymentsException( - errorType = ErrorType.PAYMENT_ALREADY_EXISTS, - message = "Payment record for order [${payment.orderNumber}] already exists", - ) - } - } - return persist(payment) - } - - override fun update(payment: Payment): Payment { - val newItem = - payment.orderNumber.let { findByOrderNumber(it)?.update(payment) } - ?: throw PaymentsException( - errorType = ErrorType.PAYMENT_NOT_FOUND, - message = "Payment record for order [${payment.orderNumber}] not found", - ) - return persist(newItem) + override fun upsert(payment: Payment): Payment { + val currentPayment = findByPaymentId(id = payment.id) ?: payment + val paymentUpdated = + currentPayment.copy( + orderNumber = payment.orderNumber, + externalOrderId = payment.externalOrderId, + externalOrderGlobalId = payment.externalOrderGlobalId, + paymentInfo = payment.paymentInfo, + createdAt = payment.createdAt, + status = payment.status, + statusChangedAt = payment.statusChangedAt, + ) + return persist(paymentUpdated) } private fun persist(payment: Payment): Payment = payment .let(mapper::toEntity) - .let(paymentJpaRepository::save) + .let(paymentRepository::save) .let(mapper::toDomain) } diff --git a/src/main/kotlin/com/fiap/payments/client/OrderApiClient.kt b/src/main/kotlin/com/fiap/payments/client/OrderApiClient.kt index 3a16f12..6f59b04 100644 --- a/src/main/kotlin/com/fiap/payments/client/OrderApiClient.kt +++ b/src/main/kotlin/com/fiap/payments/client/OrderApiClient.kt @@ -1,11 +1,14 @@ package com.fiap.payments.client - -import com.fiap.payments.domain.entities.Order import org.springframework.cloud.openfeign.FeignClient -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestMethod -@FeignClient(name = "orders-client", url = "\${clients.orders-api.url}") +@FeignClient( + name = "orders-client", + url = "\${clients.orders-api.url}" +) interface OrderApiClient { @RequestMapping( @@ -13,6 +16,5 @@ interface OrderApiClient { value = ["/notify/{orderNumber}/confirmed"], consumes = ["application/json"] ) - fun confirm(@PathVariable orderNumber: Long) : Order - + fun confirm(@PathVariable orderNumber: Long) } diff --git a/src/main/kotlin/com/fiap/payments/client/config/FeignConfig.kt b/src/main/kotlin/com/fiap/payments/client/config/FeignConfig.kt index f49f179..4052e6a 100644 --- a/src/main/kotlin/com/fiap/payments/client/config/FeignConfig.kt +++ b/src/main/kotlin/com/fiap/payments/client/config/FeignConfig.kt @@ -15,5 +15,4 @@ class FeignConfig { } } } - -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/fiap/payments/config/JWTSecurityConfig.kt b/src/main/kotlin/com/fiap/payments/config/JWTSecurityConfig.kt index 2e8c882..6bf63c1 100644 --- a/src/main/kotlin/com/fiap/payments/config/JWTSecurityConfig.kt +++ b/src/main/kotlin/com/fiap/payments/config/JWTSecurityConfig.kt @@ -27,6 +27,7 @@ class JWTSecurityConfig { csrf.disable() } .authorizeHttpRequests { authorize -> + // TODO authorize.requestMatchers(HttpMethod.POST, "/orders").permitAll() authorize.anyRequest().permitAll() } diff --git a/src/main/kotlin/com/fiap/payments/domain/entities/Component.kt b/src/main/kotlin/com/fiap/payments/domain/entities/Component.kt deleted file mode 100644 index d8c8dd2..0000000 --- a/src/main/kotlin/com/fiap/payments/domain/entities/Component.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.fiap.payments.domain.entities - -data class Component( - val number: Long? = null, - val name: String, -) { - fun update(newComponent: Component): Component = - copy( - name = newComponent.name, - ) -} diff --git a/src/main/kotlin/com/fiap/payments/domain/entities/Customer.kt b/src/main/kotlin/com/fiap/payments/domain/entities/Customer.kt deleted file mode 100644 index f69cdbc..0000000 --- a/src/main/kotlin/com/fiap/payments/domain/entities/Customer.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.fiap.payments.domain.entities - -import java.util.* - -data class Customer( - val id: UUID, - val document: String?, - val name: String?, - val email: String?, - val phone: String?, - val address: String?, -) { - fun update(newCustomer: Customer): Customer = - copy( - document = newCustomer.document, - name = newCustomer.name, - email = newCustomer.email, - phone = newCustomer.phone, - address = newCustomer.address, - ) -} diff --git a/src/main/kotlin/com/fiap/payments/domain/entities/Order.kt b/src/main/kotlin/com/fiap/payments/domain/entities/Order.kt deleted file mode 100644 index c560237..0000000 --- a/src/main/kotlin/com/fiap/payments/domain/entities/Order.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.fiap.payments.domain.entities - -import com.fiap.payments.domain.valueobjects.OrderStatus -import java.math.BigDecimal -import java.time.LocalDate - -data class Order( - val number: Long? = null, - val date: LocalDate = LocalDate.now(), - val customer: Customer? = null, - val status: OrderStatus = OrderStatus.CREATED, - val items: List, - val total: BigDecimal, -) diff --git a/src/main/kotlin/com/fiap/payments/domain/entities/OrderItem.kt b/src/main/kotlin/com/fiap/payments/domain/entities/OrderItem.kt deleted file mode 100644 index 255dc95..0000000 --- a/src/main/kotlin/com/fiap/payments/domain/entities/OrderItem.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.fiap.payments.domain.entities - -data class OrderItem( - val productNumber: Long, - val quantity: Long, -) diff --git a/src/main/kotlin/com/fiap/payments/domain/entities/Payment.kt b/src/main/kotlin/com/fiap/payments/domain/entities/Payment.kt index 97d570d..be27d29 100644 --- a/src/main/kotlin/com/fiap/payments/domain/entities/Payment.kt +++ b/src/main/kotlin/com/fiap/payments/domain/entities/Payment.kt @@ -2,24 +2,15 @@ package com.fiap.payments.domain.entities import com.fiap.payments.domain.valueobjects.PaymentStatus import java.time.LocalDateTime +import java.util.* data class Payment( + val id: String = UUID.randomUUID().toString(), val orderNumber: Long, - val externalOrderId: String, - val externalOrderGlobalId: String?, - val paymentInfo: String, + val externalOrderId: String? = null, + val externalOrderGlobalId: String? = null, + val paymentInfo: String? = null, val createdAt: LocalDateTime, val status: PaymentStatus, val statusChangedAt: LocalDateTime, -) { - fun update(newPayment: Payment): Payment = - copy( - orderNumber = newPayment.orderNumber, - externalOrderId = newPayment.externalOrderId, - externalOrderGlobalId = newPayment.externalOrderGlobalId, - paymentInfo = newPayment.paymentInfo, - createdAt = newPayment.createdAt, - status = newPayment.status, - statusChangedAt = newPayment.statusChangedAt, - ) -} +) diff --git a/src/main/kotlin/com/fiap/payments/domain/entities/Product.kt b/src/main/kotlin/com/fiap/payments/domain/entities/Product.kt deleted file mode 100644 index 1357262..0000000 --- a/src/main/kotlin/com/fiap/payments/domain/entities/Product.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.fiap.payments.domain.entities - -import com.fiap.payments.domain.valueobjects.ProductCategory -import java.math.BigDecimal - -data class Product( - val number: Long? = null, - val name: String, - val price: BigDecimal, - val description: String, - val category: ProductCategory, - val minSub: Int, - val maxSub: Int, - val subItems: List, - val components: List, -) { - fun update(newProduct: Product): Product = - copy( - name = newProduct.name, - price = newProduct.price, - description = newProduct.description, - category = newProduct.category, - subItems = newProduct.subItems, - maxSub = newProduct.maxSub, - minSub = newProduct.minSub, - components = newProduct.components, - ) - - fun isLogicalItem() = components.isEmpty() -} diff --git a/src/main/kotlin/com/fiap/payments/domain/entities/Stock.kt b/src/main/kotlin/com/fiap/payments/domain/entities/Stock.kt deleted file mode 100644 index 601e95c..0000000 --- a/src/main/kotlin/com/fiap/payments/domain/entities/Stock.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.fiap.payments.domain.entities - -data class Stock( - val componentNumber: Long, - val quantity: Long, -) { - fun update(newStock: Stock): Stock = - copy( - quantity = newStock.quantity, - ) - - fun hasSufficientInventory(quantity: Long) = quantity >= this.quantity -} diff --git a/src/main/kotlin/com/fiap/payments/domain/errors/ErrorType.kt b/src/main/kotlin/com/fiap/payments/domain/errors/ErrorType.kt index 87d0e1a..8ec186d 100644 --- a/src/main/kotlin/com/fiap/payments/domain/errors/ErrorType.kt +++ b/src/main/kotlin/com/fiap/payments/domain/errors/ErrorType.kt @@ -1,30 +1,7 @@ package com.fiap.payments.domain.errors enum class ErrorType { - CUSTOMER_NOT_FOUND, - PRODUCT_NOT_FOUND, - COMPONENT_NOT_FOUND, - STOCK_NOT_FOUND, - ORDER_NOT_FOUND, PAYMENT_NOT_FOUND, - - PRODUCT_NUMBER_IS_MANDATORY, - COMPONENT_NUMBER_IS_MANDATORY, - - CUSTOMER_ALREADY_EXISTS, - PRODUCT_ALREADY_EXISTS, - STOCK_ALREADY_EXISTS, - PAYMENT_ALREADY_EXISTS, - - EMPTY_ORDER, - INSUFFICIENT_STOCK, - - INVALID_PRODUCT_CATEGORY, - INVALID_ORDER_STATUS, - INVALID_ORDER_STATE_TRANSITION, - - PAYMENT_NOT_CONFIRMED, - PAYMENT_REQUEST_NOT_ALLOWED, - + INVALID_PAYMENT_STATE_TRANSITION, UNEXPECTED_ERROR, } diff --git a/src/main/kotlin/com/fiap/payments/domain/errors/PaymentsException.kt b/src/main/kotlin/com/fiap/payments/domain/errors/PaymentsException.kt index ba202de..62eb252 100644 --- a/src/main/kotlin/com/fiap/payments/domain/errors/PaymentsException.kt +++ b/src/main/kotlin/com/fiap/payments/domain/errors/PaymentsException.kt @@ -1,4 +1,7 @@ package com.fiap.payments.domain.errors -data class PaymentsException(var errorType: ErrorType, override val cause: Throwable? = null, override val message: String?) : - RuntimeException(message, cause) +data class PaymentsException( + var errorType: ErrorType, + override val cause: Throwable? = null, + override val message: String? +) : RuntimeException(message, cause) diff --git a/src/main/kotlin/com/fiap/payments/domain/valueobjects/OrderStatus.kt b/src/main/kotlin/com/fiap/payments/domain/valueobjects/OrderStatus.kt deleted file mode 100644 index 00c1898..0000000 --- a/src/main/kotlin/com/fiap/payments/domain/valueobjects/OrderStatus.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.fiap.payments.domain.valueobjects - -import com.fiap.payments.domain.errors.ErrorType -import com.fiap.payments.domain.errors.PaymentsException - -enum class OrderStatus { - CREATED, - PENDING, - CONFIRMED, - PREPARING, - COMPLETED, - DONE, - CANCELLED, - ; - - companion object { - fun fromString(status: String): OrderStatus { - return values().firstOrNull { it.name.equals(status.trim(), ignoreCase = true) } - ?: throw PaymentsException( - errorType = ErrorType.INVALID_ORDER_STATUS, - message = "Status $status is not valid", - ) - } - } -} diff --git a/src/main/kotlin/com/fiap/payments/domain/valueobjects/ProductCategory.kt b/src/main/kotlin/com/fiap/payments/domain/valueobjects/ProductCategory.kt deleted file mode 100644 index c226ef3..0000000 --- a/src/main/kotlin/com/fiap/payments/domain/valueobjects/ProductCategory.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.fiap.payments.domain.valueobjects - -import com.fiap.payments.domain.errors.ErrorType -import com.fiap.payments.domain.errors.PaymentsException - - -enum class ProductCategory { - DRINK, - MAIN, - SIDE, - DESSERT, - ; - - companion object { - fun fromString(category: String): ProductCategory { - return ProductCategory.values().firstOrNull { it.name.equals(category.trim(), ignoreCase = true) } - ?: throw PaymentsException( - errorType = ErrorType.INVALID_PRODUCT_CATEGORY, - message = "Product category $category is not valid", - ) - } - } -} diff --git a/src/main/kotlin/com/fiap/payments/driver/database/configuration/DynamoDBConfig.kt b/src/main/kotlin/com/fiap/payments/driver/database/configuration/DynamoDBConfig.kt index ca69893..e58fa39 100644 --- a/src/main/kotlin/com/fiap/payments/driver/database/configuration/DynamoDBConfig.kt +++ b/src/main/kotlin/com/fiap/payments/driver/database/configuration/DynamoDBConfig.kt @@ -33,8 +33,8 @@ class DynamoDBConfig { @Bean("amazonDynamoDB") @ConditionalOnProperty("aws.dynamodb.local", havingValue = "true") fun amazonDynamoDB( - @Value("\${aws.dynamodb.endpoint:#{null}}") endpoint: String, - @Value("\${aws.dynamodb.region}:#{null}") region: String, + @Value("\${aws.dynamodb.endpoint}") endpoint: String, + @Value("\${aws.dynamodb.region}") region: String, ): AmazonDynamoDB { return AmazonDynamoDBClientBuilder.standard() // using default credentials provider chain, which searches for environment variables diff --git a/src/main/kotlin/com/fiap/payments/driver/database/persistence/entities/PaymentDocument.kt b/src/main/kotlin/com/fiap/payments/driver/database/persistence/entities/PaymentDocument.kt index ba29045..1732000 100644 --- a/src/main/kotlin/com/fiap/payments/driver/database/persistence/entities/PaymentDocument.kt +++ b/src/main/kotlin/com/fiap/payments/driver/database/persistence/entities/PaymentDocument.kt @@ -1,39 +1,39 @@ package com.fiap.payments.driver.database.persistence.entities -import com.amazonaws.services.dynamodbv2.datamodeling.* -import com.fiap.payments.domain.valueobjects.PaymentStatus +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverted import com.fiap.payments.driver.database.configuration.DynamoDBConfig -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.EnumType -import jakarta.persistence.Enumerated -import jakarta.persistence.Id -import jakarta.persistence.Table import java.time.LocalDateTime +import java.util.* @DynamoDBTable(tableName = "payments") class PaymentDocument( @field:DynamoDBHashKey + @field:DynamoDBAttribute(attributeName = "payment_id") + var id: String, + @field:DynamoDBAttribute(attributeName = "payment_order_number") - var orderNumber: String? = null , + var orderNumber: String, @field:DynamoDBAttribute(attributeName = "payment_external_order_id") - var externalOrderId: String = "", + var externalOrderId: String? = null, @field:DynamoDBAttribute(attributeName = "payment_external_order_global_id") - var externalOrderGlobalId: String? = "", + var externalOrderGlobalId: String? = null, @field:DynamoDBAttribute(attributeName = "payment_payment_info") - var paymentInfo: String = "", + var paymentInfo: String? = null, @field:DynamoDBAttribute(attributeName = "payment_created_at") @field:DynamoDBTypeConverted(converter = DynamoDBConfig.Companion.LocalDateTimeConverter::class) - var createdAt: LocalDateTime? = null, + var createdAt: LocalDateTime, @field:DynamoDBAttribute(attributeName = "payment_status") - var status: String = "", + var status: String, @field:DynamoDBAttribute(attributeName = "payment_status_changed_at") @field:DynamoDBTypeConverted(converter = DynamoDBConfig.Companion.LocalDateTimeConverter::class) - var statusChangedAt: LocalDateTime? = null, + var statusChangedAt: LocalDateTime, ) diff --git a/src/main/kotlin/com/fiap/payments/driver/database/persistence/mapper/PaymentMapper.kt b/src/main/kotlin/com/fiap/payments/driver/database/persistence/mapper/PaymentMapper.kt index 4fa2a60..885cad7 100644 --- a/src/main/kotlin/com/fiap/payments/driver/database/persistence/mapper/PaymentMapper.kt +++ b/src/main/kotlin/com/fiap/payments/driver/database/persistence/mapper/PaymentMapper.kt @@ -9,5 +9,4 @@ interface PaymentMapper { fun toDomain(entity: PaymentDocument): Payment fun toEntity(domain: Payment): PaymentDocument - } diff --git a/src/main/kotlin/com/fiap/payments/driver/database/provider/MercadoPagoPaymentProvider.kt b/src/main/kotlin/com/fiap/payments/driver/database/provider/MercadoPagoPaymentProvider.kt index 8530a0b..2d0a0f6 100644 --- a/src/main/kotlin/com/fiap/payments/driver/database/provider/MercadoPagoPaymentProvider.kt +++ b/src/main/kotlin/com/fiap/payments/driver/database/provider/MercadoPagoPaymentProvider.kt @@ -4,35 +4,35 @@ import com.fiap.payments.adapter.gateway.PaymentProviderGateway import com.fiap.payments.client.MercadoPagoClient import com.fiap.payments.client.MercadoPagoQRCodeOrderRequest import com.fiap.payments.client.MercadoPagoQRCodeOrderRequestItem -import com.fiap.payments.domain.entities.Order import com.fiap.payments.domain.entities.PaymentRequest import com.fiap.payments.domain.valueobjects.PaymentStatus +import com.fiap.payments.driver.web.request.PaymentHTTPRequest class MercadoPagoPaymentProvider( private val mercadoPagoClient: MercadoPagoClient, private val webhookBaseUrl: String, ) : PaymentProviderGateway { - override fun createExternalOrder(order: Order): PaymentRequest { + override fun createExternalOrder(paymentId: String, paymentHTTPRequest: PaymentHTTPRequest): PaymentRequest { // source_news=ipn indicates application will receive only Instant Payment Notifications (IPNs), not webhooks - val notificationUrl = "${webhookBaseUrl}/payments/notifications/${order.number}?source_news=ipn" + val notificationUrl = "${webhookBaseUrl}/payments/notifications/${paymentId}?source_news=ipn" val response = mercadoPagoClient.submitMerchantOrder( MercadoPagoQRCodeOrderRequest( - title = "Order ${order.number}", - description = "Ordered at ${order.date} by ${ order.customer?.name ?: order.customer?.document ?: "anonymous" }", - externalReference = order.number.toString(), + title = "Order ${paymentHTTPRequest.orderInfo.number}", + description = "Ordered at ${paymentHTTPRequest.orderInfo.orderedAt} " + + "by ${ paymentHTTPRequest.orderInfo.orderedBy }", + externalReference = paymentHTTPRequest.orderInfo.number.toString(), notificationUrl = notificationUrl, - totalAmount = order.total, - items = - order.items.map { product -> + totalAmount = paymentHTTPRequest.orderInfo.totalAmount, + items = paymentHTTPRequest.orderInfo.lines.map { orderLine -> MercadoPagoQRCodeOrderRequestItem( - title = product.name, - unitPrice = product.price, - quantity = 1, // TODO: fix to use order lines with persisted quantities per product - unitMeasure = MercadoPagoMeasureUnit.UNIT.measureUnit, - totalAmount = product.price, + title = orderLine.name, + unitPrice = orderLine.unitPrice, + quantity = orderLine.quantity, + unitMeasure = orderLine.unitOfMeasurement, + totalAmount = orderLine.totalAmount, ) }, ), @@ -65,10 +65,6 @@ class MercadoPagoPaymentProvider( } } - enum class MercadoPagoMeasureUnit(val measureUnit: String) { - UNIT("unit"), - } - /** * Not exhaustive list of order statuses. */ diff --git a/src/main/kotlin/com/fiap/payments/driver/database/provider/PaymentProviderGatewayMock.kt b/src/main/kotlin/com/fiap/payments/driver/database/provider/PaymentProviderGatewayMock.kt index e572119..d365764 100644 --- a/src/main/kotlin/com/fiap/payments/driver/database/provider/PaymentProviderGatewayMock.kt +++ b/src/main/kotlin/com/fiap/payments/driver/database/provider/PaymentProviderGatewayMock.kt @@ -1,23 +1,27 @@ package com.fiap.payments.driver.database.provider import com.fiap.payments.adapter.gateway.PaymentProviderGateway -import com.fiap.payments.domain.entities.Order import com.fiap.payments.domain.entities.PaymentRequest import com.fiap.payments.domain.valueobjects.PaymentStatus +import com.fiap.payments.driver.web.request.PaymentHTTPRequest +import org.slf4j.LoggerFactory import java.util.* class PaymentProviderGatewayMock: PaymentProviderGateway { + private val log = LoggerFactory.getLogger(javaClass) - override fun createExternalOrder(order: Order): PaymentRequest { + override fun createExternalOrder(paymentId: String, paymentHTTPRequest: PaymentHTTPRequest): PaymentRequest { + log.info("Providing mocked payment request for order [${paymentHTTPRequest.orderInfo.number}]") return PaymentRequest( externalOrderId = UUID.randomUUID().toString(), externalOrderGlobalId = null, - paymentInfo = "mocked" + paymentInfo = "00020101021243650016COM.MERCADOLIBRE..." ) } override fun checkExternalOrderStatus(externalOrderGlobalId: String): PaymentStatus { // always confirming + log.info("Returning confirmed payment request for order [$externalOrderGlobalId]") return PaymentStatus.CONFIRMED } } diff --git a/src/main/kotlin/com/fiap/payments/driver/web/PaymentAPI.kt b/src/main/kotlin/com/fiap/payments/driver/web/PaymentAPI.kt index 8196dba..f72ab0d 100644 --- a/src/main/kotlin/com/fiap/payments/driver/web/PaymentAPI.kt +++ b/src/main/kotlin/com/fiap/payments/driver/web/PaymentAPI.kt @@ -1,8 +1,7 @@ package com.fiap.payments.driver.web import com.fiap.payments.domain.entities.Payment -import com.fiap.payments.domain.entities.PaymentRequest -import com.fiap.payments.driver.web.request.OrderRequest +import com.fiap.payments.driver.web.request.PaymentHTTPRequest import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.enums.ParameterIn @@ -18,7 +17,7 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam -@Tag(name = "pagamento", description = "API de pagamentos") +@Tag(name = "pagamento", description = "Pagamentos") @RequestMapping("/payments") interface PaymentAPI { @Operation(summary = "Retorna todos os pagamentos") @@ -37,9 +36,9 @@ interface PaymentAPI { ApiResponse(responseCode = "404", description = "Pedido não encontrado"), ], ) - @GetMapping("/{orderNumber}") - fun getByOrderNumber( - @Parameter(description = "Número do pedido") @PathVariable orderNumber: Long, + @GetMapping("/{paymentId}") + fun getByPaymentId( + @Parameter(description = "ID de pagamento") @PathVariable paymentId: String, ): ResponseEntity @Operation( @@ -61,9 +60,9 @@ interface PaymentAPI { ApiResponse(responseCode = "200", description = "Operação bem-sucedida"), ], ) - @PostMapping("/notifications/{orderNumber}") + @PostMapping("/notifications/{paymentId}") fun notify( - @Parameter(description = "Número do pedido") @PathVariable orderNumber: Long, + @Parameter(description = "ID de pagamento") @PathVariable paymentId: String, @RequestParam(value = "id") resourceId: String, @RequestParam topic: String, ): ResponseEntity @@ -73,7 +72,36 @@ interface PaymentAPI { ApiResponse(responseCode = "200", description = "Operação bem-sucedida"), ], ) - @PostMapping("/create") - fun create(@RequestBody order: OrderRequest): ResponseEntity + @PostMapping + fun create(@RequestBody paymentHTTPRequest: PaymentHTTPRequest): ResponseEntity + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Operação bem-sucedida"), + ], + ) + @PostMapping("/{paymentId}/fail") + fun fail( + @Parameter(description = "ID de pagamento") @PathVariable paymentId: String, + ): ResponseEntity + + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Operação bem-sucedida"), + ], + ) + @PostMapping("/{paymentId}/expire") + fun expire( + @Parameter(description = "ID de pagamento") @PathVariable paymentId: String, + ): ResponseEntity + + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Operação bem-sucedida"), + ], + ) + @PostMapping("/{paymentId}/confirm") + fun confirm( + @Parameter(description = "ID de pagamento") @PathVariable paymentId: String, + ): ResponseEntity } diff --git a/src/main/kotlin/com/fiap/payments/driver/web/request/OrderRequest.kt b/src/main/kotlin/com/fiap/payments/driver/web/request/OrderRequest.kt deleted file mode 100644 index 4794f16..0000000 --- a/src/main/kotlin/com/fiap/payments/driver/web/request/OrderRequest.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.fiap.payments.driver.web.request - -import com.fiap.payments.domain.entities.Customer -import com.fiap.payments.domain.entities.Order -import com.fiap.payments.domain.entities.Product -import com.fiap.payments.domain.valueobjects.OrderStatus -import java.math.BigDecimal -import java.time.LocalDate - -data class OrderRequest( - val number: Long? = null, - val date: LocalDate = LocalDate.now(), - val customer: Customer? = null, - val status: OrderStatus, - val items: List, - val total: BigDecimal, -) { - fun toDomain(): Order { - return Order( - number = number, - date = date, - customer = customer, - status = status, - total = total, - items = items.map { - Product( - name = it.name, - number = it.number, - price = it.price, - description = it.description, - category = it.category, - minSub = 0, - maxSub = Int.MAX_VALUE, - subItems = arrayListOf(), - components = arrayListOf() - ) - } - ) - } -} diff --git a/src/main/kotlin/com/fiap/payments/driver/web/request/PaymentHTTPRequest.kt b/src/main/kotlin/com/fiap/payments/driver/web/request/PaymentHTTPRequest.kt new file mode 100644 index 0000000..05a88a2 --- /dev/null +++ b/src/main/kotlin/com/fiap/payments/driver/web/request/PaymentHTTPRequest.kt @@ -0,0 +1,43 @@ +package com.fiap.payments.driver.web.request + +import com.fasterxml.jackson.annotation.JsonFormat +import io.swagger.v3.oas.annotations.media.ArraySchema +import io.swagger.v3.oas.annotations.media.Schema +import java.math.BigDecimal +import java.time.LocalDateTime + +data class PaymentHTTPRequest( + val orderInfo: OrderInfo, +) + +data class OrderInfo( + @Schema(title = "Número de pedido", example = "1", required = true) + val number: Long, + @JsonFormat(shape = JsonFormat.Shape.STRING) + @Schema(title = "Valor total", example = "10.00", required = true) + val totalAmount: BigDecimal, + @ArraySchema( + schema = Schema(implementation = OrderLine::class, required = true), + minItems = 1 + ) + val lines: List, + @Schema(title = "Data do pedido", example = "2024-05-19T11:00:00", required = true) + val orderedAt: LocalDateTime, + @Schema(title = "Solicitante", example = "John Doe", required = true) + val orderedBy: String, +) + +data class OrderLine( + @Schema(title = "Nome do item de pedido", example = "Big Mac", required = true) + val name: String, + @JsonFormat(shape = JsonFormat.Shape.STRING) + @Schema(title = "Valor unitário", example = "10.00", required = true) + val unitPrice: BigDecimal, + @Schema(title = "Quantidade", example = "1", required = true) + val quantity: Long, + @Schema(title = "Unidade de medida", example = "UND", required = true) + val unitOfMeasurement: String, + @JsonFormat(shape = JsonFormat.Shape.STRING) + @Schema(title = "Valor total", example = "10.00", required = true) + val totalAmount: BigDecimal, +) diff --git a/src/main/kotlin/com/fiap/payments/driver/web/request/ProductRequest.kt b/src/main/kotlin/com/fiap/payments/driver/web/request/ProductRequest.kt deleted file mode 100644 index 8b70e0f..0000000 --- a/src/main/kotlin/com/fiap/payments/driver/web/request/ProductRequest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.fiap.payments.driver.web.request - -import com.fiap.payments.domain.valueobjects.ProductCategory -import java.math.BigDecimal - -data class ProductRequest( - val number: Long? = null, - val name: String, - val price: BigDecimal, - val description: String, - val category: ProductCategory, -) diff --git a/src/main/kotlin/com/fiap/payments/driver/web/response/OrderToPayResponse.kt b/src/main/kotlin/com/fiap/payments/driver/web/response/OrderToPayResponse.kt deleted file mode 100644 index df28b17..0000000 --- a/src/main/kotlin/com/fiap/payments/driver/web/response/OrderToPayResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.fiap.payments.driver.web.response - -import com.fiap.payments.domain.entities.Order - -data class OrderToPayResponse( - val order: Order, - val paymentInfo: String, -) diff --git a/src/main/kotlin/com/fiap/payments/usecases/ChangePaymentStatusUseCase.kt b/src/main/kotlin/com/fiap/payments/usecases/ChangePaymentStatusUseCase.kt new file mode 100644 index 0000000..2a90647 --- /dev/null +++ b/src/main/kotlin/com/fiap/payments/usecases/ChangePaymentStatusUseCase.kt @@ -0,0 +1,11 @@ +package com.fiap.payments.usecases + +import com.fiap.payments.domain.entities.Payment + +interface ChangePaymentStatusUseCase { + fun confirmPayment(paymentId: String): Payment + + fun failPayment(paymentId: String): Payment + + fun expirePayment(paymentId: String): Payment +} diff --git a/src/main/kotlin/com/fiap/payments/usecases/ConfirmOrderUseCase.kt b/src/main/kotlin/com/fiap/payments/usecases/ConfirmOrderUseCase.kt index a38316f..a09a88b 100644 --- a/src/main/kotlin/com/fiap/payments/usecases/ConfirmOrderUseCase.kt +++ b/src/main/kotlin/com/fiap/payments/usecases/ConfirmOrderUseCase.kt @@ -1,7 +1,5 @@ package com.fiap.payments.usecases -import com.fiap.payments.domain.entities.Order - interface ConfirmOrderUseCase { - fun confirmOrder(orderNumber: Long): Order + fun confirmOrder(orderNumber: Long) } diff --git a/src/main/kotlin/com/fiap/payments/usecases/LoadPaymentUseCase.kt b/src/main/kotlin/com/fiap/payments/usecases/LoadPaymentUseCase.kt index 3e50b42..f9df0d8 100644 --- a/src/main/kotlin/com/fiap/payments/usecases/LoadPaymentUseCase.kt +++ b/src/main/kotlin/com/fiap/payments/usecases/LoadPaymentUseCase.kt @@ -3,9 +3,9 @@ package com.fiap.payments.usecases import com.fiap.payments.domain.entities.Payment interface LoadPaymentUseCase { - fun getByOrderNumber(orderNumber: Long): Payment + fun getByPaymentId(id: String): Payment fun findAll(): List - fun findByOrderNumber(orderNumber: Long): Payment? + fun findByPaymentId(id: String): Payment? } diff --git a/src/main/kotlin/com/fiap/payments/usecases/ProvidePaymentRequestUseCase.kt b/src/main/kotlin/com/fiap/payments/usecases/ProvidePaymentRequestUseCase.kt index c41e989..00bc984 100644 --- a/src/main/kotlin/com/fiap/payments/usecases/ProvidePaymentRequestUseCase.kt +++ b/src/main/kotlin/com/fiap/payments/usecases/ProvidePaymentRequestUseCase.kt @@ -1,8 +1,8 @@ package com.fiap.payments.usecases -import com.fiap.payments.domain.entities.Order -import com.fiap.payments.domain.entities.PaymentRequest +import com.fiap.payments.domain.entities.Payment +import com.fiap.payments.driver.web.request.PaymentHTTPRequest interface ProvidePaymentRequestUseCase { - fun providePaymentRequest(order: Order): PaymentRequest + fun providePaymentRequest(paymentHTTPRequest: PaymentHTTPRequest): Payment } diff --git a/src/main/kotlin/com/fiap/payments/usecases/SyncPaymentUseCase.kt b/src/main/kotlin/com/fiap/payments/usecases/SyncPaymentUseCase.kt index e18eb04..d86164b 100644 --- a/src/main/kotlin/com/fiap/payments/usecases/SyncPaymentUseCase.kt +++ b/src/main/kotlin/com/fiap/payments/usecases/SyncPaymentUseCase.kt @@ -1,6 +1,5 @@ package com.fiap.payments.usecases interface SyncPaymentUseCase { - - fun syncPayment(orderNumber: Long, externalOrderGlobalId: String) + fun syncPayment(paymentId: String, externalOrderGlobalId: String) } diff --git a/src/main/kotlin/com/fiap/payments/usecases/services/OrderService.kt b/src/main/kotlin/com/fiap/payments/usecases/services/OrderService.kt index b9ffd10..e45e5cd 100644 --- a/src/main/kotlin/com/fiap/payments/usecases/services/OrderService.kt +++ b/src/main/kotlin/com/fiap/payments/usecases/services/OrderService.kt @@ -1,7 +1,6 @@ package com.fiap.payments.usecases.services import com.fiap.payments.adapter.gateway.OrderGateway -import com.fiap.payments.domain.entities.Order import com.fiap.payments.usecases.ConfirmOrderUseCase import org.slf4j.LoggerFactory @@ -10,7 +9,7 @@ open class OrderService( ): ConfirmOrderUseCase { private val log = LoggerFactory.getLogger(javaClass) - override fun confirmOrder(orderNumber: Long): Order { + override fun confirmOrder(orderNumber: Long) { log.info("Requesting order [$orderNumber] to be confirmed") return orderGateway.confirmOrder(orderNumber) } diff --git a/src/main/kotlin/com/fiap/payments/usecases/services/PaymentService.kt b/src/main/kotlin/com/fiap/payments/usecases/services/PaymentService.kt index ae41d1a..c755753 100644 --- a/src/main/kotlin/com/fiap/payments/usecases/services/PaymentService.kt +++ b/src/main/kotlin/com/fiap/payments/usecases/services/PaymentService.kt @@ -2,59 +2,114 @@ package com.fiap.payments.usecases.services import com.fiap.payments.adapter.gateway.PaymentGateway import com.fiap.payments.adapter.gateway.PaymentProviderGateway -import com.fiap.payments.domain.entities.Order import com.fiap.payments.domain.entities.Payment -import com.fiap.payments.domain.entities.PaymentRequest import com.fiap.payments.domain.errors.ErrorType import com.fiap.payments.domain.errors.PaymentsException import com.fiap.payments.domain.valueobjects.PaymentStatus +import com.fiap.payments.driver.web.request.PaymentHTTPRequest +import com.fiap.payments.usecases.ChangePaymentStatusUseCase +import com.fiap.payments.usecases.ConfirmOrderUseCase import com.fiap.payments.usecases.LoadPaymentUseCase import com.fiap.payments.usecases.ProvidePaymentRequestUseCase import org.slf4j.LoggerFactory import java.time.LocalDateTime class PaymentService( - private val paymentRepository: PaymentGateway, + private val paymentGateway: PaymentGateway, private val paymentProvider: PaymentProviderGateway, + private val confirmOrderUseCase: ConfirmOrderUseCase, ) : LoadPaymentUseCase, - ProvidePaymentRequestUseCase + ProvidePaymentRequestUseCase, + ChangePaymentStatusUseCase { private val log = LoggerFactory.getLogger(javaClass) - - override fun getByOrderNumber(orderNumber: Long): Payment { - return paymentRepository.findByOrderNumber(orderNumber) + + override fun getByPaymentId(id: String): Payment { + return paymentGateway.findByPaymentId(id) ?: throw PaymentsException( errorType = ErrorType.PAYMENT_NOT_FOUND, - message = "Payment not found for order [$orderNumber]", + message = "Payment [$id] not found", ) } - override fun findByOrderNumber(orderNumber: Long): Payment? { - return paymentRepository.findByOrderNumber(orderNumber) + override fun findByPaymentId(id: String): Payment? { + return paymentGateway.findByPaymentId(id) } override fun findAll(): List { - return paymentRepository.findAll() + return paymentGateway.findAll() } - override fun providePaymentRequest(order: Order): PaymentRequest { - val paymentRequest = paymentProvider.createExternalOrder(order) - log.info("Payment request created for order $order") - - val payment = - Payment( - orderNumber = order.number!!, - externalOrderId = paymentRequest.externalOrderId, - externalOrderGlobalId = null, - paymentInfo = paymentRequest.paymentInfo, - createdAt = LocalDateTime.now(), - status = PaymentStatus.PENDING, - statusChangedAt = LocalDateTime.now(), + override fun providePaymentRequest(paymentHTTPRequest: PaymentHTTPRequest): Payment { + var payment = Payment( + orderNumber = paymentHTTPRequest.orderInfo.number, + createdAt = LocalDateTime.now(), + status = PaymentStatus.PENDING, + statusChangedAt = LocalDateTime.now(), + ) + payment = paymentGateway.upsert(payment) + log.info("Payment $payment stored for order [${payment.orderNumber}]") + + log.info("Requesting payment request for order [${paymentHTTPRequest.orderInfo.number}]") + val paymentRequest = paymentProvider.createExternalOrder(payment.id, paymentHTTPRequest) + + payment = payment.copy( + externalOrderId = paymentRequest.externalOrderId, + externalOrderGlobalId = null, + paymentInfo = paymentRequest.paymentInfo, + ) + paymentGateway.upsert(payment) + + return payment + } + + override fun confirmPayment(paymentId: String): Payment { + return getByPaymentId(paymentId) + .takeIf { it.status == PaymentStatus.PENDING } + ?.let { payment -> + log.info("Confirming payment $payment") + val confirmedPayment = paymentGateway.upsert(payment.copy( + status = PaymentStatus.CONFIRMED, + statusChangedAt = LocalDateTime.now() + )) + confirmOrderUseCase.confirmOrder(confirmedPayment.orderNumber) + confirmedPayment + } + ?: throw PaymentsException( + errorType = ErrorType.INVALID_PAYMENT_STATE_TRANSITION, + message = "Payment can only be confirmed when it is pending", ) + } - paymentRepository.create(payment) - log.info("Payment stored for order $order") + override fun failPayment(paymentId: String): Payment { + return getByPaymentId(paymentId) + .takeIf { it.status == PaymentStatus.PENDING } + ?.let { payment -> + log.info("Failing payment $payment") + paymentGateway.upsert(payment.copy( + status = PaymentStatus.FAILED, + statusChangedAt = LocalDateTime.now() + )) + } + ?: throw PaymentsException( + errorType = ErrorType.INVALID_PAYMENT_STATE_TRANSITION, + message = "Payment can only be failed when it is pending", + ) + } - return paymentRequest + override fun expirePayment(paymentId: String): Payment { + return getByPaymentId(paymentId) + .takeIf { it.status == PaymentStatus.PENDING } + ?.let { payment -> + log.info("Expiring payment $payment") + paymentGateway.upsert(payment.copy( + status = PaymentStatus.EXPIRED, + statusChangedAt = LocalDateTime.now() + )) + } + ?: throw PaymentsException( + errorType = ErrorType.INVALID_PAYMENT_STATE_TRANSITION, + message = "Payment can only be expired when it is pending", + ) } } diff --git a/src/main/kotlin/com/fiap/payments/usecases/services/PaymentSyncService.kt b/src/main/kotlin/com/fiap/payments/usecases/services/PaymentSyncService.kt index 536d8e6..ddb5a3e 100644 --- a/src/main/kotlin/com/fiap/payments/usecases/services/PaymentSyncService.kt +++ b/src/main/kotlin/com/fiap/payments/usecases/services/PaymentSyncService.kt @@ -2,42 +2,41 @@ package com.fiap.payments.usecases.services import com.fiap.payments.adapter.gateway.PaymentGateway import com.fiap.payments.adapter.gateway.PaymentProviderGateway +import com.fiap.payments.domain.errors.ErrorType +import com.fiap.payments.domain.errors.PaymentsException import com.fiap.payments.domain.valueobjects.PaymentStatus -import com.fiap.payments.usecases.ConfirmOrderUseCase +import com.fiap.payments.usecases.ChangePaymentStatusUseCase import com.fiap.payments.usecases.LoadPaymentUseCase import com.fiap.payments.usecases.SyncPaymentUseCase import org.slf4j.LoggerFactory -import java.time.LocalDateTime class PaymentSyncService( private val loadPaymentUseCase: LoadPaymentUseCase, private val paymentGateway: PaymentGateway, private val paymentProviderGateway: PaymentProviderGateway, - private val confirmOrderUseCase: ConfirmOrderUseCase + private val changePaymentStatusUseCase: ChangePaymentStatusUseCase, ): SyncPaymentUseCase { private val log = LoggerFactory.getLogger(javaClass) - override fun syncPayment(orderNumber: Long, externalOrderGlobalId: String) { - val payment = loadPaymentUseCase.getByOrderNumber(orderNumber) + override fun syncPayment(paymentId: String, externalOrderGlobalId: String) { + val payment = loadPaymentUseCase.getByPaymentId(paymentId) if (payment.externalOrderGlobalId == null) { - paymentGateway.update(payment.copy(externalOrderGlobalId = externalOrderGlobalId)) + paymentGateway.upsert(payment.copy(externalOrderGlobalId = externalOrderGlobalId)) } val newStatus = paymentProviderGateway.checkExternalOrderStatus(externalOrderGlobalId) - log.info("Checked payment status for order [$orderNumber]: $newStatus") + log.info("Checked payment status for payment [$paymentId]: $newStatus") if (payment.status != newStatus) { - paymentGateway.update( - payment.copy( - status = newStatus, - statusChangedAt = LocalDateTime.now(), + when (newStatus) { + PaymentStatus.CONFIRMED -> changePaymentStatusUseCase.confirmPayment(paymentId) + PaymentStatus.EXPIRED -> changePaymentStatusUseCase.expirePayment(paymentId) + PaymentStatus.FAILED -> changePaymentStatusUseCase.failPayment(paymentId) + PaymentStatus.PENDING -> throw PaymentsException( + errorType = ErrorType.INVALID_PAYMENT_STATE_TRANSITION, + message = "Payment cannot be transitioned to pending state again" ) - ) - log.info("Changed payment status for order [$orderNumber] from ${payment.status} to $newStatus") - - if (newStatus == PaymentStatus.CONFIRMED) { - confirmOrderUseCase.confirmOrder(orderNumber) } } } diff --git a/src/main/resources/db/migration/localSchema/create_schema.sh b/src/main/resources/db/migration/localSchema/create_schema.sh index f516548..6a9b614 100755 --- a/src/main/resources/db/migration/localSchema/create_schema.sh +++ b/src/main/resources/db/migration/localSchema/create_schema.sh @@ -9,9 +9,11 @@ export AWS_REGION=us-east-1 aws dynamodb create-table \ --table-name payments \ --attribute-definitions \ + AttributeName=payment_id,AttributeType=S \ AttributeName=payment_order_number,AttributeType=S \ --key-schema \ - AttributeName=payment_order_number,KeyType=HASH \ + AttributeName=payment_id,KeyType=HASH \ + AttributeName=payment_order_number,KeyType=RANGE \ --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=10 \ --endpoint-url http://localhost:54000 \ --region us-east-1 || true | cat diff --git a/src/main/resources/db/migration/localSchema/gsi.json b/src/main/resources/db/migration/localSchema/gsi.json index 9f7ca26..4daba8c 100644 --- a/src/main/resources/db/migration/localSchema/gsi.json +++ b/src/main/resources/db/migration/localSchema/gsi.json @@ -1,13 +1,13 @@ [ { - "IndexName": "payment_order_number_payment_created_at", + "IndexName": "payment_id_payment_order_number", "KeySchema": [ { - "AttributeName": "payment_order_number", + "AttributeName": "payment_id", "KeyType": "HASH" }, { - "AttributeName": "payment_created_at", + "AttributeName": "payment_order_number", "KeyType": "RANGE" } ], @@ -19,4 +19,4 @@ "WriteCapacityUnits": 10 } } -] \ No newline at end of file +] diff --git a/src/test/kotlin/TestFixtures.kt b/src/test/kotlin/TestFixtures.kt index bedae9d..b64d36d 100644 --- a/src/test/kotlin/TestFixtures.kt +++ b/src/test/kotlin/TestFixtures.kt @@ -1,106 +1,58 @@ -import com.fiap.payments.domain.entities.* -import com.fiap.payments.domain.valueobjects.OrderStatus +import com.fiap.payments.domain.entities.Payment +import com.fiap.payments.domain.entities.PaymentRequest import com.fiap.payments.domain.valueobjects.PaymentStatus -import com.fiap.payments.domain.valueobjects.ProductCategory +import com.fiap.payments.driver.web.request.OrderInfo +import com.fiap.payments.driver.web.request.OrderLine +import com.fiap.payments.driver.web.request.PaymentHTTPRequest import java.math.BigDecimal -import java.time.LocalDate import java.time.LocalDateTime import java.util.* -fun createCustomer( - id : UUID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"), - document: String = "444.555.666-77", - name: String = "Fulano de Tal", - email: String = "fulano@detal.com", - phone: String = "5511999999999", - address: String = "São Paulo", -) = Customer( - id = id, - document = document, - name = name, - email = email, - phone = phone, - address = address, -) - -fun createProduct( - number: Long = 123, - name: String = "Big Mac", - category: ProductCategory = ProductCategory.MAIN, - price: BigDecimal = BigDecimal("10.00"), - description: String = "Dois hambúrgueres, alface, queijo, molho especial, cebola, picles, num pão com gergelim", - minSub: Int = 3, - maxSub: Int = 3, - subitems: List = listOf(), - components: List = listOf(), -) = Product( - number = number, - name = name, - category = category, - price = price, - description = description, - minSub = minSub, - maxSub = maxSub, - subItems = subitems, - components = components, -) - -fun createStock( - productNumber: Long = 123, - quantity: Long = 100, -) = Stock( - componentNumber = productNumber, - quantity = quantity, +fun createPaymentHTTPRequest( + orderInfo: OrderInfo = createOrderInfo() +) = PaymentHTTPRequest( + orderInfo = orderInfo ) -fun createComponent( - componentNumber: Long = 9870001, - name: String = "Lata refrigerante coca-cola 355ml", -) = Component( - number = componentNumber, - name = name, -) - -fun createOrder( - number: Long? = 98765, - date: LocalDate = LocalDate.parse("2023-10-01"), - customer: Customer? = null, - status: OrderStatus = OrderStatus.CREATED, - items: List = listOf(createProduct()), - total: BigDecimal = BigDecimal("50.00"), -) = Order( +fun createOrderInfo( + number: Long = 1, + orderedAt: LocalDateTime = LocalDateTime.parse("2023-10-01T18:00:00"), + orderedBy: String = "John Doe", + totalAmount: BigDecimal = BigDecimal.valueOf(10) +) = OrderInfo( number = number, - date = date, - customer = customer, - status = status, - items = items, - total = total, -) - -fun createOrderItem( - productNumber: Long = 123, - quantity: Long = 1, -) = OrderItem( - productNumber = productNumber, - quantity = quantity, + orderedAt = orderedAt, + orderedBy = orderedBy, + totalAmount = totalAmount, + lines = listOf( + OrderLine( + name = "Item 1", + quantity = 1, + totalAmount = BigDecimal.valueOf(10), + unitOfMeasurement = "unit", + unitPrice = BigDecimal.valueOf(10), + ) + ) ) fun createPayment( - orderNumber: Long = 98765, + id: String = "445e11cd-c9fa-44ee-80e9-31a6cb1c7339", + orderNumber: Long = 1, externalOrderId: String = "66b0f5f7-9997-4f49-a203-3dab2d936b50", externalOrderGlobalId: String? = null, paymentInfo: String = "00020101021243650016COM.MERCADOLIBRE...", - createdAt: LocalDateTime = LocalDateTime.parse("2023-10-01T18:00:00"), + createdAt: LocalDateTime = LocalDateTime.parse("2023-10-01T18:01:00"), status: PaymentStatus = PaymentStatus.PENDING, - statusChangedAt: LocalDateTime = LocalDateTime.parse("2023-10-01T18:00:00"), + statusChangedAt: LocalDateTime = LocalDateTime.parse("2023-10-01T18:01:00"), ) = Payment( + id = id, orderNumber = orderNumber, externalOrderId = externalOrderId, externalOrderGlobalId = externalOrderGlobalId, paymentInfo = paymentInfo, createdAt = createdAt, status = status, - statusChangedAt, + statusChangedAt = statusChangedAt, ) fun createPaymentRequest( diff --git a/src/test/kotlin/com/fiap/payments/application/services/PaymentServiceTest.kt b/src/test/kotlin/com/fiap/payments/application/services/PaymentServiceTest.kt index 605e510..7b381c0 100644 --- a/src/test/kotlin/com/fiap/payments/application/services/PaymentServiceTest.kt +++ b/src/test/kotlin/com/fiap/payments/application/services/PaymentServiceTest.kt @@ -4,8 +4,10 @@ import com.fiap.payments.adapter.gateway.PaymentGateway import com.fiap.payments.adapter.gateway.PaymentProviderGateway import com.fiap.payments.domain.errors.ErrorType import com.fiap.payments.domain.errors.PaymentsException -import createOrder +import com.fiap.payments.usecases.ConfirmOrderUseCase +import com.fiap.payments.usecases.services.PaymentService import createPayment +import createPaymentHTTPRequest import createPaymentRequest import io.mockk.every import io.mockk.mockk @@ -16,16 +18,17 @@ import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import com.fiap.payments.usecases.services.PaymentService class PaymentServiceTest { private val paymentRepository = mockk() private val paymentProvider = mockk() + private val confirmOrderUseCase = mockk() private val paymentService = PaymentService( paymentRepository, paymentProvider, + confirmOrderUseCase ) @AfterEach @@ -36,23 +39,23 @@ class PaymentServiceTest { @Nested inner class GetByOrderNumberTest { @Test - fun `getByOrderNumberTest should return a Payment when it exists`() { + fun `should return a payment when it exists`() { val payment = createPayment() - every { paymentRepository.findByOrderNumber(payment.orderNumber) } returns payment + every { paymentRepository.findByPaymentId(payment.id) } returns payment - val result = paymentService.getByOrderNumber(payment.orderNumber) + val result = paymentService.getByPaymentId(payment.id) assertThat(result).isEqualTo(payment) } @Test - fun `getByOrderNumberTest should throw an exception when the payment is not found`() { - val orderNumber = 98765L + fun `should throw an exception when payment is not found`() { + val paymentId = "5019af79-11c8-4100-9d3c-e98563b1c52c" - every { paymentRepository.findByOrderNumber(orderNumber) } returns null + every { paymentRepository.findByPaymentId(paymentId) } returns null - assertThatThrownBy { paymentService.getByOrderNumber(orderNumber) } + assertThatThrownBy { paymentService.getByPaymentId(paymentId) } .isInstanceOf(PaymentsException::class.java) .hasFieldOrPropertyWithValue("errorType", ErrorType.PAYMENT_NOT_FOUND) } @@ -61,21 +64,21 @@ class PaymentServiceTest { @Nested inner class ProvidePaymentRequestTest { @Test - fun `providePaymentRequest should create a new PaymentRequest and a corresponding Payment`() { - val order = createOrder() - + fun `should create payment`() { + val paymentHTTPRequest = createPaymentHTTPRequest() + val payment = createPayment() val paymentRequest = createPaymentRequest() - every { paymentProvider.createExternalOrder(order) } returns paymentRequest - every { paymentRepository.create(any()) } answers { firstArg() } + every { paymentRepository.upsert(any()) } returns payment + every { paymentProvider.createExternalOrder(payment.id, paymentHTTPRequest) } returns paymentRequest - val result = paymentService.providePaymentRequest(order) + val result = paymentService.providePaymentRequest(paymentHTTPRequest) assertThat(result).isNotNull() assertThat(result.externalOrderId).isEqualTo(paymentRequest.externalOrderId) assertThat(result.paymentInfo).isEqualTo(paymentRequest.paymentInfo) - verify { paymentRepository.create(any()) } + verify { paymentRepository.upsert(any()) } } } } diff --git a/terraform/dynamodb.tf b/terraform/dynamodb.tf index 0ca9547..5611393 100644 --- a/terraform/dynamodb.tf +++ b/terraform/dynamodb.tf @@ -3,10 +3,15 @@ module "dynamodb_table" { version = "4.0.1" name = "payments" - hash_key = "payment_order_number" + hash_key = "payment_id" + range_key = "payment_order_number" table_class = "STANDARD" attributes = [ + { + name = "payment_id" + type = "S" + }, { name = "payment_order_number" type = "S"