PaymentService.java
package uk.gov.dhsc.htbhf.claimant.service.payments;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import uk.gov.dhsc.htbhf.claimant.entity.Payment;
import uk.gov.dhsc.htbhf.claimant.entity.PaymentCycle;
import uk.gov.dhsc.htbhf.claimant.exception.EventFailedException;
import uk.gov.dhsc.htbhf.claimant.model.card.CardBalanceResponse;
import uk.gov.dhsc.htbhf.claimant.model.card.DepositFundsRequest;
import uk.gov.dhsc.htbhf.claimant.model.card.DepositFundsResponse;
import uk.gov.dhsc.htbhf.claimant.reporting.PaymentAction;
import uk.gov.dhsc.htbhf.claimant.reporting.ReportPaymentMessageSender;
import uk.gov.dhsc.htbhf.claimant.repository.PaymentRepository;
import uk.gov.dhsc.htbhf.claimant.service.CardClient;
import uk.gov.dhsc.htbhf.claimant.service.audit.EventAuditor;
import uk.gov.dhsc.htbhf.claimant.service.audit.MakePaymentEvent;
import uk.gov.dhsc.htbhf.logging.event.FailureEvent;
import java.time.LocalDateTime;
import java.util.UUID;
import static uk.gov.dhsc.htbhf.claimant.factory.PaymentFactory.createFailedPayment;
import static uk.gov.dhsc.htbhf.claimant.factory.PaymentFactory.createSuccessfulPayment;
import static uk.gov.dhsc.htbhf.logging.ExceptionDetailGenerator.constructExceptionDetail;
@Service
@AllArgsConstructor
@Slf4j
public class PaymentService {
private CardClient cardClient;
private PaymentRepository paymentRepository;
private PaymentCycleService paymentCycleService;
private EventAuditor eventAuditor;
private PaymentCalculator paymentCalculator;
private ReportPaymentMessageSender reportPaymentMessageSender;
/**
* Calculates the amount to pay, based on the total amount from the voucher entitlement, taking into account the balance currently on the card.
*
* @param paymentCycle the current payment cycle, which should have a voucher entitlement.
* @return the amount that should be deposited on the card account.
*/
public PaymentCalculation calculatePaymentAmount(PaymentCycle paymentCycle) {
CardBalanceResponse balance = cardClient.getBalance(paymentCycle.getClaim().getCardAccountId());
return paymentCalculator.calculatePaymentCycleAmountInPence(paymentCycle.getVoucherEntitlement(), balance.getAvailableBalanceInPence());
}
/**
* Method used to store a failed payment to the database with the status of FAILURE. Will attempt to
* retrieve the payment amount and reference from the failure event if available.
*
* @param paymentCycle The payment cycle for the payment
* @param failureEvent The failure event.
*/
public void saveFailedPayment(PaymentCycle paymentCycle, FailureEvent failureEvent) {
try {
Payment failedPayment = createFailedPayment(paymentCycle, failureEvent);
paymentRepository.save(failedPayment);
} catch (Exception e) {
log.error("Unexpected exception caught saving a failed payment for paymentCycle: {}, cardAccountId: {}, failureEvent: {}, exception detail: {}",
paymentCycle.getId(), paymentCycle.getClaim().getCardAccountId(), failureEvent, constructExceptionDetail(e), e);
}
}
/**
* Make a payment of the given amount to the given card account (extracted from the given payment cycle).
*
* @param paymentCycle the cycle the payment is for
* @param paymentAmountInPence the amount to pay, in pence.
* @param paymentAction the payment action, used for reporting.
* @return PaymentResult containing the request and response ids.
*/
public PaymentResult makePayment(PaymentCycle paymentCycle, int paymentAmountInPence, PaymentAction paymentAction) {
String requestReference = UUID.randomUUID().toString();
String cardAccountId = paymentCycle.getClaim().getCardAccountId();
try {
DepositFundsResponse depositFundsResponse = depositFundsToCard(cardAccountId, requestReference, paymentAmountInPence);
eventAuditor.auditMakePayment(paymentCycle, paymentAmountInPence, requestReference, depositFundsResponse.getReferenceId());
reportPaymentMessageSender.sendReportPaymentMessage(paymentCycle.getClaim(), paymentCycle, paymentAction);
return PaymentResult.builder()
.requestReference(requestReference)
.responseReference(depositFundsResponse.getReferenceId())
.paymentTimestamp(LocalDateTime.now())
.build();
} catch (RuntimeException e) {
String failureMessage = String.format("Payment failed for cardAccountId %s, claim %s, paymentCycle %s, exception is: %s",
cardAccountId, paymentCycle.getClaim().getId(), paymentCycle.getId(), e.getMessage());
MakePaymentEvent failedEvent = buildFailedMakePaymentEvent(paymentCycle, paymentAmountInPence, requestReference);
throw new EventFailedException(failedEvent, e, failureMessage);
}
}
/**
* Completes a payment by updating the payment cycle with a newly created a {@link Payment} entity.
*
* @param paymentCycle the current payment cycle
* @param paymentCalculation the payment calculation that determined how much was paid
* @param paymentResult the result of the payment
*/
public void completePayment(PaymentCycle paymentCycle, PaymentCalculation paymentCalculation, PaymentResult paymentResult) {
Payment payment = createSuccessfulPayment(paymentCycle, paymentCalculation, paymentResult);
paymentCycleService.updatePaymentCycleFromCalculation(paymentCycle, paymentCalculation);
paymentRepository.save(payment);
}
private MakePaymentEvent buildFailedMakePaymentEvent(PaymentCycle paymentCycle, int paymentAmountInPence, String requestReference) {
return MakePaymentEvent.builder()
.claimId(paymentCycle.getClaim().getId())
.paymentAmountInPence(paymentAmountInPence)
.requestReference(requestReference)
.entitlementAmountInPence(paymentCycle.getTotalEntitlementAmountInPence())
.build();
}
private DepositFundsResponse depositFundsToCard(String cardAccountId, String requestReference, int amountInPence) {
DepositFundsRequest depositRequest = DepositFundsRequest.builder()
.reference(requestReference)
.amountInPence(amountInPence)
.build();
return cardClient.depositFundsToCard(cardAccountId, depositRequest);
}
}