PaymentService.java
package com.ctrlbuy.webshop.service;
import com.ctrlbuy.webshop.exception.PaymentException;
import com.ctrlbuy.webshop.model.*;
import com.ctrlbuy.webshop.enums.PaymentStatus;
import com.ctrlbuy.webshop.enums.PaymentType;import com.ctrlbuy.webshop.repository.OrderRepository;
import com.ctrlbuy.webshop.repository.PaymentRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.regex.Pattern;
@Service
@Transactional
public class PaymentService {
private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
// Regex för olika korttyper
private static final Pattern VISA_PATTERN = Pattern.compile("^4[0-9]{12}(?:[0-9]{3})?$");
private static final Pattern MASTERCARD_PATTERN = Pattern.compile("^5[1-5][0-9]{14}$");
private static final Pattern AMEX_PATTERN = Pattern.compile("^3[47][0-9]{13}$");
@Autowired
private PaymentGateway paymentGateway;
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentRepository paymentRepository;
/**
* Bearbetar betalning för en order
*/
public PaymentResult processPaymentForOrder(Order order, PaymentInfo paymentInfo) {
logger.info("Bearbetar betalning för order: {}", order.getOrderNumber());
// Validera att belopp matchar
if (!paymentInfo.getAmount().equals(BigDecimal.valueOf(order.getTotalAmount()))) {
throw new PaymentException("Belopp matchar inte order. Order: " +
BigDecimal.valueOf(order.getTotalAmount()) + ", Betalning: " + paymentInfo.getAmount());
}
// Validera betalningsinformation
validatePaymentInfo(paymentInfo);
try {
// Bearbeta betalning
PaymentResult result = processPayment(paymentInfo);
if (result.isSuccess()) {
// Uppdatera order med betalningsinformation
order.setPaymentStatus(PaymentStatus.COMPLETED);
order.setTransactionId(result.getTransactionId());
orderRepository.save(order);
// Spara betalningsrecord
Payment payment = createPaymentRecord(order, paymentInfo, result);
paymentRepository.save(payment);
logger.info("Betalning genomförd för order {}: {}",
order.getOrderNumber(), result.getTransactionId());
} else {
order.setPaymentStatus(PaymentStatus.FAILED);
orderRepository.save(order);
logger.warn("Betalning misslyckades för order {}: {}",
order.getOrderNumber(), result.getErrorMessage());
}
return result;
} catch (Exception e) {
order.setPaymentStatus(PaymentStatus.FAILED);
orderRepository.save(order);
logger.error("Fel vid betalningsbearbetning för order {}: {}",
order.getOrderNumber(), e.getMessage());
throw new PaymentException("Payment gateway fel: " + e.getMessage(), e);
}
}
/**
* Bearbetar betalning (generisk metod)
*/
public PaymentResult processPayment(PaymentInfo paymentInfo) {
logger.debug("Bearbetar betalning för belopp: {}", paymentInfo.getAmount());
// Validera betalningsinformation
validatePaymentInfo(paymentInfo);
try {
// Maskera kortnummer för loggar
PaymentInfo maskedInfo = maskPaymentInfo(paymentInfo);
logger.info("Skickar betalning till gateway: {}", maskedInfo.getCardType());
// Anropa payment gateway med retry-logik
PaymentResult result = processWithRetry(paymentInfo, 3);
if (result.isSuccess()) {
logger.info("Betalning godkänd: {}", result.getTransactionId());
} else {
logger.warn("Betalning avvisad: {}", result.getErrorMessage());
}
return result;
} catch (Exception e) {
logger.error("Payment gateway error: {}", e.getMessage());
throw new PaymentException("Payment gateway timeout eller fel", e);
}
}
/**
* Återbetalar en order
*/
public boolean refundPayment(Order order) {
logger.info("Bearbetar återbetalning för order: {}", order.getOrderNumber());
// Kontrollera att order kan återbetalas
if (order.getPaymentStatus() == PaymentStatus.REFUNDED) {
throw new PaymentException("Order är redan återbetald");
}
if (order.getPaymentStatus() != PaymentStatus.COMPLETED) {
throw new PaymentException("Endast betalda orders kan återbetalas");
}
if (order.getTransactionId() == null) {
throw new PaymentException("Ingen transaktions-ID hittad för återbetalning");
}
try {
// Anropa payment gateway för återbetalning
boolean success = paymentGateway.refund(order.getTransactionId(), BigDecimal.valueOf(order.getTotalAmount()));
if (success) {
order.setPaymentStatus(PaymentStatus.REFUNDED);
order.setRefundedAt(LocalDateTime.now());
orderRepository.save(order);
// Skapa återbetalningsrecord
createRefundRecord(order);
logger.info("Återbetalning genomförd för order {}", order.getOrderNumber());
} else {
logger.error("Återbetalning misslyckades för order {}", order.getOrderNumber());
}
return success;
} catch (Exception e) {
logger.error("Fel vid återbetalning för order {}: {}",
order.getOrderNumber(), e.getMessage());
throw new PaymentException("Återbetalning misslyckades: " + e.getMessage(), e);
}
}
/**
* Validerar betalningsinformation
*/
public boolean validatePaymentInfo(PaymentInfo paymentInfo) {
if (paymentInfo == null) {
throw new PaymentException("Betalningsinformation saknas");
}
// Validera kortnummer
if (!validateCardNumber(paymentInfo.getCardNumber())) {
throw new PaymentException("Ogiltigt kortnummer");
}
// Validera utgångsdatum
if (!validateExpiryDate(paymentInfo.getExpiryMonth().toString(), paymentInfo.getExpiryYear().toString())) {
throw new PaymentException("Kortet har gått ut eller ogiltigt utgångsdatum");
}
// Validera CVV
if (!validateCVV(paymentInfo.getCvv())) {
throw new PaymentException("Ogiltig CVV-kod");
}
// Validera belopp
if (paymentInfo.getAmount() == null || paymentInfo.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new PaymentException("Ogiltigt belopp");
}
return true;
}
/**
* Validerar kortnummer med Luhn-algoritmen
*/
public boolean validateCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.trim().isEmpty()) {
return false;
}
// Ta bort mellanslag och bindestreck
String cleanNumber = cardNumber.replaceAll("[\\s-]", "");
// Kontrollera att det bara innehåller siffror
if (!cleanNumber.matches("\\d+")) {
return false;
}
// Kontrollera längd (13-19 siffror är vanligt)
if (cleanNumber.length() < 13 || cleanNumber.length() > 19) {
return false;
}
// Luhn-algoritm för validering
return luhnCheck(cleanNumber) && validateCardType(cleanNumber);
}
/**
* Validerar utgångsdatum
*/
public boolean validateExpiryDate(String month, String year) {
if (month == null || year == null) {
return false;
}
try {
int monthInt = Integer.parseInt(month);
int yearInt = Integer.parseInt(year);
// Kontrollera giltiga månad
if (monthInt < 1 || monthInt > 12) {
return false;
}
// Hantera 2-siffriga år
if (yearInt < 100) {
yearInt += 2000;
}
YearMonth cardExpiry = YearMonth.of(yearInt, monthInt);
YearMonth currentMonth = YearMonth.now();
return !cardExpiry.isBefore(currentMonth);
} catch (NumberFormatException e) {
return false;
}
}
/**
* Validerar CVV
*/
public boolean validateCVV(String cvv) {
if (cvv == null) {
return false;
}
// CVV ska vara 3-4 siffror
return cvv.matches("\\d{3,4}");
}
/**
* Förbereder betalningsinformation för säker lagring
*/
public PaymentInfo preparForStorage(PaymentInfo paymentInfo) {
PaymentInfo sanitized = new PaymentInfo();
// Maskera kortnummer (visa bara sista 4 siffror)
String cardNumber = paymentInfo.getCardNumber();
if (cardNumber != null && cardNumber.length() > 4) {
sanitized.setCardNumber("****-****-****-" + cardNumber.substring(cardNumber.length() - 4));
}
sanitized.setCardType(paymentInfo.getCardType());
sanitized.setAmount(paymentInfo.getAmount());
sanitized.setExpiryMonth(paymentInfo.getExpiryMonth());
sanitized.setExpiryYear(paymentInfo.getExpiryYear());
// Ta ALDRIG med CVV i lagring
sanitized.setCvv(null);
return sanitized;
}
// ===== PRIVATE HELPER METHODS =====
private PaymentResult processWithRetry(PaymentInfo paymentInfo, int maxRetries) {
Exception lastException = null;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return paymentGateway.processPayment(paymentInfo);
} catch (Exception e) {
lastException = e;
logger.warn("Betalningsförsök {} misslyckades: {}", attempt, e.getMessage());
if (attempt < maxRetries) {
// Vänta lite mellan försök
try {
Thread.sleep(1000 * attempt); // Exponential backoff
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
throw new PaymentException("Payment gateway timeout efter " + maxRetries + " försök", lastException);
}
private boolean luhnCheck(String cardNumber) {
int sum = 0;
boolean alternate = false;
for (int i = cardNumber.length() - 1; i >= 0; i--) {
int digit = Character.getNumericValue(cardNumber.charAt(i));
if (alternate) {
digit *= 2;
if (digit > 9) {
digit = (digit % 10) + 1;
}
}
sum += digit;
alternate = !alternate;
}
return (sum % 10) == 0;
}
private boolean validateCardType(String cardNumber) {
// Identifiera korttyp och validera format
if (VISA_PATTERN.matcher(cardNumber).matches()) {
return true;
} else if (MASTERCARD_PATTERN.matcher(cardNumber).matches()) {
return true;
} else if (AMEX_PATTERN.matcher(cardNumber).matches()) {
return true;
}
// Acceptera andra korttyper med grundläggande validering
return cardNumber.length() >= 13 && cardNumber.length() <= 19;
}
private String detectCardType(String cardNumber) {
if (VISA_PATTERN.matcher(cardNumber).matches()) {
return "VISA";
} else if (MASTERCARD_PATTERN.matcher(cardNumber).matches()) {
return "MASTERCARD";
} else if (AMEX_PATTERN.matcher(cardNumber).matches()) {
return "AMEX";
} else {
return "UNKNOWN";
}
}
private PaymentInfo maskPaymentInfo(PaymentInfo paymentInfo) {
PaymentInfo masked = new PaymentInfo();
masked.setCardType(detectCardType(paymentInfo.getCardNumber()));
masked.setAmount(paymentInfo.getAmount());
// Kortnummer visas inte i loggar
return masked;
}
private Payment createPaymentRecord(Order order, PaymentInfo paymentInfo, PaymentResult result) {
Payment payment = new Payment();
payment.setOrder(order);
payment.setAmount(paymentInfo.getAmount());
payment.setCardType(detectCardType(paymentInfo.getCardNumber()));
payment.setTransactionId(result.getTransactionId());
payment.setStatus(PaymentStatus.COMPLETED);
payment.setProcessedAt(LocalDateTime.now());
// Maskera kortnummer
payment.setMaskedCardNumber("****-****-****-" +
paymentInfo.getCardNumber().substring(paymentInfo.getCardNumber().length() - 4));
return payment;
}
private void createRefundRecord(Order order) {
Payment refund = new Payment();
refund.setOrder(order);
refund.setAmount(BigDecimal.valueOf(order.getTotalAmount()).negate()); // Negativt belopp för återbetalning
refund.setTransactionId(order.getTransactionId() + "-REFUND");
refund.setStatus(PaymentStatus.REFUNDED);
refund.setProcessedAt(LocalDateTime.now());
refund.setType(com.ctrlbuy.webshop.enums.PaymentType.REFUND);
paymentRepository.save(refund);
}
}