Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NFDIV-4554] Endpoint to consume payment callbacks #4243

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a3ce088
Initial callback controller example
adamg-hmcts Nov 28, 2024
c508e9e
Update sonar exclusions
adamg-hmcts Nov 28, 2024
7c86198
Update test
adamg-hmcts Nov 28, 2024
c7b9af1
Merge branch 'master' into nfdiv-3940-payment-callback-poc
adamg-hmcts Nov 28, 2024
9f61335
Update case API url for preview
adamg-hmcts Nov 28, 2024
0417e78
Merge branch 'master' into nfdiv-3940-payment-callback-poc
adamg-hmcts Nov 29, 2024
e9a31ff
Change log level
adamg-hmcts Nov 29, 2024
6df002c
Update interceptor path exclusions
adamg-hmcts Nov 29, 2024
a6f78e0
Merge branch 'master' into nfdiv-3940-payment-callback-poc
adamg-hmcts Dec 2, 2024
9809b39
Reset logging and MVC config
adamg-hmcts Dec 2, 2024
b3e1cae
Merge master and resolve conflicts by accepting updates to payments code
adamg-hmcts Dec 19, 2024
b144328
Resolve more merge conflicts
adamg-hmcts Dec 19, 2024
9f9a95c
Define callback url in payment setup service
adamg-hmcts Dec 19, 2024
02984fc
Create service to process payment callbacks
adamg-hmcts Dec 24, 2024
fb92c22
Checkstyle corrections
adamg-hmcts Dec 24, 2024
1f628ae
Update payment setup tests
adamg-hmcts Dec 24, 2024
4136ab5
Add unit tests
adamg-hmcts Dec 24, 2024
ce74496
Prevent processing of PBA callbacks
adamg-hmcts Dec 30, 2024
ab0839b
Add simple integration test for payment callbacks
adamg-hmcts Dec 30, 2024
5d55816
Checkstyle correction
adamg-hmcts Dec 30, 2024
6c2f9e8
Checkstyle corrections
adamg-hmcts Dec 30, 2024
1c4fe01
Merge branch 'master' into nfdiv-4554-endpoint-to-consume-payment-cal…
adamg-hmcts Dec 30, 2024
234d9a1
Refactor and simplify tests
adamg-hmcts Dec 31, 2024
1f82c4a
Correct type reference in Integration test
adamg-hmcts Dec 31, 2024
20dc974
Merge branch 'master' into nfdiv-4554-endpoint-to-consume-payment-cal…
adamg-hmcts Jan 10, 2025
ea5c8f2
Update callback DTOs
adamg-hmcts Jan 13, 2025
22ded06
Remove file that was committed by mistake
adamg-hmcts Jan 13, 2025
c1cb696
Update tests
adamg-hmcts Jan 13, 2025
0a64a8f
Update integration test
adamg-hmcts Jan 13, 2025
f5ad70c
Merge branch 'master' into nfdiv-4554-endpoint-to-consume-payment-cal…
adamg-hmcts Jan 14, 2025
3d7337f
Use JSON value to correctly deserialize DTO
adamg-hmcts Jan 14, 2025
10ff117
Remove file (no longer needed)
adamg-hmcts Jan 14, 2025
1604de9
Merge branch 'master' into nfdiv-4554-endpoint-to-consume-payment-cal…
adamg-hmcts Jan 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions charts/nfdiv-case-api/values.preview.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ java:
environment:
CHANGE_ID: ${CHANGE_ID}
CITIZEN_UPDATE_CASE_STATE_ENABLED: true
CASE_API_URL: https://nfdiv-case-api-pr-${CHANGE_ID}.preview.platform.hmcts.net
keyVaults:
nfdiv:
secrets:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package uk.gov.hmcts.divorce.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import uk.gov.hmcts.divorce.common.config.WebMvcConfig;
import uk.gov.hmcts.divorce.common.config.interceptors.RequestInterceptor;
import uk.gov.hmcts.divorce.notification.NotificationService;
import uk.gov.hmcts.divorce.payment.PaymentCallbackService;
import uk.gov.hmcts.divorce.payment.model.PaymentCallbackDto;
import uk.gov.hmcts.divorce.payment.model.ServiceRequestStatus;
import uk.gov.hmcts.reform.authorisation.generators.AuthTokenGenerator;

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static uk.gov.hmcts.divorce.controller.PaymentCallbackController.PAYMENT_UPDATE_PATH;
import static uk.gov.hmcts.divorce.testutil.TestConstants.AUTHORIZATION;
import static uk.gov.hmcts.divorce.testutil.TestConstants.SERVICE_AUTHORIZATION;
import static uk.gov.hmcts.divorce.testutil.TestConstants.TEST_AUTHORIZATION_TOKEN;
import static uk.gov.hmcts.divorce.testutil.TestConstants.TEST_CASE_ID;
import static uk.gov.hmcts.divorce.testutil.TestConstants.TEST_SERVICE_AUTH_TOKEN;

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class PaymentCallbackControllerIT {
@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@MockBean
private RequestInterceptor requestInterceptor;

@MockBean
private NotificationService notificationService;

@MockBean
private WebMvcConfig webMvcConfig;

@MockBean
private AuthTokenGenerator authTokenGenerator;

@MockBean
private PaymentCallbackService paymentCallbackService;

@Test
public void givenValidServiceAuthTokenThenProcessesPaymentCallback() throws Exception {
PaymentCallbackDto paymentCallback = cardPaymentCallback();

mockMvc.perform(put(PAYMENT_UPDATE_PATH)
.contentType(APPLICATION_JSON)
.header(SERVICE_AUTHORIZATION, TEST_SERVICE_AUTH_TOKEN)
.header(AUTHORIZATION, TEST_AUTHORIZATION_TOKEN)
.content(objectMapper.writeValueAsString(paymentCallback))
.accept(APPLICATION_JSON))
.andExpect(status().isOk());

verify(paymentCallbackService).handleCallback(paymentCallback);
}

@Test
public void givenMissingServiceAuthTokenThenDoesNotProcessCallback() throws Exception {
mockMvc.perform(put(PAYMENT_UPDATE_PATH)
.contentType(APPLICATION_JSON)
.header(AUTHORIZATION, TEST_AUTHORIZATION_TOKEN)
.content(objectMapper.writeValueAsString(cardPaymentCallback()))
.accept(APPLICATION_JSON))
.andExpect(status().is4xxClientError());

verifyNoInteractions(paymentCallbackService);
}

private PaymentCallbackDto cardPaymentCallback() {
return PaymentCallbackDto.builder()
.serviceRequestStatus(ServiceRequestStatus.PAID)
.ccdCaseNumber(TEST_CASE_ID.toString())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ public AboutToStartOrSubmitResponse<CaseData, State> aboutToSubmit(final CaseDet
private void prepareServiceRequestForApplicationPayment(CaseData data, long caseId) {
Application application = data.getApplication();

String serviceRequest = paymentSetupService.createApplicationFeeServiceRequest(
data, caseId, data.getCitizenPaymentCallbackUrl()
);
String serviceRequest = paymentSetupService.createApplicationFeeServiceRequest(data, caseId);

application.setApplicationFeeServiceRequestReference(serviceRequest);
}
Expand All @@ -74,7 +72,7 @@ private void prepareServiceRequestForFinalOrderPayment(CaseData data, long caseI
FinalOrder finalOrder = data.getFinalOrder();

String serviceRequest = paymentSetupService.createFinalOrderFeeServiceRequest(
data, caseId, data.getCitizenPaymentCallbackUrl(), finalOrder.getApplicant2FinalOrderFeeOrderSummary()
data, caseId, finalOrder.getApplicant2FinalOrderFeeOrderSummary()
);

finalOrder.setApplicant2FinalOrderFeeServiceRequestReference(serviceRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,7 @@ public AboutToStartOrSubmitResponse<CaseData, State> aboutToSubmit(CaseDetails<C
data = submittedDetails.getData();
state = submittedDetails.getState();
} else {
prepareCaseDataForApplicationPayment(
details.getData(), details.getId(), details.getData().getCitizenPaymentCallbackUrl()
);
prepareCaseDataForApplicationPayment(details.getData(), details.getId());

state = AwaitingPayment;
}
Expand All @@ -102,13 +100,13 @@ public AboutToStartOrSubmitResponse<CaseData, State> aboutToSubmit(CaseDetails<C
.build();
}

public void prepareCaseDataForApplicationPayment(CaseData data, long caseId, String redirectUrl) {
public void prepareCaseDataForApplicationPayment(CaseData data, long caseId) {
Application application = data.getApplication();

OrderSummary orderSummary = paymentSetupService.createApplicationFeeOrderSummary(data, caseId);
application.setApplicationFeeOrderSummary(orderSummary);

String serviceRequest = paymentSetupService.createApplicationFeeServiceRequest(data, caseId, redirectUrl);
String serviceRequest = paymentSetupService.createApplicationFeeServiceRequest(data, caseId);
application.setApplicationFeeServiceRequestReference(serviceRequest);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ private CaseDetails<CaseData, State> setOrderSummaryAndAwaitingPaymentState(
);

String serviceRequest = paymentSetupService.createFinalOrderFeeServiceRequest(
data, caseId, data.getCitizenPaymentCallbackUrl(), orderSummary
data, caseId, orderSummary
);
finalOrder.setApplicant2FinalOrderFeeServiceRequestReference(serviceRequest);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package uk.gov.hmcts.divorce.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import uk.gov.hmcts.divorce.payment.PaymentCallbackService;
import uk.gov.hmcts.divorce.payment.model.PaymentCallbackDto;

import static uk.gov.hmcts.divorce.common.config.ControllerConstants.SERVICE_AUTHORIZATION;

@RestController
@Slf4j
@RequiredArgsConstructor
public class PaymentCallbackController {

public static final String PAYMENT_UPDATE_PATH = "/payment-update";

private final PaymentCallbackService paymentCallbackService;

@Operation(summary = "Update payment", description = "Update payment")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Updated payment successfully"),
@ApiResponse(responseCode = "400", description = "Bad Request"),
@ApiResponse(responseCode = "401", description = "Provided S2S token is missing or invalid"),
@ApiResponse(responseCode = "403", description = "Calling service is not authorised to use the endpoint"),
@ApiResponse(responseCode = "500", description = "Internal Server Error")})
@PutMapping(
path = PAYMENT_UPDATE_PATH,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
) public ResponseEntity<HttpStatus> updatePayment(
@RequestHeader(value = SERVICE_AUTHORIZATION) String s2sAuthToken,
@RequestBody PaymentCallbackDto paymentCallbackDto
) {
log.info("Payment Callback Received For Case: {}", paymentCallbackDto.getCcdCaseNumber());

paymentCallbackService.handleCallback(paymentCallbackDto);

return new ResponseEntity<>(HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package uk.gov.hmcts.divorce.payment;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import uk.gov.hmcts.divorce.common.service.task.UpdateSuccessfulPaymentStatus;
import uk.gov.hmcts.divorce.divorcecase.model.State;
import uk.gov.hmcts.divorce.idam.IdamService;
import uk.gov.hmcts.divorce.idam.User;
import uk.gov.hmcts.divorce.payment.model.OnlinePaymentMethod;
import uk.gov.hmcts.divorce.payment.model.PaymentCallbackDto;
import uk.gov.hmcts.divorce.payment.model.ServiceRequestStatus;
import uk.gov.hmcts.divorce.systemupdate.service.CcdUpdateService;
import uk.gov.hmcts.reform.authorisation.generators.AuthTokenGenerator;
import uk.gov.hmcts.reform.ccd.client.CoreCaseDataApi;
import uk.gov.hmcts.reform.ccd.client.model.CaseDetails;

import static uk.gov.hmcts.divorce.citizen.event.CitizenPaymentMade.CITIZEN_PAYMENT_MADE;
import static uk.gov.hmcts.divorce.citizen.event.RespondentFinalOrderPaymentMade.RESPONDENT_FINAL_ORDER_PAYMENT_MADE;

@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentCallbackService {

private final CcdUpdateService ccdUpdateService;

private final IdamService idamService;

private final AuthTokenGenerator authTokenGenerator;

private final CoreCaseDataApi coreCaseDataApi;

private final UpdateSuccessfulPaymentStatus updateSuccessfulPaymentStatus;

private static final String LOG_NOT_PROCESSING_CALLBACK = """
Not processing callback for payment {}, case id: {}, status: {}, payment method: {}
""";

public void handleCallback(PaymentCallbackDto paymentCallback) {
final String caseRef = paymentCallback.getCcdCaseNumber();
final String paymentRef = paymentCallback.getPayment().getPaymentReference();
final ServiceRequestStatus serviceRequestStatus = paymentCallback.getServiceRequestStatus();
final OnlinePaymentMethod paymentMethod = paymentCallback.getPayment().getPaymentMethod();

if (serviceRequestNotPaid(serviceRequestStatus) || isSolicitorPbaPayment(paymentMethod)) {
log.info(LOG_NOT_PROCESSING_CALLBACK, paymentRef, caseRef, serviceRequestStatus, paymentMethod);
return;
}

final User systemUpdateUser = idamService.retrieveSystemUpdateUserDetails();
final String serviceAuthorization = authTokenGenerator.generate();
final CaseDetails details = coreCaseDataApi.getCase(systemUpdateUser.getAuthToken(), serviceAuthorization, caseRef);
final State state = State.valueOf(details.getState());
final String paymentMadeEvent = paymentMadeEvent(state);

if (paymentMadeEvent == null) {
log.info(LOG_NOT_PROCESSING_CALLBACK, paymentRef, caseRef, serviceRequestStatus, paymentMethod);
return;
}

ccdUpdateService.submitEventWithRetry(
caseRef,
paymentMadeEvent,
updateSuccessfulPaymentStatus,
systemUpdateUser,
serviceAuthorization
);
}

private boolean serviceRequestNotPaid(ServiceRequestStatus serviceRequestStatus) {
return !ServiceRequestStatus.PAID.equals(serviceRequestStatus);
}

private boolean isSolicitorPbaPayment(OnlinePaymentMethod paymentMethod) {
return OnlinePaymentMethod.PAYMENT_BY_ACCOUNT.equals(paymentMethod);
}

private String paymentMadeEvent(State state) {
return switch (state) {
case AwaitingPayment -> CITIZEN_PAYMENT_MADE;
case AwaitingFinalOrderPayment -> RESPONDENT_FINAL_ORDER_PAYMENT_MADE;
default -> null;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import uk.gov.hmcts.ccd.sdk.type.OrderSummary;
import uk.gov.hmcts.divorce.divorcecase.model.CaseData;

import static uk.gov.hmcts.divorce.controller.PaymentCallbackController.PAYMENT_UPDATE_PATH;
import static uk.gov.hmcts.divorce.payment.PaymentService.EVENT_GENERAL;
import static uk.gov.hmcts.divorce.payment.PaymentService.EVENT_ISSUE;
import static uk.gov.hmcts.divorce.payment.PaymentService.KEYWORD_DIVORCE;
Expand All @@ -20,15 +21,18 @@ public class PaymentSetupService {

private final PaymentService paymentService;

public String createApplicationFeeServiceRequest(CaseData data, long caseId, String redirectUrl) {
public static final String PAYMENT_CALLBACK_URL =
System.getenv().getOrDefault("CASE_API_URL", "http://localhost:4013") + PAYMENT_UPDATE_PATH;

public String createApplicationFeeServiceRequest(CaseData data, long caseId) {
if (data.getApplication() != null && data.getApplication().getApplicationFeeServiceRequestReference() != null) {
return data.getApplication().getApplicationFeeServiceRequestReference();
}

log.info("Application fee service request not found for case id: {}, creating service request", caseId);

return paymentService.createServiceRequestReference(
redirectUrl,
PAYMENT_CALLBACK_URL,
caseId,
data.getApplicant1().getFullName(),
data.getApplication().getApplicationFeeOrderSummary()
Expand All @@ -45,15 +49,15 @@ public OrderSummary createApplicationFeeOrderSummary(CaseData data, long caseId)
return paymentService.getOrderSummaryByServiceEvent(SERVICE_DIVORCE, EVENT_ISSUE, KEYWORD_DIVORCE);
}

public String createFinalOrderFeeServiceRequest(CaseData data, long caseId, String redirectUrl, OrderSummary orderSummary) {
public String createFinalOrderFeeServiceRequest(CaseData data, long caseId, OrderSummary orderSummary) {
if (data.getFinalOrder() != null && data.getFinalOrder().getApplicant2FinalOrderFeeServiceRequestReference() != null) {
return data.getFinalOrder().getApplicant2FinalOrderFeeServiceRequestReference();
}

log.info("Final order fee service request not found for case id: {}, creating service request", caseId);

return paymentService.createServiceRequestReference(
redirectUrl,
PAYMENT_CALLBACK_URL,
caseId,
data.getApplicant2().getFullName(),
orderSummary
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package uk.gov.hmcts.divorce.payment.model;

import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import uk.gov.hmcts.ccd.sdk.api.HasLabel;


@Getter
@RequiredArgsConstructor
public enum OnlinePaymentMethod implements HasLabel {
CARD("card"),
PAYMENT_BY_ACCOUNT("payment by account");

@JsonValue
private final String label;
}
Loading
Loading