From 83375cf9cc44e88a254bfb815a4514afce349048 Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Fri, 12 Sep 2025 15:11:57 +0200 Subject: [PATCH] update TransactionService with withdrawal --- .../ltd/lulz/service/TransactionService.kt | 13 ++ .../lulz/service/TransactionServiceTest.kt | 131 +++++++++++++----- 2 files changed, 113 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/ltd/lulz/service/TransactionService.kt b/src/main/kotlin/ltd/lulz/service/TransactionService.kt index ca56cbd..db13c24 100644 --- a/src/main/kotlin/ltd/lulz/service/TransactionService.kt +++ b/src/main/kotlin/ltd/lulz/service/TransactionService.kt @@ -2,7 +2,9 @@ package ltd.lulz.service import io.github.oshai.kotlinlogging.KotlinLogging import java.math.BigDecimal +import java.math.BigDecimal.ZERO import java.util.UUID +import ltd.lulz.exception.InsufficientFundsException import ltd.lulz.model.AccountEntity import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -23,4 +25,15 @@ class TransactionService( .map { it.copy(amount = it.amount + amount) } .doOnNext { log.trace { "Deposited $amount to account ${it.id}" } } .flatMap { accountService.save(it) } + + @Transactional + fun withdrawal( + account: UUID, + amount: BigDecimal, + ): Mono = 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) } } diff --git a/src/test/kotlin/ltd/lulz/service/TransactionServiceTest.kt b/src/test/kotlin/ltd/lulz/service/TransactionServiceTest.kt index de42337..eee291b 100644 --- a/src/test/kotlin/ltd/lulz/service/TransactionServiceTest.kt +++ b/src/test/kotlin/ltd/lulz/service/TransactionServiceTest.kt @@ -7,9 +7,11 @@ import io.mockk.verify import java.math.BigDecimal import java.util.UUID import ltd.lulz.exception.AccountNotFoundException +import ltd.lulz.exception.InsufficientFundsException import ltd.lulz.model.AccountEntity import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import reactor.core.publisher.Mono import reactor.test.StepVerifier @@ -31,44 +33,111 @@ class TransactionServiceTest { service = TransactionService(accountService) } - @Test - fun `deposit to account - success`() { - // given - val deposit = BigDecimal.valueOf(1.10) + @Nested + inner class Deposit { - val capture = slot() - every { accountService.getForUpdateById(capture(capture)) } - .answers { Mono.just(AccountEntity(capture.captured, name, amount)) } - val entity = slot() - every { accountService.save(capture(entity)) } - .answers { Mono.just(entity.captured) } + @Test + fun `deposit to account - success`() { + // given + val deposit = BigDecimal.valueOf(1.10) - // when stepped - 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() + val capture = slot() + every { accountService.getForUpdateById(capture(capture)) } + .answers { Mono.just(AccountEntity(capture.captured, name, amount)) } + val entity = slot() + every { accountService.save(capture(entity)) } + .answers { Mono.just(entity.captured) } - verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) } - verify(exactly = 1) { accountService.save(any()) } + // when stepped + 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 - fun `deposit to account - account not found`() { - // given - val deposit = BigDecimal.valueOf(1.10) + @Nested + inner class Withdrawal { - every { accountService.getForUpdateById(any()) } returns Mono.error(AccountNotFoundException()) + @Test + fun `withdrawal from account - success`() { + // given + val deposit = BigDecimal.valueOf(1.01) - // when stepped - StepVerifier.create(service.deposit(uuid, deposit)) - .expectError(AccountNotFoundException::class.java) - .verify() + val capture = slot() + every { accountService.getForUpdateById(capture(capture)) } + .answers { Mono.just(AccountEntity(capture.captured, name, amount)) } + val entity = slot() + every { accountService.save(capture(entity)) } + .answers { Mono.just(entity.captured) } - verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) } - verify(exactly = 0) { accountService.save(any()) } + // when stepped + 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() + 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()) } + } } }