Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

19 - deposit money to bank account #39

Merged
merged 12 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions demo-transactions/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.nhaarman.mockitokotlin2</groupId>
<artifactId>mockito-kotlin</artifactId>
<version>${mockito-kotlin.version}</version>
<groupId>io.mockk</groupId>
<artifactId>mockk-jvm</artifactId>
<version>${mockk.version}</version>
<scope>test</scope>
</dependency>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.holixon.cqrshexagonaldemo.demoparent.transactions.adapter

import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.exception.AccountNotFoundException
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.exception.InvalidInputException
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class GlobalExceptionHandler {

@ExceptionHandler(AccountNotFoundException::class)
fun handleException(request: HttpServletRequest, exception: AccountNotFoundException): ResponseEntity<Unit> {
return ResponseEntity.notFound().build()
}

@ExceptionHandler(InvalidInputException::class)
fun handleException(request: HttpServletRequest, exception: InvalidInputException): ResponseEntity<Unit> {
return ResponseEntity.badRequest().build()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@ import io.holixon.cqrshexagonaldemo.demoparent.transactions.adapter.inbound.Acco
import io.holixon.cqrshexagonaldemo.demoparent.transactions.adapter.inbound.account.mapper.toAccountCreatedResponseDto
import io.holixon.cqrshexagonaldemo.demoparent.transactions.adapter.inbound.dto.AccountCreatedResponseDto
import io.holixon.cqrshexagonaldemo.demoparent.transactions.adapter.inbound.dto.CreateAccountRequestDto
import io.holixon.cqrshexagonaldemo.demoparent.transactions.adapter.inbound.dto.DepositRequestDto
import io.holixon.cqrshexagonaldemo.demoparent.transactions.application.port.inbound.account.CreateAccountInPort
import io.holixon.cqrshexagonaldemo.demoparent.transactions.application.port.inbound.account.DepositAccountInPort
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Amount
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.customer.CustomerNumber
import io.holixon.cqrshexagonaldemo.demoparent.transactions.framework.InAdapter
import jakarta.transaction.Transactional
import mu.KLogging
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
import java.math.BigDecimal

@RestController
@InAdapter
class CreateAccountRestInAdapter(
private val createAccountInPort: CreateAccountInPort
open class AccountRestInAdapter(
private val createAccountInPort: CreateAccountInPort,
private val depositAccountInPort: DepositAccountInPort
) : AccountApiDelegate {

companion object : KLogging()
Expand All @@ -23,4 +29,10 @@ class CreateAccountRestInAdapter(
val createdAccount = createAccountInPort.createAccount(CustomerNumber(createAccountRequestDto.customerNumber))
return ResponseEntity.ok(createdAccount.toAccountCreatedResponseDto())
}

@Transactional
override fun deposit(depositRequestDto: DepositRequestDto): ResponseEntity<Void> {
depositAccountInPort.deposit(depositRequestDto.accountNumber, Amount(BigDecimal.valueOf(depositRequestDto.amount)))
return ResponseEntity.noContent().build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,27 @@ package io.holixon.cqrshexagonaldemo.demoparent.transactions.adapter.outbound.ac

import io.holixon.cqrshexagonaldemo.demoparent.transactions.adapter.outbound.account.jpa.mapper.AccountEntityMapper
import io.holixon.cqrshexagonaldemo.demoparent.transactions.application.port.outbound.account.AccountOutPort
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.exception.AccountNotFoundException
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Account
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Amount
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Iban
import io.holixon.cqrshexagonaldemo.demoparent.transactions.framework.OutAdapter
import mu.KLogging

@OutAdapter
class AccountOutAdapter(
private val jpaAccountOutAdapter: JpaAccountOutAdapter,
private val accountEntityMapper: AccountEntityMapper
) : AccountOutPort {

override fun findAccount(iban: Iban): Account? {
val entity = jpaAccountOutAdapter.findById(iban.value)
if (!entity.isPresent) return null
companion object : KLogging()

return accountEntityMapper.toDomain(entity.get())
override fun findAccount(iban: Iban): Account {
val entity = jpaAccountOutAdapter.findById(iban.value).orElseThrow {
AccountNotFoundException("Account not found for Iban: ${iban.value}")
}

return accountEntityMapper.toDomain(entity)
}

override fun createAccount(account: Account): Account {
Expand All @@ -26,4 +32,13 @@ class AccountOutAdapter(
val savedAccount = jpaAccountOutAdapter.save(toEntity)
return accountEntityMapper.toDomain(savedAccount)
}

override fun deposit(account: Account, amount: Amount): Account {
jpaAccountOutAdapter.save(accountEntityMapper.toEntity(account))
mmiikkkkaa marked this conversation as resolved.
Show resolved Hide resolved

account.deposit(amount)

return account
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.holixon.cqrshexagonaldemo.demoparent.transactions.application.port.inbound.account

import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Amount

interface DepositAccountInPort {
fun deposit(accountNumber: String, amount: Amount)
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.holixon.cqrshexagonaldemo.demoparent.transactions.application.port.outbound.account

import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Account
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Amount
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Iban

interface AccountOutPort {
fun findAccount(iban: Iban): Account?
fun findAccount(iban: Iban): Account
fun createAccount(account: Account): Account
fun deposit(account: Account, amount: Amount): Account
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.holixon.cqrshexagonaldemo.demoparent.transactions.application.usecase

import io.holixon.cqrshexagonaldemo.demoparent.transactions.application.port.inbound.account.DepositAccountInPort
import io.holixon.cqrshexagonaldemo.demoparent.transactions.application.port.outbound.account.AccountOutPort
import io.holixon.cqrshexagonaldemo.demoparent.transactions.application.port.outbound.eventing.EventingOutAdapter
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Amount
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Iban
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.event.MoneyDepositedEvent
import io.holixon.cqrshexagonaldemo.demoparent.transactions.framework.Usecase

@Usecase
class DepositUsecase(
private val accountOutPort: AccountOutPort,
private val eventingOutAdapter: EventingOutAdapter
) : DepositAccountInPort {

override fun deposit(accountNumber: String, amount: Amount) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should already an Iban comming in, instead of an account number. Adapters/Infrastructure components should be the only ones, who use primitives/non-domain-objects.

val account = accountOutPort.findAccount(Iban(accountNumber))

val depositedAccount = accountOutPort.deposit(account, amount)

eventingOutAdapter.publishEvent(MoneyDepositedEvent(
depositedAccount.iban,
depositedAccount.balance,
System.currentTimeMillis()
))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.exception

class AccountNotFoundException(message: String) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.exception

class InvalidInputException(message: String) : RuntimeException(message)
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ data class Account(
var customerNumber: CustomerNumber,
var iban: Iban,
var balance: Money
)
) {
fun deposit(amount: Amount) {
this.balance.amount += amount.value
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account

import java.math.BigDecimal

/**
* For now this class can be value class in future when currency is introduced must be revised to support currency
*/
@JvmInline
value class Amount(val value: BigDecimal) {
init {
require(value > BigDecimal.ZERO){
"Amount must not be negative or zero"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.event

import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Iban
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.account.Money

data class MoneyDepositedEvent(val iban: Iban, val balance: Money, val timeStamp: Long) : Event
27 changes: 27 additions & 0 deletions demo-transactions/src/main/resources/api/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,33 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/AccountCreatedResponse'
/api/v1/account/deposit:
put:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put is not the correct rest method here, since put calls should be idempotent. But depositing money shouldn't be idempotent, since the result each time should be an increased balance.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah makes sense 🙈 since only deposit of 0 would be idempotent but it doesn't make sense to do it 😅 in the beginning i was using post request but since deposit was updating the entity it felt natural to use a put request i wasn't think of the idempotency.

tags:
- account
summary: >
Deposit money
description: >
Deposit money for a specified account
operationId: deposit
requestBody:
required: true
content:
application/json:
schema:
properties:
accountNumber:
type: string
amount:
type: number
required:
- accountNumber
- amount
responses:
'204':
description: Money Deposited
'404':
description: No Account found
components:
schemas:
AccountCreatedResponse:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package io.holixon.cqrshexagonaldemo.demoparent.transactions.application.usecase

import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import io.holixon.cqrshexagonaldemo.demoparent.transactions.application.port.outbound.account.AccountOutPort
import io.holixon.cqrshexagonaldemo.demoparent.transactions.application.port.outbound.customer.CustomerOutPort
import io.holixon.cqrshexagonaldemo.demoparent.transactions.application.port.outbound.eventing.EventingOutAdapter
Expand All @@ -15,56 +12,60 @@ import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.custome
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.model.event.AccountCreatedEvent
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.service.CustomerAccountVerificationService
import io.holixon.cqrshexagonaldemo.demoparent.transactions.domain.service.IbanCreationService
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.junit5.MockKExtension
import io.mockk.verify
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.jupiter.MockitoExtension


@ExtendWith(MockitoExtension::class)
@ExtendWith(MockKExtension::class)
class CreateAccountUsecaseTest {

@Mock
@MockK
private lateinit var accountOutPort: AccountOutPort

@Mock
@MockK
private lateinit var customerOutPort: CustomerOutPort

@Mock
@MockK
private lateinit var customerAccountVerificationService: CustomerAccountVerificationService

@Mock
@MockK
private lateinit var ibanCreationService: IbanCreationService

@Mock
@RelaxedMockK
private lateinit var eventingOutAdapter: EventingOutAdapter

@InjectMocks
@InjectMockKs
private lateinit var createAccountUsecase: CreateAccountUsecase

@Test
fun `should create an account`() {
// given
val customerNumber = CustomerNumber("123456789")
val customer = Customer(customerNumber, Name("Dagobert Duck"))
whenever(customerOutPort.findCustomer(customerNumber)).thenReturn(customer)
whenever(customerAccountVerificationService.canAccountBeCreatedForCustomer(customer)).thenReturn(true)
every { customerOutPort.findCustomer(customerNumber) } returns customer
every { customerAccountVerificationService.canAccountBeCreatedForCustomer(customer) } returns true

val iban = Iban("DE00123456789")
whenever(ibanCreationService.generateNextIban()).thenReturn(iban)

every { ibanCreationService.generateNextIban() } returns iban
val balance = Money.ZERO
val account = Account(customerNumber, iban, balance)
whenever(accountOutPort.createAccount(any())).thenReturn(account)

every { accountOutPort.createAccount(this.any()) } returns account

// when
val createdAccount = createAccountUsecase.createAccount(customerNumber)

// then
Assertions.assertThat(createdAccount).isNotNull
verify(accountOutPort).createAccount(account)
Mockito.verify(eventingOutAdapter).publishEvent(AccountCreatedEvent(customerNumber, iban))
verify { accountOutPort.createAccount(account) }
verify { eventingOutAdapter.publishEvent(AccountCreatedEvent(customerNumber, iban)) }
}

}
Loading
Loading