-
Notifications
You must be signed in to change notification settings - Fork 0
feat(inqcust): translate INQCUST to Java #4
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
Changes from all commits
9ec3bb7
effb579
27ef2ed
def0eb3
58acecc
3db0eca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.augment.cbsa.domain; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.Objects; | ||
|
|
||
| public record CustomerDetails( | ||
| String sortcode, | ||
| long customerNumber, | ||
| String name, | ||
| String address, | ||
| LocalDate dateOfBirth, | ||
| int creditScore, | ||
| LocalDate csReviewDate | ||
| ) { | ||
|
|
||
| public CustomerDetails { | ||
| Objects.requireNonNull(sortcode, "sortcode must not be null"); | ||
| Objects.requireNonNull(name, "name must not be null"); | ||
| Objects.requireNonNull(address, "address must not be null"); | ||
| Objects.requireNonNull(dateOfBirth, "dateOfBirth must not be null"); | ||
|
|
||
| if (creditScore < 0 || creditScore > 999) { | ||
| throw new IllegalArgumentException("creditScore must be between 0 and 999"); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package com.augment.cbsa.domain; | ||
|
|
||
| public record InqcustRequest(long customerNumber) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.augment.cbsa.domain; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| public record InqcustResult( | ||
| boolean inquirySuccess, | ||
| String failCode, | ||
| long customerNumber, | ||
| CustomerDetails customer, | ||
| String message | ||
| ) { | ||
|
|
||
| public InqcustResult { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Severity: low 🤖 Was this useful? React with 👍 or 👎 |
||
| Objects.requireNonNull(failCode, "failCode must not be null"); | ||
|
|
||
| if (inquirySuccess && customer == null) { | ||
| throw new IllegalArgumentException("Successful results must include a customer"); | ||
| } | ||
|
|
||
| if (!inquirySuccess && customer != null) { | ||
| throw new IllegalArgumentException("Failure results must not include a customer"); | ||
| } | ||
|
|
||
| if (!inquirySuccess && (message == null || message.isBlank())) { | ||
| throw new IllegalArgumentException("Failure results must include a non-blank message"); | ||
| } | ||
| } | ||
|
|
||
| public static InqcustResult success(CustomerDetails customer) { | ||
| Objects.requireNonNull(customer, "customer must not be null"); | ||
| return new InqcustResult(true, "0", customer.customerNumber(), customer, null); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Severity: low 🤖 Was this useful? React with 👍 or 👎 |
||
| } | ||
|
|
||
| public static InqcustResult failure(String failCode, long customerNumber, String message) { | ||
| return new InqcustResult(false, failCode, customerNumber, null, message); | ||
| } | ||
|
|
||
| public boolean isNotFoundFailure() { | ||
| return !inquirySuccess && "1".equals(failCode); | ||
| } | ||
|
|
||
| public boolean isRandomRetryExhaustedFailure() { | ||
| return !inquirySuccess && "R".equals(failCode); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.augment.cbsa.error; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| public class CbsaAbendException extends RuntimeException { | ||
|
|
||
| private final String abendCode; | ||
|
|
||
| public CbsaAbendException(String abendCode, String message) { | ||
| super(message); | ||
| this.abendCode = Objects.requireNonNull(abendCode, "abendCode must not be null"); | ||
| } | ||
|
|
||
| public CbsaAbendException(String abendCode, String message, Throwable cause) { | ||
| super(message, cause); | ||
| this.abendCode = Objects.requireNonNull(abendCode, "abendCode must not be null"); | ||
| } | ||
|
|
||
| public String getAbendCode() { | ||
| return abendCode; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package com.augment.cbsa.error; | ||
|
|
||
| import jakarta.validation.ConstraintViolationException; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ProblemDetail; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.validation.BindException; | ||
| import org.springframework.web.bind.MethodArgumentNotValidException; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
| import org.springframework.web.method.annotation.HandlerMethodValidationException; | ||
|
|
||
| @RestControllerAdvice | ||
| public class CbsaExceptionHandler { | ||
|
|
||
| private static final Logger logger = LoggerFactory.getLogger(CbsaExceptionHandler.class); | ||
|
|
||
| @ExceptionHandler({ | ||
| MethodArgumentNotValidException.class, | ||
| BindException.class, | ||
| HandlerMethodValidationException.class, | ||
| ConstraintViolationException.class | ||
| }) | ||
| public ResponseEntity<ProblemDetail> handleValidation(Exception exception) { | ||
| ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); | ||
| problemDetail.setTitle("Validation failed"); | ||
| problemDetail.setDetail("Request validation failed."); | ||
| return ResponseEntity.badRequest().body(problemDetail); | ||
| } | ||
|
|
||
| @ExceptionHandler(CbsaAbendException.class) | ||
| public ResponseEntity<ProblemDetail> handleAbend(CbsaAbendException exception) { | ||
| logger.error("CBSA abend while processing request [abendCode={}]", exception.getAbendCode(), exception); | ||
|
|
||
| ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR); | ||
| problemDetail.setTitle("CBSA abend"); | ||
| problemDetail.setDetail("Service abend"); | ||
| problemDetail.setProperty("abendCode", exception.getAbendCode()); | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(problemDetail); | ||
| } | ||
|
|
||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<ProblemDetail> handleUnexpected(Exception exception) { | ||
| logger.error("Unhandled exception while processing request", exception); | ||
|
|
||
| ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR); | ||
| problemDetail.setTitle("Unexpected error"); | ||
| problemDetail.setDetail("Internal server error"); | ||
| problemDetail.setProperty("abendCode", "UNEX"); | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(problemDetail); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.augment.cbsa.error; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| public record CbsaFailureResponse(String failCode, String message) { | ||
|
|
||
| public CbsaFailureResponse { | ||
| Objects.requireNonNull(failCode, "failCode must not be null"); | ||
| Objects.requireNonNull(message, "message must not be null"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.augment.cbsa.repository; | ||
|
|
||
| import com.augment.cbsa.domain.CustomerDetails; | ||
| import com.augment.cbsa.jooq.tables.records.CustomerRecord; | ||
| import java.util.Optional; | ||
| import org.jooq.DSLContext; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import static com.augment.cbsa.jooq.Tables.CUSTOMER; | ||
|
|
||
| @Repository | ||
| public class CustomerRepository { | ||
|
|
||
| private final DSLContext dsl; | ||
|
|
||
| public CustomerRepository(DSLContext dsl) { | ||
| this.dsl = dsl; | ||
| } | ||
|
|
||
| public Optional<CustomerDetails> findBySortcodeAndCustomerNumber(String sortcode, long customerNumber) { | ||
| return dsl.selectFrom(CUSTOMER) | ||
| .where(CUSTOMER.SORTCODE.eq(sortcode)) | ||
| .and(CUSTOMER.CUSTOMER_NUMBER.eq(customerNumber)) | ||
| .fetchOptional(this::toDomain); | ||
| } | ||
|
|
||
| public Optional<CustomerDetails> findLastBySortcode(String sortcode) { | ||
| return dsl.selectFrom(CUSTOMER) | ||
| .where(CUSTOMER.SORTCODE.eq(sortcode)) | ||
| .orderBy(CUSTOMER.CUSTOMER_NUMBER.desc()) | ||
| .limit(1) | ||
| .fetchOptional(this::toDomain); | ||
| } | ||
|
|
||
| private CustomerDetails toDomain(CustomerRecord record) { | ||
| return new CustomerDetails( | ||
| record.getSortcode(), | ||
| record.getCustomerNumber(), | ||
| record.getName(), | ||
| record.getAddress(), | ||
| record.getDateOfBirth(), | ||
| record.getCreditScore() == null ? 0 : record.getCreditScore(), | ||
| record.getCsReviewDate() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| package com.augment.cbsa.service; | ||
|
|
||
| import com.augment.cbsa.domain.CustomerDetails; | ||
| import com.augment.cbsa.domain.InqcustRequest; | ||
| import com.augment.cbsa.domain.InqcustResult; | ||
| import com.augment.cbsa.error.CbsaAbendException; | ||
| import com.augment.cbsa.repository.CustomerRepository; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import org.jooq.exception.DataAccessException; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| public class InqcustService { | ||
|
|
||
| private static final String ABEND_CODE = "CVR1"; | ||
| private static final String NO_CUSTOMERS_EXIST_MESSAGE = "No customers exist."; | ||
| private static final String NOT_FOUND_CODE = "1"; | ||
| private static final String RANDOM_RETRY_EXHAUSTED_CODE = "R"; | ||
| private static final long RANDOM_CUSTOMER_NUMBER = 0L; | ||
| private static final long LAST_CUSTOMER_NUMBER = 9_999_999_999L; | ||
| private static final int RANDOM_RETRY_LIMIT = 1000; | ||
|
|
||
| private final CustomerRepository customerRepository; | ||
| private final RandomCustomerNumberGenerator randomCustomerNumberGenerator; | ||
| private final String sortcode; | ||
|
|
||
| public InqcustService( | ||
| CustomerRepository customerRepository, | ||
| @Value("${cbsa.sortcode}") String sortcode, | ||
| RandomCustomerNumberGenerator randomCustomerNumberGenerator | ||
| ) { | ||
| this.customerRepository = customerRepository; | ||
| this.sortcode = sortcode; | ||
| this.randomCustomerNumberGenerator = randomCustomerNumberGenerator; | ||
| } | ||
|
|
||
| public InqcustResult inquire(InqcustRequest request) { | ||
| Objects.requireNonNull(request, "request must not be null"); | ||
|
|
||
| try { | ||
| long customerNumber = request.customerNumber(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If Severity: low 🤖 Was this useful? React with 👍 or 👎 |
||
|
|
||
| if (customerNumber == RANDOM_CUSTOMER_NUMBER) { | ||
| return findRandomCustomer(); | ||
| } | ||
|
|
||
| if (customerNumber == LAST_CUSTOMER_NUMBER) { | ||
| return customerRepository.findLastBySortcode(sortcode) | ||
| .map(InqcustResult::success) | ||
| .orElseGet(() -> InqcustResult.failure( | ||
| NOT_FOUND_CODE, | ||
| customerNumber, | ||
| NO_CUSTOMERS_EXIST_MESSAGE | ||
| )); | ||
| } | ||
|
|
||
| return customerRepository.findBySortcodeAndCustomerNumber(sortcode, customerNumber) | ||
| .map(InqcustResult::success) | ||
| .orElseGet(() -> InqcustResult.failure( | ||
| NOT_FOUND_CODE, | ||
| customerNumber, | ||
| "Customer number %d was not found.".formatted(customerNumber) | ||
| )); | ||
| } catch (DataAccessException exception) { | ||
| throw new CbsaAbendException(ABEND_CODE, "INQCUST failed to read the customer data.", exception); | ||
| } | ||
| } | ||
|
|
||
| private InqcustResult findRandomCustomer() { | ||
| Optional<CustomerDetails> lastCustomer = customerRepository.findLastBySortcode(sortcode); | ||
| if (lastCustomer.isEmpty() || lastCustomer.get().customerNumber() < 1) { | ||
| return InqcustResult.failure(NOT_FOUND_CODE, RANDOM_CUSTOMER_NUMBER, NO_CUSTOMERS_EXIST_MESSAGE); | ||
| } | ||
|
|
||
| long highestCustomerNumber = lastCustomer.get().customerNumber(); | ||
| for (int attempt = 0; attempt < RANDOM_RETRY_LIMIT; attempt++) { | ||
| long candidate = randomCustomerNumberGenerator.nextCustomerNumber(highestCustomerNumber); | ||
| Optional<CustomerDetails> customer = customerRepository.findBySortcodeAndCustomerNumber(sortcode, candidate); | ||
| if (customer.isPresent()) { | ||
| return InqcustResult.success(customer.get()); | ||
| } | ||
| } | ||
|
|
||
| return InqcustResult.failure( | ||
| RANDOM_RETRY_EXHAUSTED_CODE, | ||
| RANDOM_CUSTOMER_NUMBER, | ||
| "Unable to find a random customer after exhausting retry attempts." | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.augment.cbsa.service; | ||
|
|
||
| @FunctionalInterface | ||
| public interface RandomCustomerNumberGenerator { | ||
|
|
||
| long nextCustomerNumber(long highestCustomerNumber); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
highestCustomerNumber + 1can overflow if the DB ever contains a very largecustomer_number(there’s no DB constraint on the range), which would makeThreadLocalRandom.nextLong(...)throw and turn random lookups into 500s.Severity: medium
🤖 Was this useful? React with 👍 or 👎