From 44aafdb505cfa71633f567b700cd24386374d4ce Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Sat, 13 Sep 2025 03:40:18 +0200 Subject: [PATCH] update TransactionService with transfer --- .../ltd/lulz/service/TransactionService.kt | 9 ++ .../lulz/service/TransactionServiceTest.kt | 128 +++++++++++++++--- 2 files changed, 120 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/ltd/lulz/service/TransactionService.kt b/src/main/kotlin/ltd/lulz/service/TransactionService.kt index db13c24..7be5026 100644 --- a/src/main/kotlin/ltd/lulz/service/TransactionService.kt +++ b/src/main/kotlin/ltd/lulz/service/TransactionService.kt @@ -36,4 +36,13 @@ class TransactionService( .doOnNext { log.trace { "withdrawal $amount to account ${it.id}" } } .switchIfEmpty(Mono.error(InsufficientFundsException())) .flatMap { accountService.save(it) } + + @Transactional + fun transfer( + account: UUID, + receiver: UUID, + amount: BigDecimal, + ): Mono = withdrawal(account, amount) + .zipWith(deposit(receiver, amount)) + .then() } diff --git a/src/test/kotlin/ltd/lulz/service/TransactionServiceTest.kt b/src/test/kotlin/ltd/lulz/service/TransactionServiceTest.kt index eee291b..72464bd 100644 --- a/src/test/kotlin/ltd/lulz/service/TransactionServiceTest.kt +++ b/src/test/kotlin/ltd/lulz/service/TransactionServiceTest.kt @@ -20,9 +20,12 @@ import reactor.test.StepVerifier class TransactionServiceTest { companion object { - val name: String = "some name" - val amount: BigDecimal = BigDecimal.valueOf(1.01) - val uuid: UUID = UUID.fromString("00000000-0000-0000-0000-000000000000") + val accountName: String = "some name" + val accountAmount: BigDecimal = BigDecimal.valueOf(1.01) + val accountUuid: UUID = UUID.fromString("00000000-0000-0000-0000-000000000000") + val receiverName: String = "different name" + val receiverAmount: BigDecimal = BigDecimal.valueOf(1.01) + val receiverUuid: UUID = UUID.fromString("00000000-0000-0000-0000-000000000001") } private val accountService: AccountService = mockk() @@ -43,17 +46,17 @@ class TransactionServiceTest { val capture = slot() every { accountService.getForUpdateById(capture(capture)) } - .answers { Mono.just(AccountEntity(capture.captured, name, amount)) } + .answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } val entity = slot() every { accountService.save(capture(entity)) } .answers { Mono.just(entity.captured) } // when stepped - StepVerifier.create(service.deposit(uuid, deposit)) + StepVerifier.create(service.deposit(accountUuid, deposit)) .assertNext { result -> - assertThat(result.id).isEqualTo(uuid) - assertThat(result.name).isEqualTo(name) - assertThat(result.amount).isEqualTo(amount + deposit) + assertThat(result.id).isEqualTo(accountUuid) + assertThat(result.name).isEqualTo(accountName) + assertThat(result.amount).isEqualTo(accountAmount + deposit) } .verifyComplete() @@ -69,7 +72,7 @@ class TransactionServiceTest { every { accountService.getForUpdateById(any()) } returns Mono.error(AccountNotFoundException()) // when stepped - StepVerifier.create(service.deposit(uuid, deposit)) + StepVerifier.create(service.deposit(accountUuid, deposit)) .expectError(AccountNotFoundException::class.java) .verify() @@ -88,17 +91,17 @@ class TransactionServiceTest { val capture = slot() every { accountService.getForUpdateById(capture(capture)) } - .answers { Mono.just(AccountEntity(capture.captured, name, amount)) } + .answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } val entity = slot() every { accountService.save(capture(entity)) } .answers { Mono.just(entity.captured) } // when stepped - StepVerifier.create(service.withdrawal(uuid, deposit)) + StepVerifier.create(service.withdrawal(accountUuid, deposit)) .assertNext { result -> - assertThat(result.id).isEqualTo(uuid) - assertThat(result.name).isEqualTo(name) - assertThat(result.amount).isEqualTo(amount - deposit) + assertThat(result.id).isEqualTo(accountUuid) + assertThat(result.name).isEqualTo(accountName) + assertThat(result.amount).isEqualTo(accountAmount - deposit) } .verifyComplete() @@ -113,10 +116,10 @@ class TransactionServiceTest { val capture = slot() every { accountService.getForUpdateById(capture(capture)) } - .answers { Mono.just(AccountEntity(capture.captured, name, amount)) } + .answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } // when stepped - StepVerifier.create(service.withdrawal(uuid, deposit)) + StepVerifier.create(service.withdrawal(accountUuid, deposit)) .expectError(InsufficientFundsException::class.java) .verify() @@ -132,7 +135,7 @@ class TransactionServiceTest { every { accountService.getForUpdateById(any()) } returns Mono.error(AccountNotFoundException()) // when stepped - StepVerifier.create(service.withdrawal(uuid, deposit)) + StepVerifier.create(service.withdrawal(accountUuid, deposit)) .expectError(AccountNotFoundException::class.java) .verify() @@ -140,4 +143,95 @@ class TransactionServiceTest { verify(exactly = 0) { accountService.save(any()) } } } + + @Nested + inner class Transfer { + + @Test + fun `transfer from account - success`() { + // given + val transfer = BigDecimal.valueOf(1.00) + + val capture = slot() + every { accountService.getForUpdateById(capture(capture)) } + .answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } + .andThenAnswer { Mono.just(AccountEntity(capture.captured, receiverName, receiverAmount)) } + val entity = slot() + every { accountService.save(capture(entity)) } + .answers { Mono.just(entity.captured) } + + // when stepped + StepVerifier.create(service.transfer(accountUuid, receiverUuid, transfer)) + .verifyComplete() + + verify(exactly = 2) { accountService.getForUpdateById(any(UUID::class)) } + verify(exactly = 2) { accountService.save(any()) } + } + + @Test + fun `transfer from account - insufficient founds`() { + // given + val transfer = BigDecimal.valueOf(5.00) + + val capture = slot() + every { accountService.getForUpdateById(capture(capture)) } + .answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } + .andThenAnswer { Mono.just(AccountEntity(capture.captured, receiverName, receiverAmount)) } + val entity = slot() + every { accountService.save(capture(entity)) } + .answers { Mono.just(entity.captured) } + + // when stepped + StepVerifier.create(service.transfer(accountUuid, receiverUuid, transfer)) + .expectError(InsufficientFundsException::class.java) + .verify() + + verify(exactly = 2) { accountService.getForUpdateById(any(UUID::class)) } + verify(exactly = 0) { accountService.save(any()) } + } + + @Test + fun `transfer from account - account not found`() { + // given + val transfer = BigDecimal.valueOf(1.00) + + val capture = slot() + every { accountService.getForUpdateById(capture(capture)) } + .answers { Mono.error(AccountNotFoundException()) } + .andThenAnswer { Mono.just(AccountEntity(capture.captured, receiverName, receiverAmount)) } + val entity = slot() + every { accountService.save(capture(entity)) } + .answers { Mono.just(entity.captured) } + + // when stepped + StepVerifier.create(service.transfer(accountUuid, receiverUuid, transfer)) + .expectError(AccountNotFoundException::class.java) + .verify() + + verify(exactly = 2) { accountService.getForUpdateById(any(UUID::class)) } + verify(exactly = 0) { accountService.save(any()) } + } + + @Test + fun `transfer from account - receiver not found`() { + // given + val transfer = BigDecimal.valueOf(1.00) + + val capture = slot() + every { accountService.getForUpdateById(capture(capture)) } + .answers { Mono.just(AccountEntity(capture.captured, accountName, accountAmount)) } + .andThenAnswer { Mono.error(AccountNotFoundException()) } + val entity = slot() + every { accountService.save(capture(entity)) } + .answers { Mono.just(entity.captured) } + + // when stepped + StepVerifier.create(service.transfer(accountUuid, receiverUuid, transfer)) + .expectError(AccountNotFoundException::class.java) + .verify() + + verify(exactly = 2) { accountService.getForUpdateById(any(UUID::class)) } + verify(exactly = 1) { accountService.save(any()) } + } + } }