PaymentApplicationService.java
package com.example.orderplatform.payments.application;
import com.example.orderplatform.BusinessRuleViolationException;
import com.example.orderplatform.Money;
import com.example.orderplatform.ResourceNotFoundException;
import com.example.orderplatform.orders.api.OrderCreatedEvent;
import com.example.orderplatform.payments.api.PaymentAuthorizedEvent;
import com.example.orderplatform.payments.api.PaymentManagement;
import com.example.orderplatform.payments.api.PaymentSummary;
import com.example.orderplatform.payments.domain.Payment;
import java.time.OffsetDateTime;
import java.util.UUID;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
/**
* Application service that prepares payments from committed orders and authorizes matching payments.
*/
@Service
@Transactional
public class PaymentApplicationService implements PaymentManagement {
private final PaymentStore payments;
private final ApplicationEventPublisher events;
public PaymentApplicationService(PaymentStore payments, ApplicationEventPublisher events) {
this.payments = payments;
this.events = events;
}
/**
* Prepares one pending payment for a committed order.
*
* <p>The listener runs after the order transaction commits and uses a new transaction so payment
* preparation is not part of the original order write.
*
* @param event committed order event
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void preparePayment(OrderCreatedEvent event) {
payments.findByOrderId(event.orderId()).orElseGet(() -> payments.save(Payment.pending(
UUID.randomUUID(),
event.orderId(),
Money.of(event.totalAmount(), event.currency()),
OffsetDateTime.now())));
}
/**
* Authorizes an existing pending payment when the submitted amount equals the order total.
*
* @param orderId order identifier
* @param amount submitted payment amount
* @return authorized payment summary
*/
@Override
public PaymentSummary authorize(UUID orderId, Money amount) {
Payment payment = payments.findByOrderId(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Payment for order " + orderId + " was not found."));
if (!payment.amount().equals(amount)) {
throw new BusinessRuleViolationException("Payment amount must match the submitted order total.");
}
PaymentSummary summary = toSummary(payments.save(payment.authorize(OffsetDateTime.now())));
events.publishEvent(new PaymentAuthorizedEvent(
summary.id(),
summary.orderId(),
summary.amount().amount(),
summary.amount().currency(),
summary.authorizedAt()));
return summary;
}
/**
* Retrieves a payment by identifier.
*
* @param paymentId payment identifier
* @return payment summary
* @throws ResourceNotFoundException when the payment does not exist
*/
@Override
@Transactional(readOnly = true)
public PaymentSummary getPayment(UUID paymentId) {
return payments.findById(paymentId)
.map(this::toSummary)
.orElseThrow(() -> new ResourceNotFoundException("Payment " + paymentId + " was not found."));
}
private PaymentSummary toSummary(Payment payment) {
return new PaymentSummary(
payment.id(),
payment.orderId(),
payment.status().name(),
payment.amount(),
payment.createdAt(),
payment.authorizedAt());
}
}