Skip to content

Commit

Permalink
Merge pull request #65 from KjellBerlin/Introduce-complex-payment-sta…
Browse files Browse the repository at this point in the history
…tuses

Introduce complex payment statuses
  • Loading branch information
KjellBerlin authored Aug 28, 2024
2 parents 8c0bc63 + ae798c9 commit 650833b
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 39 deletions.
2 changes: 1 addition & 1 deletion src/main/kotlin/com/carbonara/core/order/OrderQuery.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ class OrderQuery(

@GraphQLDescription("Get all paid orders of an user")
suspend fun paidOrders(userId: String): List<OrderDto> {
return orderService.getPaidOrdersByAuth0UserId(userId)
return orderService.getNonPendingOrdersByAuth0UserId(userId)
}
}
6 changes: 4 additions & 2 deletions src/main/kotlin/com/carbonara/core/order/OrderRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ interface OrderRepository: ReactiveMongoRepository<OrderDao, ObjectId> {
@Query("{'paymentDetails.paymentId': ?0}")
fun findFirstByPaymentId(paymentId: String): Mono<OrderDao>

@Query("{'auth0UserId': ?0, 'paymentDetails.paid': true}")
fun findAllByAuth0UserIdAndPaid(auth0UserId: String): Flux<OrderDao>
@Query("{'auth0UserId': ?0, 'paymentDetails.internalPaymentStatus': { \$in: ?1 }}")
fun findAllByAuth0UserIdAndPaymentStatuses(auth0UserId: String, paymentStatuses: List<String>): Flux<OrderDao>


}
55 changes: 40 additions & 15 deletions src/main/kotlin/com/carbonara/core/order/OrderService.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.carbonara.core.order

import be.woutschoovaerts.mollie.data.payment.PaymentStatus
import com.carbonara.core.payment.InternalPaymentStatus
import com.carbonara.core.payment.MolliePaymentService
import com.carbonara.core.payment.PaymentException
import com.carbonara.core.product.ProductDao
Expand Down Expand Up @@ -52,26 +53,22 @@ class OrderService(
suspend fun handleOrderPayment(
paymentId: String
) {
val paymentStatus = molliePaymentService.getMolliePaymentStatus(paymentId)
if (paymentStatus == PaymentStatus.PAID) {
val order = retrieveOrderFromDatabase(paymentId)

if (!order.paymentDetails.paid) {
updateOrderToPaid(order, paymentStatus)

// TODO: trigger delivery
}
} else {
log.info("Retrieved payment status={} for paymentId={}. Not processing order for now further",
when(val paymentStatus = molliePaymentService.getMolliePaymentStatus(paymentId)) {
PaymentStatus.PAID -> handlePaidOrder(paymentId, paymentStatus)
PaymentStatus.CANCELED -> handleUnpaidOrder(paymentId, paymentStatus)
PaymentStatus.FAILED -> handleUnpaidOrder(paymentId, paymentStatus)
else -> log.info("Retrieved payment status={} for paymentId={}. Not processing order for now further",
paymentStatus, paymentId)
}
}

suspend fun getPaidOrdersByAuth0UserId(
suspend fun getNonPendingOrdersByAuth0UserId(
auth0UserId: String
): List<OrderDto> {
return orderRepository.findAllByAuth0UserIdAndPaid(auth0UserId)
.collectList().awaitSingleOrNull()?.map { it.toOrderDto() } ?: emptyList()
return orderRepository.findAllByAuth0UserIdAndPaymentStatuses(
auth0UserId = auth0UserId,
paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name)
).collectList().awaitSingleOrNull()?.map { it.toOrderDto() } ?: emptyList()
}

private fun createPaymentDescription(products: List<ProductDao>): String {
Expand All @@ -93,10 +90,25 @@ class OrderService(
}
}

private suspend fun handlePaidOrder(paymentId: String, paymentStatus: PaymentStatus) {
val order = retrieveOrderFromDatabase(paymentId)

if (order.paymentDetails.internalPaymentStatus == InternalPaymentStatus.PAID) {
updateOrderToPaid(order, paymentStatus)

// TODO: trigger delivery
}
}

private suspend fun handleUnpaidOrder(paymentId: String, paymentStatus: PaymentStatus) {
val order = retrieveOrderFromDatabase(paymentId)
updateOrderToFailed(order, paymentStatus)
}

private suspend fun updateOrderToPaid(order: OrderDao, paymentStatus: PaymentStatus) {
log.info("Retrieved payment status={} for orderId={}, now processing order", paymentStatus, order.orderId)
val updatedOrder = order.copy(
paymentDetails = order.paymentDetails.copy(paid = true),
paymentDetails = order.paymentDetails.copy(internalPaymentStatus = InternalPaymentStatus.PAID),
updatedAt = OffsetDateTime.now().toString(),
orderStatus = OrderStatus.PROCESSING_ORDER
)
Expand All @@ -106,6 +118,19 @@ class OrderService(
}
}

private suspend fun updateOrderToFailed(order: OrderDao, paymentStatus: PaymentStatus) {
log.info("Retrieved payment status={} for orderId={}, now processing order", paymentStatus, order.orderId)
val updatedOrder = order.copy(
paymentDetails = order.paymentDetails.copy(internalPaymentStatus = InternalPaymentStatus.FAILED),
updatedAt = OffsetDateTime.now().toString(),
orderStatus = OrderStatus.CANCELLED
)
orderRepository.save(updatedOrder).awaitSingleOrNull() ?: run {
log.error("Failed to update payment status to failed for orderId={}", order.orderId)
throw PaymentException("Failed to update payment status")
}
}

companion object {
private val log = KotlinLogging.logger {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class MolliePaymentService {
PaymentDetails(
paymentId = molliePayment.id,
paymentRedirectLink = molliePayment.links.checkout.href,
paid = false
internalPaymentStatus = InternalPaymentStatus.PENDING
)
} catch (ex: MollieException) {
log.error("Failed to create payment for user={} and amount in cents={}, details={}", userId, amountInCents, ex.details)
Expand Down
8 changes: 7 additions & 1 deletion src/main/kotlin/com/carbonara/core/payment/PaymentDetails.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@ package com.carbonara.core.payment
data class PaymentDetails(
val paymentRedirectLink: String,
val paymentId: String,
val paid: Boolean
val internalPaymentStatus: InternalPaymentStatus,
)

enum class InternalPaymentStatus {
PENDING,
PAID,
FAILED
}
58 changes: 39 additions & 19 deletions src/test/kotlin/com/carbonara/core/order/OrderServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.carbonara.core.order

import be.woutschoovaerts.mollie.data.payment.PaymentStatus
import com.carbonara.core.address.Address
import com.carbonara.core.payment.InternalPaymentStatus
import com.carbonara.core.payment.PaymentDetails
import com.carbonara.core.payment.MolliePaymentService
import com.carbonara.core.payment.PaymentException
Expand Down Expand Up @@ -49,7 +50,7 @@ class OrderServiceTest {
fun `Happy case`() {
coEvery { productService.getProductDaosByIds(any()) } returns listOf(TEST_PRODUCT)
every { orderRepository.save(any()) } returns ORDER_DAO.toMono()
every { molliePaymentService.createMolliePaymentLink(any(), any(), any()) } returns MOLLIE_PAYMENT_DETAILS
every { molliePaymentService.createMolliePaymentLink(any(), any(), any()) } returns PAYMENT_DETAILS

val result = runBlocking { orderService.createOrder(CREATE_ORDER_INPUT) }
assertEquals(ORDER_DAO.toOrderDto(), result)
Expand All @@ -68,7 +69,7 @@ class OrderServiceTest {
fun `Null return form database`() {
coEvery { productService.getProductDaosByIds(any()) } returns listOf(TEST_PRODUCT)
every { orderRepository.save(any()) } returns null.toMono()
every { molliePaymentService.createMolliePaymentLink(any(), any(), any()) } returns MOLLIE_PAYMENT_DETAILS
every { molliePaymentService.createMolliePaymentLink(any(), any(), any()) } returns PAYMENT_DETAILS

assertThrows<OrderCreationException> {
runBlocking { orderService.createOrder(CREATE_ORDER_INPUT) }
Expand Down Expand Up @@ -105,13 +106,13 @@ class OrderServiceTest {
fun `Status not paid`() {
every { molliePaymentService.getMolliePaymentStatus(any()) } returns PaymentStatus.FAILED
coEvery { orderRepository.findFirstByPaymentId(any()) } returns ORDER_DAO.toMono()
coEvery { orderRepository.save(any()) } returns ORDER_DAO_PAID.toMono()
coEvery { orderRepository.save(any()) } returns ORDER_DAO_PAYMENT_FAILED.toMono()

runBlocking { orderService.handleOrderPayment(PAYMENT_ID) }

verify(exactly = 1) { molliePaymentService.getMolliePaymentStatus(PAYMENT_ID) }
coVerify(exactly = 0) { orderRepository.findFirstByPaymentId(PAYMENT_ID)}
coVerify(exactly = 0) { orderRepository.save(ORDER_DAO_PAID) }
coVerify(exactly = 1) { orderRepository.findFirstByPaymentId(PAYMENT_ID)}
coVerify(exactly = 1) { orderRepository.save(ORDER_DAO_PAYMENT_FAILED) }
}

@Test
Expand Down Expand Up @@ -144,26 +145,38 @@ class OrderServiceTest {
}

@Nested
inner class GetPaidOrdersByAuth0UserIdTests {
inner class GetNonPendingOrdersByAuth0UserIdTests {

@Test
fun `Happy case`() {
coEvery { orderRepository.findAllByAuth0UserIdAndPaid(AUTH0_USER_ID) } returns listOf(ORDER_DAO).toFlux()

val result = runBlocking { orderService.getPaidOrdersByAuth0UserId(AUTH0_USER_ID) }
assertEquals(listOf(ORDER_DAO.toOrderDto()), result)

coVerify(exactly = 1) { orderRepository.findAllByAuth0UserIdAndPaid(AUTH0_USER_ID) }
coEvery { orderRepository.findAllByAuth0UserIdAndPaymentStatuses(
auth0UserId = AUTH0_USER_ID,
paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name)
) } returns listOf(ORDER_DAO_PAID, ORDER_DAO_PAYMENT_FAILED).toFlux()

val result = runBlocking { orderService.getNonPendingOrdersByAuth0UserId(AUTH0_USER_ID) }
assertEquals(listOf(ORDER_DAO_PAID.toOrderDto(), ORDER_DAO_PAYMENT_FAILED.toOrderDto()), result)

coVerify(exactly = 1) { orderRepository.findAllByAuth0UserIdAndPaymentStatuses(
auth0UserId = AUTH0_USER_ID,
paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name)
) }
}

@Test
fun `No orders found`() {
coEvery { orderRepository.findAllByAuth0UserIdAndPaid(AUTH0_USER_ID) } returns emptyList<OrderDao>().toFlux()
coEvery { orderRepository.findAllByAuth0UserIdAndPaymentStatuses(
auth0UserId = AUTH0_USER_ID,
paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name)
) } returns emptyList<OrderDao>().toFlux()

val result = runBlocking { orderService.getPaidOrdersByAuth0UserId(AUTH0_USER_ID) }
val result = runBlocking { orderService.getNonPendingOrdersByAuth0UserId(AUTH0_USER_ID) }
assertEquals(emptyList<OrderDao>(), result)

coVerify(exactly = 1) { orderRepository.findAllByAuth0UserIdAndPaid(AUTH0_USER_ID) }
coVerify(exactly = 1) { orderRepository.findAllByAuth0UserIdAndPaymentStatuses(
auth0UserId = AUTH0_USER_ID,
paymentStatuses = listOf(InternalPaymentStatus.PAID.name, InternalPaymentStatus.FAILED.name)
) }
}
}

Expand Down Expand Up @@ -197,10 +210,10 @@ class OrderServiceTest {
productsIds = listOf(PRODUCT_ID.toString()),
additionalDetails = "No additional details"
)
val MOLLIE_PAYMENT_DETAILS = PaymentDetails(
val PAYMENT_DETAILS = PaymentDetails(
paymentId = PAYMENT_ID,
paymentRedirectLink = "https://example.com",
paid = false
internalPaymentStatus = InternalPaymentStatus.PENDING
)
val ORDER_DAO = OrderDao(
orderId = ObjectId(),
Expand All @@ -209,11 +222,18 @@ class OrderServiceTest {
deliveryAddress = CREATE_ORDER_INPUT.deliveryAddress,
products = listOf(TEST_PRODUCT),
additionalDetails = CREATE_ORDER_INPUT.additionalDetails,
paymentDetails = MOLLIE_PAYMENT_DETAILS,
paymentDetails = PAYMENT_DETAILS,
orderStatus = OrderStatus.PROCESSING_ORDER,
createdAt = TIME.toString(),
updatedAt = TIME.toString()
)
val ORDER_DAO_PAID = ORDER_DAO.copy(paymentDetails = ORDER_DAO.paymentDetails.copy(paid = true))
val ORDER_DAO_PAID = ORDER_DAO.copy(
paymentDetails = ORDER_DAO.paymentDetails.copy(internalPaymentStatus = InternalPaymentStatus.PAID)
)
val ORDER_DAO_PAYMENT_FAILED = ORDER_DAO.copy(
paymentDetails = ORDER_DAO.paymentDetails.copy(
internalPaymentStatus = InternalPaymentStatus.FAILED),
orderStatus = OrderStatus.CANCELLED
)
}
}

0 comments on commit 650833b

Please sign in to comment.