PaymentGatewayService.java
Bridget/payment_solutions/src/main/java/com/checkout/payment/gateway/service/PaymentGatewayService.java
package com.checkout.payment.gateway.service;
import com.checkout.payment.gateway.client.BankClient;
import com.checkout.payment.gateway.entity.IdempotencyEntity;
import com.checkout.payment.gateway.entity.PaymentEntity;
import com.checkout.payment.gateway.enums.PaymentStatus;
import com.checkout.payment.gateway.exception.EventProcessingException;
import com.checkout.payment.gateway.exception.IdempotencyConflictException;
import com.checkout.payment.gateway.model.BankRequest;
import com.checkout.payment.gateway.model.BankResponse;
import com.checkout.payment.gateway.model.GetPaymentResponse;
import com.checkout.payment.gateway.model.PostPaymentRequest;
import com.checkout.payment.gateway.model.PostPaymentResponse;
import com.checkout.payment.gateway.model.ProcessPaymentResult;
import com.checkout.payment.gateway.repository.IdempotencyJpaRepository;
import com.checkout.payment.gateway.repository.PaymentJpaRepository;
import com.checkout.payment.gateway.security.RequestFingerprinter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Instant;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.Currency;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@Service
public class PaymentGatewayService {
private static final Logger LOG = LoggerFactory.getLogger(PaymentGatewayService.class);
private static final Set<String> SUPPORTED_CURRENCIES = Set.of("USD", "GBP", "EUR");
private final PaymentJpaRepository paymentJpaRepository;
private final IdempotencyJpaRepository idempotencyJpaRepository;
private final BankClient bankClient;
private final ObjectMapper objectMapper;
private final RequestFingerprinter requestFingerprinter;
public PaymentGatewayService(
PaymentJpaRepository paymentJpaRepository,
IdempotencyJpaRepository idempotencyJpaRepository,
BankClient bankClient,
ObjectMapper objectMapper,
RequestFingerprinter requestFingerprinter) {
this.paymentJpaRepository = paymentJpaRepository;
this.idempotencyJpaRepository = idempotencyJpaRepository;
this.bankClient = bankClient;
this.objectMapper = objectMapper;
this.requestFingerprinter = requestFingerprinter;
}
public GetPaymentResponse getPaymentById(UUID id) {
LOG.debug("Retrieving payment with ID {}", id);
PaymentEntity entity = paymentJpaRepository.findById(id)
.orElseThrow(() -> new EventProcessingException("Payment not found"));
return toGetResponse(entity);
}
@Transactional
public ProcessPaymentResult processPayment(String idempotencyKey, PostPaymentRequest request) {
String normalizedKey = StringUtils.hasText(idempotencyKey) ? idempotencyKey.trim() : null;
String fingerprint = null;
if (normalizedKey != null) {
fingerprint = requestFingerprinter.fingerprint(request);
Optional<IdempotencyEntity> existing = idempotencyJpaRepository.findById(normalizedKey);
if (existing.isPresent()) {
IdempotencyEntity row = existing.get();
if (!fingerprint.equals(row.getRequestFingerprint())) {
throw new IdempotencyConflictException(
"Idempotency-Key was reused with a different request payload");
}
try {
PostPaymentResponse cached = objectMapper.readValue(
row.getResponseJson(), PostPaymentResponse.class);
LOG.info("Returning idempotent replay for key {}", normalizedKey);
return new ProcessPaymentResult(cached, row.getHttpStatus());
} catch (JsonProcessingException e) {
throw new IllegalStateException("Corrupt idempotency payload", e);
}
}
}
List<String> validationErrors = validate(request);
if (!validationErrors.isEmpty()) {
LOG.warn("Payment request rejected (validation): {} error(s)", validationErrors.size());
PostPaymentResponse rejected = new PostPaymentResponse();
rejected.setStatus(PaymentStatus.REJECTED);
rejected.setErrors(validationErrors);
if (normalizedKey != null) {
persistIdempotency(normalizedKey, fingerprint, 400, rejected, null);
}
return new ProcessPaymentResult(rejected, 400);
}
BankRequest bankRequest = new BankRequest(
request.getCardNumber(),
request.getExpiryMonth(),
request.getExpiryYear(),
request.getCurrency(),
request.getAmount(),
request.getCvv()
);
BankResponse bankResponse = bankClient.processPayment(bankRequest);
PostPaymentResponse response = new PostPaymentResponse();
UUID paymentId = UUID.randomUUID();
response.setId(paymentId);
response.setStatus(bankResponse.isAuthorized()
? PaymentStatus.AUTHORIZED
: PaymentStatus.DECLINED);
response.setCardNumberLastFour(extractLastFour(request.getCardNumber()));
response.setExpiryMonth(request.getExpiryMonth());
response.setExpiryYear(request.getExpiryYear());
response.setCurrency(request.getCurrency());
response.setAmount(request.getAmount());
PaymentEntity entity = toEntity(response);
paymentJpaRepository.save(entity);
if (normalizedKey != null) {
persistIdempotency(normalizedKey, fingerprint, 200, response, paymentId);
}
LOG.info("Payment {} processed with status {}", paymentId, response.getStatus());
return new ProcessPaymentResult(response, 200);
}
private void persistIdempotency(
String key,
String fingerprint,
int httpStatus,
PostPaymentResponse response,
UUID paymentId) {
try {
IdempotencyEntity row = new IdempotencyEntity();
row.setIdempotencyKey(key);
row.setRequestFingerprint(fingerprint);
row.setHttpStatus(httpStatus);
row.setResponseJson(objectMapper.writeValueAsString(response));
row.setPaymentId(paymentId);
row.setCreatedAt(Instant.now());
idempotencyJpaRepository.save(row);
} catch (JsonProcessingException e) {
throw new IllegalStateException("Failed to serialize idempotent response", e);
}
}
List<String> validate(PostPaymentRequest request) {
List<String> errors = new ArrayList<>();
if (request.getCardNumber() == null || request.getCardNumber().isBlank()) {
errors.add("Card number is required");
} else {
if (request.getCardNumber().length() < 14 || request.getCardNumber().length() > 19) {
errors.add("Card number must be between 14 and 19 characters");
}
if (!request.getCardNumber().matches("\\d+")) {
errors.add("Card number must contain only numeric characters");
}
}
if (request.getExpiryMonth() == null) {
errors.add("Expiry month is required");
} else if (request.getExpiryMonth() < 1 || request.getExpiryMonth() > 12) {
errors.add("Expiry month must be between 1 and 12");
}
if (request.getExpiryYear() == null) {
errors.add("Expiry year is required");
}
if (request.getExpiryMonth() != null && request.getExpiryYear() != null
&& request.getExpiryMonth() >= 1 && request.getExpiryMonth() <= 12) {
YearMonth expiry = YearMonth.of(request.getExpiryYear(), request.getExpiryMonth());
if (!expiry.isAfter(YearMonth.now())) {
errors.add("Card expiry date must be in the future");
}
}
if (request.getCurrency() == null || request.getCurrency().isBlank()) {
errors.add("Currency is required");
} else if (request.getCurrency().length() != 3) {
errors.add("Currency must be a 3-letter ISO 4217 code");
} else if (!isValidIsoCurrency(request.getCurrency())) {
errors.add("Currency is not a valid ISO 4217 code");
} else if (!SUPPORTED_CURRENCIES.contains(request.getCurrency().toUpperCase())) {
errors.add("Currency is not supported. Accepted currencies: USD, GBP, EUR");
}
if (request.getAmount() == null) {
errors.add("Amount is required");
} else if (request.getAmount() <= 0) {
errors.add("Amount must be a positive integer");
}
if (request.getCvv() == null || request.getCvv().isBlank()) {
errors.add("CVV is required");
} else {
if (request.getCvv().length() < 3 || request.getCvv().length() > 4) {
errors.add("CVV must be 3 or 4 characters");
}
if (!request.getCvv().matches("\\d+")) {
errors.add("CVV must contain only numeric characters");
}
}
return errors;
}
private boolean isValidIsoCurrency(String code) {
try {
Currency.getInstance(code.toUpperCase());
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
private int extractLastFour(String cardNumber) {
String lastFour = cardNumber.substring(cardNumber.length() - 4);
return Integer.parseInt(lastFour);
}
private static PaymentEntity toEntity(PostPaymentResponse response) {
PaymentEntity entity = new PaymentEntity();
entity.setId(response.getId());
entity.setStatus(response.getStatus());
entity.setCardNumberLastFour(response.getCardNumberLastFour());
entity.setExpiryMonth(response.getExpiryMonth());
entity.setExpiryYear(response.getExpiryYear());
entity.setCurrency(response.getCurrency());
entity.setAmount(response.getAmount());
entity.setCreatedAt(Instant.now());
return entity;
}
private static GetPaymentResponse toGetResponse(PaymentEntity entity) {
GetPaymentResponse response = new GetPaymentResponse();
response.setId(entity.getId());
response.setStatus(entity.getStatus());
response.setCardNumberLastFour(entity.getCardNumberLastFour());
response.setExpiryMonth(entity.getExpiryMonth());
response.setExpiryYear(entity.getExpiryYear());
response.setCurrency(entity.getCurrency());
response.setAmount(entity.getAmount());
return response;
}
}
相關文章
PaymentGatewayApplication.java
PaymentGatewayApplication.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/PaymentGatewayApplication.java).
閱讀文章 →ApplicationConfiguration.java
ApplicationConfiguration.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/configuration/ApplicationConfiguration.java).
閱讀文章 →RequestResponseLogger.java
RequestResponseLogger.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/configuration/RequestResponseLogger.java).
閱讀文章 →PaymentGatewayController.java
PaymentGatewayController.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/controller/PaymentGatewayController.java).
閱讀文章 →PaymentStatus.java
PaymentStatus.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/enums/PaymentStatus.java).
閱讀文章 →BankErrorException.java
BankErrorException.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/exception/BankErrorException.java).
閱讀文章 →