S SmartDocs
系列: Bridget java 256 行 · 更新於 2026-04-18

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;
    }
}

相關文章

Bridget java 更新於 2026-03-30

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).

閱讀文章 →
Bridget java 更新於 2026-03-20

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).

閱讀文章 →
Bridget java 更新於 2026-03-30

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).

閱讀文章 →
Bridget java 更新於 2026-04-14

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).

閱讀文章 →
Bridget java 更新於 2026-03-20

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).

閱讀文章 →
Bridget java 更新於 2026-03-29

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).

閱讀文章 →