#38 hexagonal: banking service - register bank account

This commit is contained in:
haerong22
2023-07-24 21:49:23 +09:00
parent cbf4f49fd6
commit b2d17c133e
19 changed files with 457 additions and 1 deletions

View File

@@ -0,0 +1,40 @@
package org.example.banking.adapter.in.web;
import lombok.RequiredArgsConstructor;
import org.example.banking.application.port.in.RegisterBankAccountCommand;
import org.example.banking.application.port.in.RegisterBankAccountUseCase;
import org.example.banking.domain.RegisteredBankAccount;
import org.example.common.WebAdapter;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@WebAdapter
@RestController
@RequiredArgsConstructor
public class RegisterBankAccountController {
private final RegisterBankAccountUseCase registerBankAccountUseCase;
@PostMapping("/banking/account/register")
RegisteredBankAccount registeredBankAccount(@RequestBody RegisterBankAccountRequest request) {
RegisterBankAccountCommand command = RegisterBankAccountCommand.builder()
.membershipId(request.getMembershipId())
.bankName(request.getBankName())
.bankAccountNumber(request.getBankAccountNumber())
.isValid(true)
.build();
RegisteredBankAccount registeredBankAccount = registerBankAccountUseCase.registerBankAccount(command);
if (registeredBankAccount == null) {
// TODO Error Handling
return null;
}
return registeredBankAccount;
}
}

View File

@@ -0,0 +1,19 @@
package org.example.banking.adapter.in.web;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RegisterBankAccountRequest {
private String membershipId;
private String bankName;
private String bankAccountNumber;
private boolean isValid;
}

View File

@@ -0,0 +1,13 @@
package org.example.banking.adapter.out.external.bank;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class BankAccount {
private String bankName;
private String bankAccountNumber;
private boolean isValid;
}

View File

@@ -0,0 +1,15 @@
package org.example.banking.adapter.out.external.bank;
import lombok.RequiredArgsConstructor;
import org.example.banking.application.port.out.RequestBankAccountInfoPort;
import org.example.common.ExternalSystemAdapter;
@ExternalSystemAdapter
@RequiredArgsConstructor
public class BankAccountAdapter implements RequestBankAccountInfoPort {
@Override
public BankAccount getBankAccountInfo(GetBankAccountRequest request) {
return new BankAccount(request.getBankName(), request.getBankAccountNumber(), true);
}
}

View File

@@ -0,0 +1,12 @@
package org.example.banking.adapter.out.external.bank;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class GetBankAccountRequest {
private String bankName;
private String bankAccountNumber;
}

View File

@@ -0,0 +1,30 @@
package org.example.banking.adapter.out.persistence;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@Table(name = "registered_bank_account")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RegisteredBankAccountJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long registeredBankAccountId;
private String membershipId;
private String bankName;
private String bankAccountNumber;
private boolean linkedStatusIsValid;
public RegisteredBankAccountJpaEntity(String membershipId, String bankName, String bankAccountNumber, boolean linkedStatusIsValid) {
this.membershipId = membershipId;
this.bankName = bankName;
this.bankAccountNumber = bankAccountNumber;
this.linkedStatusIsValid = linkedStatusIsValid;
}
}

View File

@@ -0,0 +1,18 @@
package org.example.banking.adapter.out.persistence;
import org.example.banking.domain.RegisteredBankAccount;
import org.springframework.stereotype.Component;
@Component
public class RegisteredBankAccountMapper {
public RegisteredBankAccount mapToDomainEntity(RegisteredBankAccountJpaEntity membershipJpaEntity) {
return RegisteredBankAccount.generateRegisteredBankAccount(
new RegisteredBankAccount.RegisteredBankAccountId(membershipJpaEntity.getRegisteredBankAccountId()+""),
new RegisteredBankAccount.MembershipId(membershipJpaEntity.getMembershipId()),
new RegisteredBankAccount.BankName(membershipJpaEntity.getBankName()),
new RegisteredBankAccount.BankAccountNumber(membershipJpaEntity.getBankAccountNumber()),
new RegisteredBankAccount.LinkedStatusIsValid(membershipJpaEntity.isLinkedStatusIsValid())
);
}
}

View File

@@ -0,0 +1,25 @@
package org.example.banking.adapter.out.persistence;
import lombok.RequiredArgsConstructor;
import org.example.banking.application.port.out.RegisterBankAccountPort;
import org.example.banking.domain.RegisteredBankAccount;
import org.example.common.PersistenceAdapter;
@PersistenceAdapter
@RequiredArgsConstructor
public class RegisteredBankAccountPersistenceAdapter implements RegisterBankAccountPort {
private final SpringDataRegisteredBankAccountRepository bankAccountRepository;
@Override
public RegisteredBankAccountJpaEntity createRegisteredBankAccount(RegisteredBankAccount.MembershipId membershipId, RegisteredBankAccount.BankName bankName, RegisteredBankAccount.BankAccountNumber bankAccountNumber, RegisteredBankAccount.LinkedStatusIsValid linkedStatusIsValid) {
return bankAccountRepository.save(
new RegisteredBankAccountJpaEntity(
membershipId.getMembershipId(),
bankName.getBankName(),
bankAccountNumber.getBankAccountNumber(),
linkedStatusIsValid.isLinkedStatusIsValid()
)
);
}
}

View File

@@ -0,0 +1,6 @@
package org.example.banking.adapter.out.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataRegisteredBankAccountRepository extends JpaRepository<RegisteredBankAccountJpaEntity, Long> {
}

View File

@@ -0,0 +1,36 @@
package org.example.banking.application.port.in;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.example.common.SelfValidating;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
@Builder
@Data
@EqualsAndHashCode(callSuper = false)
public class RegisterBankAccountCommand extends SelfValidating<RegisterBankAccountCommand> {
@NotEmpty
private final String membershipId;
@NotEmpty
private final String bankName;
@NotEmpty
private final String bankAccountNumber;
@AssertTrue
private final boolean isValid;
public RegisterBankAccountCommand(String membershipId, String bankName, String bankAccountNumber, boolean isValid) {
this.membershipId = membershipId;
this.bankName = bankName;
this.bankAccountNumber = bankAccountNumber;
this.isValid = isValid;
this.validateSelf();
}
}

View File

@@ -0,0 +1,8 @@
package org.example.banking.application.port.in;
import org.example.banking.domain.RegisteredBankAccount;
public interface RegisterBankAccountUseCase {
RegisteredBankAccount registerBankAccount(RegisterBankAccountCommand command);
}

View File

@@ -0,0 +1,14 @@
package org.example.banking.application.port.out;
import org.example.banking.adapter.out.persistence.RegisteredBankAccountJpaEntity;
import org.example.banking.domain.RegisteredBankAccount;
public interface RegisterBankAccountPort {
RegisteredBankAccountJpaEntity createRegisteredBankAccount(
RegisteredBankAccount.MembershipId membershipId,
RegisteredBankAccount.BankName bankName,
RegisteredBankAccount.BankAccountNumber bankAccountNumber,
RegisteredBankAccount.LinkedStatusIsValid linkedStatusIsValid
);
}

View File

@@ -0,0 +1,9 @@
package org.example.banking.application.port.out;
import org.example.banking.adapter.out.external.bank.BankAccount;
import org.example.banking.adapter.out.external.bank.GetBankAccountRequest;
public interface RequestBankAccountInfoPort {
BankAccount getBankAccountInfo(GetBankAccountRequest request);
}

View File

@@ -0,0 +1,47 @@
package org.example.banking.application.service;
import lombok.RequiredArgsConstructor;
import org.example.banking.adapter.out.external.bank.BankAccount;
import org.example.banking.adapter.out.external.bank.GetBankAccountRequest;
import org.example.banking.adapter.out.persistence.RegisteredBankAccountJpaEntity;
import org.example.banking.adapter.out.persistence.RegisteredBankAccountMapper;
import org.example.banking.application.port.in.RegisterBankAccountCommand;
import org.example.banking.application.port.in.RegisterBankAccountUseCase;
import org.example.banking.application.port.out.RegisterBankAccountPort;
import org.example.banking.application.port.out.RequestBankAccountInfoPort;
import org.example.banking.domain.RegisteredBankAccount;
import org.example.common.UseCase;
import org.springframework.transaction.annotation.Transactional;
@UseCase
@Transactional
@RequiredArgsConstructor
public class RegisterBankAccountService implements RegisterBankAccountUseCase {
private final RegisterBankAccountPort registerBankAccountPort;
private final RegisteredBankAccountMapper mapper;
private final RequestBankAccountInfoPort requestBankAccountInfoPort;
@Override
public RegisteredBankAccount registerBankAccount(RegisterBankAccountCommand command) {
BankAccount accountInfo = requestBankAccountInfoPort.getBankAccountInfo(new GetBankAccountRequest(command.getBankName(), command.getBankAccountNumber()));
boolean accountIsValid = accountInfo.isValid();
if (accountIsValid) {
RegisteredBankAccountJpaEntity savedAccountInfo = registerBankAccountPort.createRegisteredBankAccount(
new RegisteredBankAccount.MembershipId(command.getMembershipId() + ""),
new RegisteredBankAccount.BankName(accountInfo.getBankName()),
new RegisteredBankAccount.BankAccountNumber(accountInfo.getBankAccountNumber()),
new RegisteredBankAccount.LinkedStatusIsValid(true)
);
return mapper.mapToDomainEntity(savedAccountInfo);
} else {
return null;
}
}
}

View File

@@ -0,0 +1,84 @@
package org.example.banking.domain;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Value;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class RegisteredBankAccount {
private final String registeredBankAccountId;
private final String membershipId;
private final String bankName;
private final String bankAccountNumber;
private final boolean linkedStatusIsValid;
public static RegisteredBankAccount generateRegisteredBankAccount(
RegisteredBankAccountId registeredBankAccountId,
MembershipId membershipId,
BankName bankName,
BankAccountNumber bankAccountNumber,
LinkedStatusIsValid linkedStatusIsValid
) {
return new RegisteredBankAccount(
registeredBankAccountId.registeredBankAccountId,
membershipId.membershipId,
bankName.bankName,
bankAccountNumber.bankAccountNumber,
linkedStatusIsValid.linkedStatusIsValid
);
}
@Value
public static class RegisteredBankAccountId {
String registeredBankAccountId;
public RegisteredBankAccountId(String value) {
this.registeredBankAccountId = value;
}
}
@Value
public static class MembershipId {
String membershipId;
public MembershipId(String value) {
this.membershipId = value;
}
}
@Value
public static class BankName {
String bankName;
public BankName(String value) {
this.bankName = value;
}
}
@Value
public static class BankAccountNumber {
String bankAccountNumber;
public BankAccountNumber(String value) {
this.bankAccountNumber = value;
}
}
@Value
public static class LinkedStatusIsValid {
boolean linkedStatusIsValid;
public LinkedStatusIsValid(boolean value) {
this.linkedStatusIsValid = value;
}
}
}

View File

@@ -0,0 +1,63 @@
package org.example.banking.adapter.in.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.banking.domain.RegisteredBankAccount;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
@SpringBootTest
@AutoConfigureMockMvc
class RegisterBankAccountControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private WebApplicationContext ctx;
@Autowired
private ObjectMapper mapper;
@BeforeEach
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(ctx)
.addFilters(new CharacterEncodingFilter("UTF-8", true))
.alwaysDo(print())
.build();
}
@Test
void registerBankAccountTest() throws Exception {
RegisterBankAccountRequest request =
new RegisterBankAccountRequest("1", "국민은행", "1234567890", true);
RegisteredBankAccount bankAccount = RegisteredBankAccount.generateRegisteredBankAccount(
new RegisteredBankAccount.RegisteredBankAccountId("1"),
new RegisteredBankAccount.MembershipId("1"),
new RegisteredBankAccount.BankName("국민은행"),
new RegisteredBankAccount.BankAccountNumber("1234567890"),
new RegisteredBankAccount.LinkedStatusIsValid(true)
);
mockMvc.perform(
MockMvcRequestBuilders.post("/banking/account/register")
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request))
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string(mapper.writeValueAsString(bankAccount)))
;
}
}

View File

@@ -0,0 +1,16 @@
package org.example.common;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ExternalSystemAdapter {
@AliasFor(annotation = Component.class)
String value() default "";
}

View File

@@ -3,7 +3,7 @@ plugins {
}
group 'org.example.membership'
version '1.0.1'
version '1.0.0'
repositories {
mavenCentral()

View File

@@ -1,4 +1,5 @@
rootProject.name = 'bobby-pay'
include 'membership-service'
include 'common'
include 'banking-service'