update TransactionService with withdrawal

This commit is contained in:
2025-09-12 15:11:57 +02:00
parent adfe96720b
commit f9a5cd5922
2 changed files with 113 additions and 31 deletions

View File

@@ -2,7 +2,9 @@ package ltd.lulz.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import java.math.BigDecimal import java.math.BigDecimal
import java.math.BigDecimal.ZERO
import java.util.UUID import java.util.UUID
import ltd.lulz.exception.InsufficientFundsException
import ltd.lulz.model.AccountEntity import ltd.lulz.model.AccountEntity
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@@ -23,4 +25,15 @@ class TransactionService(
.map { it.copy(amount = it.amount + amount) } .map { it.copy(amount = it.amount + amount) }
.doOnNext { log.trace { "Deposited $amount to account ${it.id}" } } .doOnNext { log.trace { "Deposited $amount to account ${it.id}" } }
.flatMap { accountService.save(it) } .flatMap { accountService.save(it) }
@Transactional
fun withdrawal(
account: UUID,
amount: BigDecimal,
): Mono<AccountEntity> = accountService.getForUpdateById(account)
.map { it.copy(amount = it.amount - amount) }
.filter { it.amount >= ZERO }
.doOnNext { log.trace { "withdrawal $amount to account ${it.id}" } }
.switchIfEmpty(Mono.error(InsufficientFundsException()))
.flatMap { accountService.save(it) }
} }

View File

@@ -7,9 +7,11 @@ import io.mockk.verify
import java.math.BigDecimal import java.math.BigDecimal
import java.util.UUID import java.util.UUID
import ltd.lulz.exception.AccountNotFoundException import ltd.lulz.exception.AccountNotFoundException
import ltd.lulz.exception.InsufficientFundsException
import ltd.lulz.model.AccountEntity import ltd.lulz.model.AccountEntity
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.test.StepVerifier import reactor.test.StepVerifier
@@ -31,44 +33,111 @@ class TransactionServiceTest {
service = TransactionService(accountService) service = TransactionService(accountService)
} }
@Test @Nested
fun `deposit to account - success`() { inner class Deposit {
// given
val deposit = BigDecimal.valueOf(1.10)
val capture = slot<UUID>() @Test
every { accountService.getForUpdateById(capture(capture)) } fun `deposit to account - success`() {
.answers { Mono.just(AccountEntity(capture.captured, name, amount)) } // given
val entity = slot<AccountEntity>() val deposit = BigDecimal.valueOf(1.10)
every { accountService.save(capture(entity)) }
.answers { Mono.just(entity.captured) }
// when stepped val capture = slot<UUID>()
StepVerifier.create(service.deposit(uuid, deposit)) every { accountService.getForUpdateById(capture(capture)) }
.assertNext { result -> .answers { Mono.just(AccountEntity(capture.captured, name, amount)) }
assertThat(result.id).isEqualTo(uuid) val entity = slot<AccountEntity>()
assertThat(result.name).isEqualTo(name) every { accountService.save(capture(entity)) }
assertThat(result.amount).isEqualTo(amount + deposit) .answers { Mono.just(entity.captured) }
}
.verifyComplete()
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) } // when stepped
verify(exactly = 1) { accountService.save(any()) } StepVerifier.create(service.deposit(uuid, deposit))
.assertNext { result ->
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount + deposit)
}
.verifyComplete()
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
verify(exactly = 1) { accountService.save(any()) }
}
@Test
fun `deposit to account - account not found`() {
// given
val deposit = BigDecimal.valueOf(1.10)
every { accountService.getForUpdateById(any()) } returns Mono.error(AccountNotFoundException())
// when stepped
StepVerifier.create(service.deposit(uuid, deposit))
.expectError(AccountNotFoundException::class.java)
.verify()
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
verify(exactly = 0) { accountService.save(any()) }
}
} }
@Test @Nested
fun `deposit to account - account not found`() { inner class Withdrawal {
// given
val deposit = BigDecimal.valueOf(1.10)
every { accountService.getForUpdateById(any()) } returns Mono.error(AccountNotFoundException()) @Test
fun `withdrawal from account - success`() {
// given
val deposit = BigDecimal.valueOf(1.01)
// when stepped val capture = slot<UUID>()
StepVerifier.create(service.deposit(uuid, deposit)) every { accountService.getForUpdateById(capture(capture)) }
.expectError(AccountNotFoundException::class.java) .answers { Mono.just(AccountEntity(capture.captured, name, amount)) }
.verify() val entity = slot<AccountEntity>()
every { accountService.save(capture(entity)) }
.answers { Mono.just(entity.captured) }
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) } // when stepped
verify(exactly = 0) { accountService.save(any()) } StepVerifier.create(service.withdrawal(uuid, deposit))
.assertNext { result ->
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount - deposit)
}
.verifyComplete()
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
verify(exactly = 1) { accountService.save(any()) }
}
@Test
fun `withdrawal from account - insufficient founds`() {
// given
val deposit = BigDecimal.valueOf(1.10)
val capture = slot<UUID>()
every { accountService.getForUpdateById(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, name, amount)) }
// when stepped
StepVerifier.create(service.withdrawal(uuid, deposit))
.expectError(InsufficientFundsException::class.java)
.verify()
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
verify(exactly = 0) { accountService.save(any()) }
}
@Test
fun `withdrawal from account - account not found`() {
// given
val deposit = BigDecimal.valueOf(1.10)
every { accountService.getForUpdateById(any()) } returns Mono.error(AccountNotFoundException())
// when stepped
StepVerifier.create(service.withdrawal(uuid, deposit))
.expectError(AccountNotFoundException::class.java)
.verify()
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
verify(exactly = 0) { accountService.save(any()) }
}
} }
} }