The following microservice is just handling loan application and response the result of the application is accepted or rejected. It is part of my assignment for an interview.
The project description is that; A user sends them Id, username, surname, and monthly salary via REST API. Assuming that you have got already credit scores in DB, you must return the loan application result based on that credit score. And then save the application on DB.
They give me the ruleset. And they said that please show what you have got like design patterns, test-driven development, clean code, REST API design, SOLID principles, OOP, etc. Also Docker, MongoDB
But they rejected me. Anyway, I appreciated if you judging my code, need to improve my skills.
I try to use the Strategy and Factory patterns to process the application.
Source code is huge so the project repo is HERE, please check this.
LoanController Class
@Slf4j
@RestController
@RequestMapping(value = "/api")
public class LoanController {
private final LoanApplicantService loanApplicantService;
public LoanController(LoanApplicantService loanApplicantService) {
this.loanApplicantService = loanApplicantService;
}
@PostMapping("/loan")
public ResponseEntity<Response> applyForLoan(@Valid @RequestBody LoanRequest request) {
Response response;
try{
log.info("incoming request: {}", request.getId());
response = Response.builder()
.success(Boolean.TRUE)
.data(loanApplicantService.process(request))
.build();
}catch ( Exception e ) {
log.error("Exception: {}", e.getCause());
response = Response.builder()
.success(Boolean.FALSE)
.message(e.getMessage())
.build();
}
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
LoanApplicantService class
@Slf4j
@Service
public class LoanApplicantService implements ILoanApplicantService{
private final LoanApplicantRepository loanApplicantRepository;
private final LoanApplicantResultRepository resultRepository;
private final LoanApplicantScoreService scoreService;
private final SmsService smsService;
public LoanApplicantService(LoanApplicantRepository loanApplicantRepository,
LoanApplicantScoreService scoreService,
LoanApplicantResultRepository resultRepository,
SmsService smsService){
this.loanApplicantRepository = loanApplicantRepository;
this.scoreService = scoreService;
this.resultRepository = resultRepository;
this.smsService = smsService;
}
public LoanResponse process(LoanRequest request) {
// convert request to entity
LoanApplicant applicant = LoanConverter.convertRequest(request).build();
// save request
save(applicant);
// find score
LoanApplicantScore applicantScore = scoreService.findApplicantScoreById(applicant.getId());
// get strategy by score
LoanStrategy strategy = LoanStrategyFactory.getStrategy(applicantScore.getScore(),
applicant.getMonthlySalary());
// execute the result of application
LoanApplicantResult result = strategy.execute(applicantScore, applicant);
// save result of application
resultRepository.save(result);
LoanResponse response = LoanConverter.convertResult(result).build();
// send sms
smsService.sendSMS(applicant);
return response;
}
public LoanApplicant save(LoanApplicant applicant) {
return loanApplicantRepository.save(applicant);
}
}
LoanStrategyFactory class
public class LoanStrategyFactory {
private LoanStrategyFactory() {}
public static LoanStrategy getStrategy(Integer score, BigInteger salary) {
if (LoanRange.MEDIUM.contains(score) && isSalaryUnderBarrier(salary)) {
return new MediumScoreStrategy();
} else if(LoanRange.HIGH.contains(score)) {
return new HighScoreStrategy();
} else {
return new LowScoreStrategy();
}
}
private static boolean isSalaryUnderBarrier(BigInteger salary) {
return LoanConstant.INCOME_BARRIER.compareTo(salary) > 0;
}
}
LoanExceptionHandler class
@ControllerAdvice
public class LoanExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
Response response = Response.builder()
.success(Boolean.FALSE)
.message(ex.getMessage())
.build();
return new ResponseEntity<>(response, headers, status);
}
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
Response response = Response.builder()
.success(Boolean.FALSE)
.message("Required request body is missing")
.build();
return new ResponseEntity<>(response, headers, status);
}
}
LoanConverter class
public class LoanConverter {
private LoanConverter() { }
public static LoanApplicant.LoanApplicantBuilder convertRequest(LoanRequest loan) {
return Optional.ofNullable(loan).map(d -> LoanApplicant.builder()
.id(loan.getId())
.name(loan.getName())
.surname(loan.getSurname())
.monthlySalary(new BigInteger(loan.getMonthlySalary()))
.phoneNumber(loan.getPhoneNumber()))
.orElseGet(LoanApplicant::builder);
}
public static LoanResponse.LoanResponseBuilder convertResult(LoanApplicantResult loan) {
return Optional.ofNullable(loan).map(d -> LoanResponse.builder()
.amount(loan.getAmount())
.status(loan.getStatus()))
.orElseGet(LoanResponse::builder);
}
}
LoanRange class
import org.apache.commons.lang3.Range;
public class LoanRange {
private LoanRange() {}
public static final Range<Integer> LOW = Range.between(0, 499);
public static final Range<Integer> MEDIUM = Range.between(499, 999);
public static final Range<Integer> HIGH = Range.between(999, Integer.MAX_VALUE);
}
LoanControllerTest class
@WebMvcTest
public class LoanControllerTest {
private static final String API = "/api/loan";
MockMvc mockMvc;
private final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
LoanController loanController;
@MockBean
LoanApplicantService loanApplicantService;
@BeforeEach
public void setup() {
this.mockMvc = MockMvcBuilders
.standaloneSetup(loanController)
.setControllerAdvice(new LoanExceptionHandler())
.build();
}
@Test
void whenRequestLoan_thenReturn200() throws Exception {
LoanRequest request = LoanRequest.builder()
.id("12312312312")
.name("fuat")
.surname("kara")
.monthlySalary("1000")
.phoneNumber("5312341234").build();
performRequest(request, status().isOk());
}
@Test
void whenInvalidRequestLoan_thenReturn400() throws Exception {
LoanRequest request = LoanRequest.builder()
.id("123123123123")
.name("fuat")
.surname("kara")
.monthlySalary("1000")
.phoneNumber("5312341234").build();
performRequest(request, status().isBadRequest());
}
@Test
void whenNullRequestLoan_thenReturn400() throws Exception {
mockMvc.perform(post(API))
.andExpect(status().isBadRequest());
}
private ResultActions performRequest(Object request, ResultMatcher matcher) throws Exception {
return performRequest(request, post(API), matcher);
}
private ResultActions performRequest(Object request, MockHttpServletRequestBuilder builder, ResultMatcher matcher) throws Exception {
return mockMvc.perform(
builder
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(matcher);
}
}