This is order, orderitems and payment linked by foreign key.
Controller:
@PatchMapping("/orders/buy/{salesJourneyId}")
@Operation(summary = "To create either card or cash order in cash-hub for BUY flow", description = "To create either card or cash order in cash-hub for BUY flow")
public ResponseEntity newOrderBuyFlow(
@RequestHeader("retailerId") String retailerId,
@RequestHeader("source") String source,
@RequestHeader("trackingId") String trackingId,
@PathVariable("salesJourneyId") String salesJourneyId,
@RequestBody @Valid SalesOrderSubmissionCardRequest orderRequest
) {
validator.validateDealerLoginHeaders();
ResponseEnvelop<Map<String, Object>> response = salesOrderService.processOrder(salesJourneyId, orderRequest,
source, trackingId, retailerId);
return ResponseEntity.ok(response);
}
Service:
public CashHubResponseEnvelop<Map<String, Object>> processOrder(String salesJourneyId, SalesOrderSubmissionCardRequest orderRequest,
String source, String trackingId, String retailerId) {
logger.info("Processing order for Sales Journey ID: {}", salesJourneyId);
validateRequest(orderRequest);
Orders order = Orders.builder()
.orderId(Long.valueOf(orderRequest.getOrderSummary().getId()))
.orderType(orderRequest.getOrderSummary().getType())
.description(orderRequest.getOrderSummary().getDescription())
.amount(BigDecimal.valueOf(orderRequest.getOrderSummary().getTotal()))
.phone(Long.valueOf(orderRequest.getCustomer().getPhone()))
.zip(orderRequest.getCustomer().getZip())
.customerRelationshipId(orderRequest.getCustomer().getCustomerRelationshipId())
.salesJourneyId(salesJourneyId)
.build();
salesJourneyDao.saveOrder(order, source, trackingId, retailerId);
for (PricePlan pricePlan : orderRequest.getPricePlans()) {
for (Plan plan : pricePlan.getPlans()) {
OrderItems orderItem = OrderItems.builder()
.ordersObjId(order.getOrderId())
.lob(pricePlan.getLob())
.planId(plan.getId())
.unitPrice(BigDecimal.valueOf(plan.getUnitPrice()))
.quantity(plan.getQuantity())
.build();
salesJourneyDao.saveOrderItem(orderItem, source, trackingId);
}
}
String paymentStatus = orderRequest.getBilling().getPaymentMethod().equals("CARD") ? Constants.COMPLETE : Constants.PENDING_POS;
Payments payment = Payments.builder()
.orderItemsObjId(order.getOrderId()) // Foreign key to OrderItems
.cashhubRef(orderRequest.getBilling().getCashhubRef())
.paymentMethod(orderRequest.getBilling().getPaymentMethod())
.paymentType(orderRequest.getBilling().getPaymentType())
.paymentStatus(paymentStatus)
.build();
salesJourneyDao.savePayment(payment, source, trackingId);
Map<String, Object> responseData = new HashMap<>();
responseData.put("hubConfNumber", orderRequest.getBilling().getCashhubRef());
logger.info("Response prepared successfully for Order ID: {}", order.getOrderId());
return ResponseEnvelop.<Map<String, Object>>builder()
.data(responseData)
.message("Order created successfully")
.error(null)
.build();
}
private void validateRequest(SalesOrderSubmissionCardRequest orderRequest) {
if (orderRequest.getCustomer() == null || orderRequest.getPricePlans() == null || orderRequest.getOrderSummary() == null || orderRequest.getBilling() == null) {
throw new InvalidRequestException("Missing required fields");
}
}
Repository
@Repository
public class SalesJourneyDaoImpl implements SalesJourneyDao {
private final JdbcTemplate jdbcTemplate;
@Value("${insert.salesJourney}")
private String insertSalesJourneyQuery;
@Value("${select.byId}")
private String selectByIdQuery;
@Value("${select.bySalesJourneyId}")
private String selectBySalesJourneyIdQuery;
@Value("${exists.bySalesJourneyId}")
private String existsBySalesJourneyIdQuery;
@Value("${insert.order}")
private String insertOrderQuery;
@Value("${insert.orderItem}")
private String insertOrderItemQuery;
@Value("${insert.payment}")
private String insertPaymentQuery;
@Autowired
public SalesJourneyDaoImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void save(Orders salesJourney) {
jdbcTemplate.update(
insertSalesJourneyQuery,
salesJourney.getSalesJourneyId(),
salesJourney.getRetailerId(),
salesJourney.getSource(),
salesJourney.getTrackingId(),
Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/New_York")))
);
}
@Override
public Optional<Orders> findById(Long id) {
try {
Orders salesJourney = jdbcTemplate.queryForObject(
selectByIdQuery,
salesJourneyRowMapper(),
id
);
return Optional.ofNullable(salesJourney);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public Optional<Orders> findBySalesJourneyId(String salesJourneyId) {
try {
Orders salesJourney = jdbcTemplate.queryForObject(
selectBySalesJourneyIdQuery,
salesJourneyRowMapper(),
salesJourneyId
);
return Optional.ofNullable(salesJourney);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public boolean existsBySalesJourneyId(String salesJourneyId) {
Integer count = jdbcTemplate.queryForObject(
existsBySalesJourneyIdQuery,
Integer.class,
salesJourneyId
);
return count != null && count > 0;
}
private RowMapper<Orders> salesJourneyRowMapper() {
return new RowMapper<Orders>() {
@Override
public Orders mapRow(ResultSet rs, int rowNum) throws SQLException {
Orders salesJourney = new Orders();
salesJourney.setId(rs.getLong("id"));
salesJourney.setSalesJourneyId(rs.getString("sales_journey_id"));
salesJourney.setRetailerId(rs.getString("retailer_id"));
salesJourney.setSource(rs.getString("source"));
salesJourney.setTrackingId(rs.getString("tracking_id"));
salesJourney.setCreateDate(rs.getTimestamp("created_at").toLocalDateTime());
return salesJourney;
}
};
}
public void saveOrder(Orders order, String source, String trackingId, String retailerId) {
jdbcTemplate.update(insertOrderQuery, order.getOrderId(), order.getOrderType(), order.getDescription(), order.getAmount(),
order.getPhone(), order.getZip(), order.getCustomerRelationshipId(), order.getSalesJourneyId(), source, trackingId, retailerId,
Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/New_York"))));
}
public void saveOrderItem(OrderItems orderItem, String source, String trackingId) {
jdbcTemplate.update(insertOrderItemQuery, orderItem.getOrdersObjId(), orderItem.getLob(), orderItem.getPlanId(), orderItem.getUnitPrice(),
orderItem.getQuantity(), source, trackingId, Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/New_York"))));
}
public void savePayment(Payments payment, String source, String trackingId) {
jdbcTemplate.update(insertPaymentQuery, payment.getOrderItemsObjId(), payment.getPaymentMethod(), payment.getPaymentStatus(),
payment.getCashhubRef(), source, trackingId, Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/New_York"))));
}
}
controller advice
@RestControllerAdvice
public class GlobalControllerAdvice extends ResponseEntityExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalControllerAdvice.class);
@Autowired
@Qualifier("MessageSource")
private ResourceBundleMessageSource messageSource;
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException exception,
HttpHeaders httpHeaders, HttpStatus httpStatus, WebRequest webRequest) {
// Map validation errors to the Error class
List<ErrorCashhub> errors = exception.getBindingResult()
.getFieldErrors()
.stream()
.map(fieldError -> ErrorCashhub.builder()
.code("ERR_VALIDATION")
.message(fieldError.getDefaultMessage())
.details("Field: " + fieldError.getField())
.build())
.collect(Collectors.toList());
return new ResponseEntity<>(errors, httpHeaders, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(CashHubException.class)
public ResponseEntity<CashHubResponseEnvelop<Void>> handleDealerPortalException(CashHubException ex) {
return ResponseEntity
.status(ex.getHttpStatus())
.body(new CashHubResponseEnvelop<>(ex.getApiError()));
}
@ExceptionHandler(MissingRequestHeaderException.class)
public ResponseEntity<CashHubResponseEnvelop<ErrorCashhub>> handleMissingRequestHeaderException(MissingRequestHeaderException ex) {
ErrorCashhub error = ErrorCashhub.builder()
.code(ENUMConfig.ERR_VALIDATION.toString())
.message(ENUMConfig.ERR_VALIDATION.getValue())
.details("Required header is missing: " + ex.getHeaderName())
.build();
CashHubResponseEnvelop<ErrorCashhub> response = new CashHubResponseEnvelop<>(error);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(InvalidHeaderException.class)
public ResponseEntity<CashHubResponseEnvelop<ErrorCashhub>> handleInvalidHeaderException(InvalidHeaderException ex, HttpServletRequest request) {
ErrorCashhub error = ErrorCashhub.builder()
.code(ENUMConfig.ERR_VALIDATION.toString())
.message(ENUMConfig.ERR_VALIDATION.getValue())
.details(ex.getMessage())
.build();
CashHubResponseEnvelop<ErrorCashhub> response = new CashHubResponseEnvelop<>(error);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception ex) {
return new ResponseEntity<>("Internal Server Error: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(InvalidRequestException.class)
public ResponseEntity<ErrorCashhub> handleInvalidRequestException(InvalidRequestException ex) {
ErrorCashhub errorResponse = ErrorCashhub.builder()
.code(ENUMConfig.ERR_VALIDATION.toString())
.message(ENUMConfig.ERR_VALIDATION.getValue())
.details("Invalid request parameters: " + ex.getMessage())
.build();
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(DataAccessException.class)
public ResponseEntity<ErrorCashhub> handleDatabaseException(DataAccessException ex) {
ErrorCashhub errorResponse = ErrorCashhub.builder()
.code(ENUMConfig.DATABASE_EXCEPTION_109.toString())
.message(ENUMConfig.DATABASE_EXCEPTION_109.getValue())
.details("Error while accessing the database")
.build();
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Validator:
@Component
public class Validator {
private static final String SPLIT_REGEX = "\\s*,\\s*";
private final Logger log = LoggerFactory.getLogger(Validator.class);
@Autowired
private WebRequest webRequest;
@Value("${eligible.sources}")
private String eligibleSourcesStr;
@Value("${eligible.dealerLogin.sources}")
private String eligibleDealerLoginSourcesStr;
private List<String> eligibleSourceList;
private List<String> eligibleDealerLoginSourceList;
Validator() {
log.info("Validator initialized!");
}
@PostConstruct
private void initFields() {
if (!empty(eligibleSourcesStr)) {
eligibleSourceList = Arrays.asList(eligibleSourcesStr.toUpperCase().split(SPLIT_REGEX));
}
log.info("eligibleSources: {} ", eligibleSourceList);
}
private boolean empty(final String s) {
return s == null || s.trim().isEmpty();
}
public void validateDealerLoginHeaders() {
final String trackingId = webRequest.getHeader(Constants.HEADER_TRACKING_ID);
final String source = webRequest.getHeader(Constants.HEADER_SOURCE);
final String retailerId = webRequest.getHeader(Constants.HEADER_RETAILER_ID);
if (!isInputValid(trackingId)) {
log.error(Constants.VALIDATION_ERROR_PRECONDITION_FAILED, ENUMConfig.DEALERPORTAL_SERVICE_COMMON_002);
throw new InvalidHeaderException("Header '" + trackingId + "' must not be null or empty.");
} else if (!isInputValid(source)) {
log.error(Constants.VALIDATION_ERROR_PRECONDITION_FAILED, ENUMConfig.DEALERPORTAL_SERVICE_COMMON_001);
throw new InvalidHeaderException("Header '" + source + "' must not be null or empty.");
} else if (!isInputValid(retailerId)) {
log.error(Constants.VALIDATION_ERROR_PRECONDITION_FAILED, ENUMConfig.DEALERPORTAL_SERVICE_COMMON_004);
throw new InvalidHeaderException("Header '" + retailerId + "' must not be null or empty.");
}
if (!isDealerLoginSourceAuthorized(source)) {
log.error(Constants.VALIDATION_ERROR_FORBIDDEN, ENUMConfig.DEALERPORTAL_SERVICE_COMMON_003);
throw new InvalidHeaderException("Header '" + source + "' unauthorized.");
}
}
private boolean isInputValid(String input) {
return input != null && !input.trim().equals("");
}
private boolean isDealerLoginSourceAuthorized(String source) {
return CollectionUtils.isEmpty(eligibleSourceList) ||
eligibleSourceList.contains(source.trim().toUpperCase());
}
}
Some things jumping out to me are logging along with transaction handling. There are one to many and many to one relation between table, can I leverage something else available with jdbctemplate?
1 Answer 1
you already invoke spring validation on SalesOrderSubmissionCardRequest
why do you need validateRequest() method? can be replaced with @NotNull(message = "Missing required fields")
annotation on fields in the request
processOrder()
should be broken down to several methods like saveOrder()
, savePricePlans()
etc in order to adhere to the single responsibility principle
since you already make use of apache common library, you can implement isInputValid(String input)
as StringUtils.isNotBlank(input)