Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ public record AccountCreateReq(

@NotBlank(message = "비밀번호는 필수입니다.")
@Pattern(regexp = "^\\d{4}$", message = "비밀번호는 4자리 숫자여야 합니다.")
String password,

@NotBlank(message = "비밀번호는 필수입니다.")
@Pattern(regexp = "^\\d{4}$", message = "비밀번호는 4자리 숫자여야 합니다.")
String passwordConfirmation
String password
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ public class AccountService {
*/
@Transactional
public AccountRes createAccount(AccountCreateReq request, Long userId) {
// 비밀번호 일치 확인
if (!request.password().equals(request.passwordConfirmation())) {
throw new CustomBaseException(ErrorBaseCode.MISMATCH_PASSWORD);
}

// 비밀번호 유효성 검사
passwordValidator.validatePassword(request.password());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
import org.creditto.core_banking.global.common.CurrencyCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand Down Expand Up @@ -48,9 +46,12 @@ public ResponseEntity<BaseResponse<SingleExchangeRateRes>> getExchangeRate(@Path
return ApiResponseUtil.success(SuccessCode.OK, exchangeService.getRateByCurrency(currencyCode));
}

@GetMapping("/preferential-rate/{userId}")
public ResponseEntity<BaseResponse<PreferentialRateRes>> getPreferentialRate(@PathVariable Long userId) {
double rate = creditScoreService.getPreferentialRate(userId);
return ApiResponseUtil.success(SuccessCode.OK, new PreferentialRateRes(rate));
@GetMapping("/preferential-rate/{userId}/{currency}")
public ResponseEntity<BaseResponse<PreferentialRateRes>> getPreferentialRate(
@PathVariable Long userId,
@PathVariable String currency
) {
CurrencyCode currencyCode = CurrencyCode.from(currency);
return ApiResponseUtil.success(SuccessCode.OK, exchangeService.getPreferentialRateInfo(userId, currencyCode));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.creditto.core_banking.domain.exchange.dto;

import java.math.BigDecimal;

public record PreferentialRateRes(
double preferentialRate
double preferentialRate,
BigDecimal appliedRate
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.creditto.core_banking.domain.exchange.dto.ExchangeRes;
import org.creditto.core_banking.domain.exchange.entity.Exchange;
import org.creditto.core_banking.domain.exchange.repository.ExchangeRepository;
import org.creditto.core_banking.domain.exchange.dto.PreferentialRateRes;
import org.creditto.core_banking.domain.exchange.dto.SingleExchangeRateRes;
import org.creditto.core_banking.global.common.CurrencyCode;
import org.creditto.core_banking.global.feign.ExchangeRateProvider;
Expand Down Expand Up @@ -190,6 +191,23 @@ private Exchange saveExchangeHistory(ExchangeReq exchangeReq, BigDecimal fromAmo
return exchangeRepository.save(exchange);
}

/**
* 우대 환율 및 적용 환율 정보를 조회하여 반환
* @param userId 사용자 ID
* @param currencyCode 조회할 통화 코드
* @return 우대 환율 및 적용 환율 정보
*/
public PreferentialRateRes getPreferentialRateInfo(Long userId, CurrencyCode currencyCode) {
double preferentialRate = creditScoreService.getPreferentialRate(userId);
Map<String, ExchangeRateRes> rateMap = getLatestRates();
BigDecimal baseRateFromApi = getBaseRateForCurrency(rateMap, currencyCode);
BigDecimal adjustedBaseRate = baseRateFromApi.divide(new BigDecimal(currencyCode.getUnit()), ADJUSTED_RATE_SCALE, RoundingMode.HALF_UP);

BigDecimal appliedRate = calculateAppliedRate(adjustedBaseRate, true, preferentialRate).setScale(2, RoundingMode.HALF_UP);

return new PreferentialRateRes(preferentialRate, appliedRate);
}

/**
* 환율 정보 DTO에서 매매 기준율을 BigDecimal 타입으로 추출
* @param rateMap 전체 환율 맵
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -29,6 +30,13 @@ public class OverseasRemittanceRequestDto {
@NotBlank(message = "출금 계좌번호는 필수입니다.")
private String accountNo;

/**
* 출금될 계좌의 비밀번호
*/
@NotBlank(message = "계좌 비밀번호는 필수입니다.")
@Pattern(regexp = "^\\d{4}$", message = "비밀번호는 4자리 숫자여야 합니다.")
private String password;

/**
* 수취인의 상세 정보
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.RequiredArgsConstructor;
import org.creditto.core_banking.domain.account.entity.Account;
import org.creditto.core_banking.domain.account.repository.AccountRepository;
import org.creditto.core_banking.domain.account.service.AccountService;
import org.creditto.core_banking.domain.overseasremittance.dto.ExecuteRemittanceCommand;
import org.creditto.core_banking.domain.overseasremittance.dto.OverseasRemittanceRequestDto;
import org.creditto.core_banking.domain.overseasremittance.dto.OverseasRemittanceResponseDto;
Expand Down Expand Up @@ -30,6 +31,7 @@ public class OneTimeRemittanceService {
private final AccountRepository accountRepository;
private final RecipientFactory recipientFactory;
private final OverseasRemittanceRepository overseasRemittanceRepository;
private final AccountService accountService;

/**
* 클라이언트의 해외송금 요청을 받아 전체 송금 프로세스를 조정합니다.
Expand All @@ -43,6 +45,9 @@ public OverseasRemittanceResponseDto processRemittance(Long userId, OverseasRemi
Account account = accountRepository.findByAccountNo(request.getAccountNo())
.orElseThrow(() -> new CustomBaseException(ErrorBaseCode.NOT_FOUND_ACCOUNT));

// 비밀번호 검증
accountService.verifyPassword(account.getId(), request.getPassword());

// RecipientFactory를 통해 수취인 조회 또는 생성
RecipientCreateDto recipientCreateDto = request.getRecipientInfo().toRecipientCreateDto();
Recipient recipient = recipientFactory.findOrCreate(recipientCreateDto);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ void createAccount_Success() {
AccountCreateReq request = new AccountCreateReq(
"새로운 계좌",
AccountType.DEPOSIT,
"1234",
"1234"
);
Long userId = 1L;
Expand Down Expand Up @@ -96,37 +95,13 @@ void createAccount_Success() {
assertThat(capturedAccount.getUserId()).isEqualTo(userId);
}

@Test
@DisplayName("계좌 생성 실패 - 비밀번호와 비밀번호 확인 불일치")
void createAccount_Failure_PasswordMismatch() {
// Given
AccountCreateReq request = new AccountCreateReq(
"새로운 계좌",
AccountType.DEPOSIT,
"1234",
"5678"
);
Long userId = 1L;

// When & Then
assertThatThrownBy(() -> accountService.createAccount(request, userId))
.isInstanceOf(CustomBaseException.class)
.extracting("errorCode")
.isEqualTo(ErrorBaseCode.MISMATCH_PASSWORD);

verify(passwordValidator, never()).validatePassword(anyString());
verify(passwordEncoder, never()).encode(anyString());
verify(accountRepository, never()).save(any(Account.class));
}

@Test
@DisplayName("계좌 생성 실패 - 비밀번호 정책 위반")
void createAccount_Failure_InvalidPasswordPolicy() {
// Given
AccountCreateReq request = new AccountCreateReq(
"새로운 계좌",
AccountType.DEPOSIT,
"1111",
"1111"
);
Long userId = 1L;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.creditto.core_banking.domain.exchange.dto.ExchangeRateRes;
import org.creditto.core_banking.domain.exchange.dto.ExchangeReq;
import org.creditto.core_banking.domain.exchange.dto.ExchangeRes;
import org.creditto.core_banking.domain.exchange.dto.PreferentialRateRes;
import org.creditto.core_banking.domain.exchange.dto.SingleExchangeRateRes;
import org.creditto.core_banking.domain.exchange.entity.Exchange;
import org.creditto.core_banking.domain.exchange.repository.ExchangeRepository;
Expand Down Expand Up @@ -208,4 +209,30 @@ void getRateByCurrency_NotFound_ThrowsException() {
.extracting("errorCode")
.isEqualTo(ErrorBaseCode.CURRENCY_NOT_SUPPORTED);
}

@Test
@DisplayName("우대 환율 정보 및 적용 환율 조회 성공")
void getPreferentialRateInfo_Success() {
// Given
Long userId = 1L;
CurrencyCode currencyCode = CurrencyCode.USD;
double preferentialRate = MOCK_PREFERENTIAL_RATE.doubleValue(); // 0.5

given(creditScoreService.getPreferentialRate(userId)).willReturn(preferentialRate);
given(exchangeRateProvider.getExchangeRates()).willReturn(rateMap);

// Expected applied rate calculation
BigDecimal baseRate = new BigDecimal(usdRate.getBaseRate()); // 1300.00
BigDecimal adjustedBaseRate = baseRate.divide(new BigDecimal(currencyCode.getUnit()), 4, RoundingMode.HALF_UP); // 1300.00 / 1 = 1300.00
BigDecimal effectiveSpread = SPREAD_RATE.multiply(BigDecimal.ONE.subtract(MOCK_PREFERENTIAL_RATE)); // 0.01 * (1 - 0.5) = 0.005
BigDecimal expectedAppliedRate = adjustedBaseRate.multiply(BigDecimal.ONE.add(effectiveSpread)).setScale(2, RoundingMode.HALF_UP); // 1300.00 * (1 + 0.005) = 1306.50

// When
PreferentialRateRes result = exchangeService.getPreferentialRateInfo(userId, currencyCode);

// Then
assertThat(result).isNotNull();
assertThat(result.preferentialRate()).isEqualTo(preferentialRate);
assertThat(result.appliedRate()).isEqualByComparingTo(expectedAppliedRate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.creditto.core_banking.domain.exchange.controller;

import org.creditto.core_banking.domain.creditscore.service.CreditScoreService;
import org.creditto.core_banking.domain.exchange.dto.PreferentialRateRes;
import org.creditto.core_banking.domain.exchange.service.ExchangeService;
import org.creditto.core_banking.global.common.CurrencyCode;
import org.junit.jupiter.api.DisplayName;
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.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import java.math.BigDecimal;

import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class ExchangeControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private ExchangeService exchangeService;

@MockBean
private CreditScoreService creditScoreService;

@Test
@DisplayName("우대 환율 정보 조회 성공")
void getPreferentialRate_Success() throws Exception {
Long userId = 1L;
String currency = "USD";
double preferentialRate = 0.5;
BigDecimal appliedRate = new BigDecimal("1306.50");

PreferentialRateRes mockResponse = new PreferentialRateRes(preferentialRate, appliedRate);

given(exchangeService.getPreferentialRateInfo(userId, CurrencyCode.USD)).willReturn(mockResponse);

mockMvc.perform(get("/api/core/exchange/preferential-rate/{userId}/{currency}", userId, currency))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("200"))
.andExpect(jsonPath("$.message").value("요청이 성공했습니다."))
.andExpect(jsonPath("$.data.preferentialRate").value(preferentialRate))
.andExpect(jsonPath("$.data.appliedRate").value(appliedRate.doubleValue()));
}
}
Loading