From ad2a5197a55a13c5893ba738a055a3d03dbd0d90 Mon Sep 17 00:00:00 2001 From: Krish Date: Thu, 3 Sep 2020 18:22:56 +0100 Subject: [PATCH] PP-7077 Get refunds from Ledger - Added service method to get refund transactions for payment from Ledger. This is going to be used to calculate refundability when refunds have been expunged from connector --- .../GetRefundsForPaymentException.java | 9 +++ .../paritycheck/LedgerException.java | 24 +++++++ .../connector/paritycheck/LedgerService.java | 47 ++++++++++++-- .../RefundTransactionsForPayment.java | 23 +++++++ .../paritycheck/LedgerServiceTest.java | 55 ++++++++++++++-- .../util/TestTemplateResourceLoader.java | 1 + .../templates/ledger/refunds_for_payment.json | 62 +++++++++++++++++++ 7 files changed, 211 insertions(+), 10 deletions(-) create mode 100644 src/main/java/uk/gov/pay/connector/paritycheck/GetRefundsForPaymentException.java create mode 100644 src/main/java/uk/gov/pay/connector/paritycheck/LedgerException.java create mode 100644 src/main/java/uk/gov/pay/connector/paritycheck/RefundTransactionsForPayment.java create mode 100644 src/test/resources/templates/ledger/refunds_for_payment.json diff --git a/src/main/java/uk/gov/pay/connector/paritycheck/GetRefundsForPaymentException.java b/src/main/java/uk/gov/pay/connector/paritycheck/GetRefundsForPaymentException.java new file mode 100644 index 0000000000..3b2a2209e2 --- /dev/null +++ b/src/main/java/uk/gov/pay/connector/paritycheck/GetRefundsForPaymentException.java @@ -0,0 +1,9 @@ +package uk.gov.pay.connector.paritycheck; + +import javax.ws.rs.core.Response; + +public class GetRefundsForPaymentException extends LedgerException { + public GetRefundsForPaymentException(Response response) { + super(response); + } +} diff --git a/src/main/java/uk/gov/pay/connector/paritycheck/LedgerException.java b/src/main/java/uk/gov/pay/connector/paritycheck/LedgerException.java new file mode 100644 index 0000000000..54b2673fe4 --- /dev/null +++ b/src/main/java/uk/gov/pay/connector/paritycheck/LedgerException.java @@ -0,0 +1,24 @@ +package uk.gov.pay.connector.paritycheck; + +import javax.ws.rs.core.Response; + +public class LedgerException extends RuntimeException { + private Integer status; + + public LedgerException(Response response) { + super(response.toString()); + status = response.getStatus(); + } + + public LedgerException(Exception exception) { + super(exception); + } + + @Override + public String toString() { + return "LedgerException{" + + "status=" + status + + ", message=" + getMessage() + + '}'; + } +} diff --git a/src/main/java/uk/gov/pay/connector/paritycheck/LedgerService.java b/src/main/java/uk/gov/pay/connector/paritycheck/LedgerService.java index 5c5687d61e..f181e77375 100644 --- a/src/main/java/uk/gov/pay/connector/paritycheck/LedgerService.java +++ b/src/main/java/uk/gov/pay/connector/paritycheck/LedgerService.java @@ -1,8 +1,11 @@ package uk.gov.pay.connector.paritycheck; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import uk.gov.pay.connector.app.ConnectorConfiguration; import javax.inject.Inject; +import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Client; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -10,10 +13,15 @@ import java.util.Optional; import static java.lang.String.format; +import static net.logstash.logback.argument.StructuredArguments.kv; import static org.apache.http.HttpStatus.SC_OK; +import static uk.gov.pay.logging.LoggingKeys.GATEWAY_ACCOUNT_ID; +import static uk.gov.pay.logging.LoggingKeys.PAYMENT_EXTERNAL_ID; public class LedgerService { + private final Logger logger = LoggerFactory.getLogger(LedgerService.class); + private final Client client; private final String ledgerUrl; @@ -51,12 +59,33 @@ public Optional getTransactionForGatewayAccount(String id, Lo return getTransactionFromLedger(uri); } + public RefundTransactionsForPayment getRefundsForPayment(Long gatewayAccountId, String paymentExternalId) { + var uri = UriBuilder + .fromPath(ledgerUrl) + .path(format("/v1/transaction/%s/transaction", paymentExternalId)) + .queryParam("gateway_account_id", gatewayAccountId); + + Response response = getResponse(uri); + + if (response.getStatus() == SC_OK) { + try { + return response.readEntity(RefundTransactionsForPayment.class); + } catch (ProcessingException exception) { + logger.error("Error processing response from ledger for payment refunds: {} {}", + kv(GATEWAY_ACCOUNT_ID, gatewayAccountId), + kv(PAYMENT_EXTERNAL_ID, paymentExternalId)); + throw new LedgerException(exception); + } + } else { + logger.error("Received non-success status code for get refunds for payment from Ledger: {}, {}", + kv(GATEWAY_ACCOUNT_ID, gatewayAccountId), + kv(PAYMENT_EXTERNAL_ID, paymentExternalId)); + throw new GetRefundsForPaymentException(response); + } + } + private Optional getTransactionFromLedger(UriBuilder uri) { - Response response = client - .target(uri) - .request() - .accept(MediaType.APPLICATION_JSON) - .get(); + Response response = getResponse(uri); if (response.getStatus() == SC_OK) { return Optional.of(response.readEntity(LedgerTransaction.class)); @@ -65,4 +94,12 @@ private Optional getTransactionFromLedger(UriBuilder uri) { return Optional.empty(); } + private Response getResponse(UriBuilder uri) { + return client + .target(uri) + .request() + .accept(MediaType.APPLICATION_JSON) + .get(); + } + } diff --git a/src/main/java/uk/gov/pay/connector/paritycheck/RefundTransactionsForPayment.java b/src/main/java/uk/gov/pay/connector/paritycheck/RefundTransactionsForPayment.java new file mode 100644 index 0000000000..e520e4835e --- /dev/null +++ b/src/main/java/uk/gov/pay/connector/paritycheck/RefundTransactionsForPayment.java @@ -0,0 +1,23 @@ +package uk.gov.pay.connector.paritycheck; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import java.util.List; + +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class RefundTransactionsForPayment { + + String parentTransactionId; + List transactions; + + public String getParentTransactionId() { + return parentTransactionId; + } + + public List getTransactions() { + return transactions; + } +} diff --git a/src/test/java/uk/gov/pay/connector/paritycheck/LedgerServiceTest.java b/src/test/java/uk/gov/pay/connector/paritycheck/LedgerServiceTest.java index be0e56864e..58e2900c14 100644 --- a/src/test/java/uk/gov/pay/connector/paritycheck/LedgerServiceTest.java +++ b/src/test/java/uk/gov/pay/connector/paritycheck/LedgerServiceTest.java @@ -8,14 +8,17 @@ import org.mockito.junit.MockitoJUnitRunner; import uk.gov.pay.connector.app.ConnectorConfiguration; +import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Client; import javax.ws.rs.client.Invocation; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; +import java.util.List; import java.util.Optional; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.apache.http.HttpStatus.SC_NOT_FOUND; import static org.apache.http.HttpStatus.SC_OK; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; @@ -23,22 +26,24 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static uk.gov.pay.connector.util.TestTemplateResourceLoader.LEDGER_GET_REFUNDS_FOR_PAYMENT; import static uk.gov.pay.connector.util.TestTemplateResourceLoader.LEDGER_GET_TRANSACTION; import static uk.gov.pay.connector.util.TestTemplateResourceLoader.load; @RunWith(MockitoJUnitRunner.class) public class LedgerServiceTest { - ObjectMapper objectMapper = new ObjectMapper(); + private ObjectMapper objectMapper = new ObjectMapper(); private LedgerService ledgerService; + private Response mockResponse; @Before - public void setUp() throws JsonProcessingException { + public void setUp() { Client mockClient = mock(Client.class); ConnectorConfiguration mockConnectorConfiguration = mock(ConnectorConfiguration.class); WebTarget mockWebTarget = mock(WebTarget.class); Invocation.Builder mockBuilder = mock(Invocation.Builder.class); - Response mockResponse = mock(Response.class); + mockResponse = mock(Response.class); when(mockConnectorConfiguration.getLedgerBaseUrl()).thenReturn("http://ledgerUrl"); when(mockClient.target(any(UriBuilder.class))).thenReturn(mockWebTarget); @@ -46,13 +51,14 @@ public void setUp() throws JsonProcessingException { when(mockBuilder.accept(APPLICATION_JSON)).thenReturn(mockBuilder); when(mockBuilder.get()).thenReturn(mockResponse); - when(mockResponse.readEntity(LedgerTransaction.class)).thenReturn(objectMapper.readValue(load(LEDGER_GET_TRANSACTION), LedgerTransaction.class)); when(mockResponse.getStatus()).thenReturn(SC_OK); ledgerService = new LedgerService(mockClient, mockConnectorConfiguration); } @Test - public void getTransaction_shouldSerialiseLedgerTransaction() { + public void getTransaction_shouldSerialiseLedgerTransaction() throws JsonProcessingException { + when(mockResponse.readEntity(LedgerTransaction.class)).thenReturn(objectMapper.readValue(load(LEDGER_GET_TRANSACTION), LedgerTransaction.class)); + String externalId = "external-id"; Optional mayBeTransaction = ledgerService.getTransaction("external-id"); @@ -63,4 +69,43 @@ public void getTransaction_shouldSerialiseLedgerTransaction() { assertThat(transaction.getGatewayAccountId(), is("3")); assertThat(transaction.getExternalMetaData(), is(notNullValue())); } + + @Test + public void getRefundsFromLedgerShouldSerialiseResponseCorrectly() throws JsonProcessingException { + when(mockResponse.readEntity(RefundTransactionsForPayment.class)). + thenReturn(objectMapper.readValue(load(LEDGER_GET_REFUNDS_FOR_PAYMENT), RefundTransactionsForPayment.class)); + + String externalId = "650516the13q5jpfo435f1m1fm"; + RefundTransactionsForPayment refundTransactionsForPayment = + ledgerService.getRefundsForPayment(152L, externalId); + + assertThat(refundTransactionsForPayment.getParentTransactionId(), is(externalId)); + + List transactions = refundTransactionsForPayment.getTransactions(); + + assertThat(transactions.get(0).getTransactionId(), is("nklfm1pk9flpu91j815kp2835o")); + assertThat(transactions.get(0).getGatewayAccountId(), is("152")); + assertThat(transactions.get(0).getAmount(), is(100L)); + assertThat(transactions.get(0).getState().getStatus(), is("success")); + + assertThat(transactions.get(1).getTransactionId(), is("migtkmlt6gvm16sim5h0p7oeje")); + assertThat(transactions.get(1).getAmount(), is(110L)); + assertThat(transactions.get(1).getGatewayAccountId(), is("152")); + assertThat(transactions.get(1).getState().getStatus(), is("failed")); + } + + @Test(expected = GetRefundsForPaymentException.class) + public void getRefundsFromLedgerShouldThrowExceptionForNon2xxResponse() { + when(mockResponse.getStatus()).thenReturn(SC_NOT_FOUND); + + ledgerService.getRefundsForPayment(152L, "external-id"); + } + + @Test(expected = LedgerException.class) + public void getRefundsFromLedgerShouldThrowExceptionIfResponseCannotBeProcessed() { + when(mockResponse.readEntity(RefundTransactionsForPayment.class)). + thenThrow(ProcessingException.class); + + ledgerService.getRefundsForPayment(152L, "external-id"); + } } diff --git a/src/test/java/uk/gov/pay/connector/util/TestTemplateResourceLoader.java b/src/test/java/uk/gov/pay/connector/util/TestTemplateResourceLoader.java index c4d5b3d218..6a247e1eda 100644 --- a/src/test/java/uk/gov/pay/connector/util/TestTemplateResourceLoader.java +++ b/src/test/java/uk/gov/pay/connector/util/TestTemplateResourceLoader.java @@ -159,6 +159,7 @@ public class TestTemplateResourceLoader { public static final String SQS_ERROR_RESPONSE = TEMPLATE_BASE_NAME + "/sqs/error-response.xml"; public static final String LEDGER_GET_TRANSACTION = TEMPLATE_BASE_NAME + "/ledger/transaction.json"; + public static final String LEDGER_GET_REFUNDS_FOR_PAYMENT = TEMPLATE_BASE_NAME + "/ledger/refunds_for_payment.json"; public static String load(String location) { return fixture(location); diff --git a/src/test/resources/templates/ledger/refunds_for_payment.json b/src/test/resources/templates/ledger/refunds_for_payment.json new file mode 100644 index 0000000000..b6c95556d4 --- /dev/null +++ b/src/test/resources/templates/ledger/refunds_for_payment.json @@ -0,0 +1,62 @@ +{ + "parent_transaction_id": "650516the13q5jpfo435f1m1fm", + "transactions": [ + { + "gateway_account_id": "152", + "amount": 100, + "state": { + "finished": true, + "status": "success" + }, + "created_date": "2019-12-23T15:24:07.061Z", + "gateway_transaction_id": "ff3f12da-fa4e-4f2c-8460-fa80d20b3e97", + "refunded_by": "refunded_by_user1", + "transaction_type": "REFUND", + "payment_details": { + "description": "An example payment description", + "reference": "V58CDG66T2", + "email": "test@example.org", + "card_details": { + "cardholder_name": "test", + "card_brand": "Visa", + "last_digits_card_number": "4242", + "first_digits_card_number": "424242", + "expiry_date": "11/21", + "card_type": "credit" + }, + "transaction_type": "PAYMENT" + }, + "transaction_id": "nklfm1pk9flpu91j815kp2835o", + "parent_transaction_id": "650516the13q5jpfo435f1m1fm" + }, + { + "gateway_account_id": "152", + "amount": 110, + "state": { + "finished": true, + "status": "failed" + }, + "created_date": "2019-12-23T16:20:12.343Z", + "gateway_transaction_id": "0ecb93b8-2db6-41b1-a63d-5438ab7cf0ba", + "refunded_by": "refunded_by_user1", + "refunded_by_user_email": "test@example.org", + "transaction_type": "REFUND", + "payment_details": { + "description": "An example payment description", + "reference": "V58CDG66T2", + "email": "test@example.org", + "card_details": { + "cardholder_name": "test", + "card_brand": "Visa", + "last_digits_card_number": "4242", + "first_digits_card_number": "424242", + "expiry_date": "11/21", + "card_type": "credit" + }, + "transaction_type": "PAYMENT" + }, + "transaction_id": "migtkmlt6gvm16sim5h0p7oeje", + "parent_transaction_id": "650516the13q5jpfo435f1m1fm" + } + ] +}