package uk.gov.pay.api.it; import com.google.gson.GsonBuilder; import io.restassured.response.Response; import io.restassured.response.ValidatableResponse; import org.junit.Before; import org.junit.Test; import uk.gov.pay.api.it.fixtures.PaymentRefundJsonFixture; import uk.gov.pay.api.model.Address; import uk.gov.pay.api.model.CardDetails; import uk.gov.pay.api.model.RefundSummary; import uk.gov.pay.api.model.ledger.TransactionState; import uk.gov.pay.api.utils.PublicAuthMockClient; import uk.gov.pay.api.utils.mocks.ConnectorMockClient; import uk.gov.pay.api.utils.mocks.LedgerMockClient; import uk.gov.pay.api.utils.mocks.RefundTransactionFromLedgerFixture; import uk.gov.pay.commons.model.SupportedLanguage; import uk.gov.pay.commons.validation.DateTimeUtils; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Map; import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; import static java.lang.String.format; import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.Response.Status.ACCEPTED; import static javax.ws.rs.core.Response.Status.NOT_FOUND; import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED; import static org.hamcrest.core.Is.is; import static uk.gov.pay.api.utils.Urls.paymentLocationFor; import static uk.gov.pay.api.utils.mocks.ChargeResponseFromConnector.ChargeResponseFromConnectorBuilder.aCreateOrGetChargeResponseFromConnector; import static uk.gov.pay.api.utils.mocks.RefundTransactionFromLedgerFixture.RefundTransactionFromLedgerBuilder.aRefundTransactionFromLedgerFixture; import static uk.gov.pay.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; public class PaymentRefundsResourceIT extends PaymentResourceITestBase { private static final int AMOUNT = 1000; private static final int REFUND_AMOUNT_AVAILABLE = 9000; private static final String CHARGE_ID = "ch_ab2341da231434l"; private static final String REFUND_ID = "111999"; private static final ZonedDateTime TIMESTAMP = DateTimeUtils.toUTCZonedDateTime("2016-01-01T12:00:00Z").get(); private static final String CREATED_DATE = ISO_INSTANT_MILLISECOND_PRECISION.format(TIMESTAMP); private static final Address BILLING_ADDRESS = new Address("line1", "line2", "NR2 5 6EG", "city", "UK"); private static final CardDetails CARD_DETAILS = new CardDetails("1234", "123456", "Mr. Payment", "12/19", BILLING_ADDRESS, "Visa", null); private ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); private LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); @Before public void setUp() { publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); } @Test public void getRefundByIdThroughConnector_shouldGetValidResponse() { connectorMockClient.respondWithGetRefundById(GATEWAY_ACCOUNT_ID, CHARGE_ID, REFUND_ID, AMOUNT, REFUND_AMOUNT_AVAILABLE, "available", CREATED_DATE); assertSingleRefund(getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID)); } @Test public void getRefundByIdThroughLedger_shouldGetValidResponse() { ledgerMockClient.respondWithRefund(REFUND_ID, aRefundTransactionFromLedgerFixture() .withAmount((long) AMOUNT) .withState(new TransactionState("available", false)) .withParentTransactionId(CHARGE_ID) .withTransactionId(REFUND_ID) .withCreatedDate(CREATED_DATE) .build()); assertSingleRefund(getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID, "ledger-only")); } private void assertSingleRefund(ValidatableResponse paymentRefundByIdResponse) { paymentRefundByIdResponse .statusCode(200) .contentType(JSON) .body("refund_id", is(REFUND_ID)) .body("amount", is(AMOUNT)) .body("status", is("available")) .body("created_date", is(CREATED_DATE)) .body("_links.self.href", is(paymentRefundLocationFor(CHARGE_ID, REFUND_ID))) .body("_links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))); } @Test public void getRefundById_shouldGetNonAuthorized_whenPublicAuthRespondsUnauthorised() { publicAuthMockClient.respondUnauthorised(); getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID) .statusCode(401); } @Test public void getRefundByIdThroughConnector_shouldReturnNotFound_whenRefundDoesNotExist() { connectorMockClient.respondRefundNotFound(GATEWAY_ACCOUNT_ID, CHARGE_ID, "unknown-refund-id"); getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID) .statusCode(404) .contentType(JSON) .body("code", is("P0700")) .body("description", is("Not found")); } @Test public void getRefundByIdThroughLedger_shouldReturnNotFound_whenRefundDoesNotExist() { ledgerMockClient.respondRefundNotFound("unknown-refund-id"); getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID, "ledger-only") .statusCode(404) .contentType(JSON) .body("code", is("P0700")) .body("description", is("Not found")); } @Test public void getRefundById_returns500_whenConnectorRespondsWithResponseOtherThan200Or404() { connectorMockClient.respondRefundWithError(GATEWAY_ACCOUNT_ID, CHARGE_ID, REFUND_ID); getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID, CONNECTOR_ONLY_STRATEGY) .statusCode(500) .contentType(JSON) .body("code", is("P0798")) .body("description", is("Downstream system error")); } @Test public void getRefundById_returns500_whenLedgerRespondsWithResponseOtherThan200Or404() { ledgerMockClient.respondRefundWithError(REFUND_ID); getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID, "ledger-only") .statusCode(500) .contentType(JSON) .body("code", is("P0798")) .body("description", is("Downstream system error")); } @Test public void getRefundsThroughConnector_shouldGetValidResponse() { PaymentRefundJsonFixture refund1 = new PaymentRefundJsonFixture(100L, CREATED_DATE, "100", "available", new ArrayList<>()); PaymentRefundJsonFixture refund2 = new PaymentRefundJsonFixture(300L, CREATED_DATE, "300", "pending", new ArrayList<>()); connectorMockClient.respondWithGetAllRefunds(GATEWAY_ACCOUNT_ID, CHARGE_ID, refund1, refund2); assertRefundsResponse(getPaymentRefundsResponse(CHARGE_ID, CONNECTOR_ONLY_STRATEGY)); } @Test public void getRefundsThroughLedger_shouldGetValidResponse() { RefundTransactionFromLedgerFixture refund1 = aRefundTransactionFromLedgerFixture() .withAmount(100L) .withCreatedDate(CREATED_DATE) .withTransactionId("100") .withState(new TransactionState("available", false)) .build(); RefundTransactionFromLedgerFixture refund2 = aRefundTransactionFromLedgerFixture() .withAmount(300L) .withCreatedDate(CREATED_DATE) .withTransactionId("300") .withState(new TransactionState("pending", false)) .build(); ledgerMockClient.respondWithGetAllRefunds(CHARGE_ID, refund1, refund2); assertRefundsResponse(getPaymentRefundsResponse(CHARGE_ID, "ledger-only")); } private void assertRefundsResponse(ValidatableResponse paymentRefundsResponse) { paymentRefundsResponse .statusCode(200) .contentType(JSON) .body("payment_id", is(CHARGE_ID)) .body("_links.self.href", is(paymentRefundsLocationFor(CHARGE_ID))) .body("_links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) .body("_embedded.refunds.size()", is(2)) .body("_embedded.refunds[0].refund_id", is("100")) .body("_embedded.refunds[0].created_date", is(CREATED_DATE)) .body("_embedded.refunds[0].amount", is(100)) .body("_embedded.refunds[0].status", is("available")) .body("_embedded.refunds[0]._links.size()", is(2)) .body("_embedded.refunds[0]._links.self.href", is(paymentRefundLocationFor(CHARGE_ID, "100"))) .body("_embedded.refunds[0]._links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) .body("_embedded.refunds[1].refund_id", is("300")) .body("_embedded.refunds[1].created_date", is(CREATED_DATE)) .body("_embedded.refunds[1].amount", is(300)) .body("_embedded.refunds[1].status", is("pending")) .body("_embedded.refunds[1]._links.size()", is(2)) .body("_embedded.refunds[1]._links.self.href", is(paymentRefundLocationFor(CHARGE_ID, "300"))) .body("_embedded.refunds[1]._links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))); } @Test public void getRefunds_shouldGetValidResponse_whenListReturnedIsEmpty() { connectorMockClient.respondWithGetAllRefunds(GATEWAY_ACCOUNT_ID, CHARGE_ID); assertEmptyRefundsResponse(getPaymentRefundsResponse(CHARGE_ID, CONNECTOR_ONLY_STRATEGY)); } @Test public void getRefundsThroughLedger_shouldGetValidResponse_whenListReturnedIsEmpty() { ledgerMockClient.respondWithGetAllRefunds(CHARGE_ID); assertEmptyRefundsResponse(getPaymentRefundsResponse(CHARGE_ID, "ledger-only")); } private void assertEmptyRefundsResponse(ValidatableResponse paymentRefundsResponse) { paymentRefundsResponse .statusCode(200) .contentType(JSON) .body("payment_id", is(CHARGE_ID)) .body("_links.self.href", is(paymentRefundsLocationFor(CHARGE_ID))) .body("_links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) .body("_embedded.refunds.size()", is(0)); } @Test public void getRefunds_shouldGetNonAuthorized_whenPublicAuthRespondsUnauthorised() { publicAuthMockClient.respondUnauthorised(); getPaymentRefundsResponse(CHARGE_ID) .statusCode(401); } @Test public void createRefund_shouldGetAcceptedResponse() { String payload = new GsonBuilder().create().toJson(Map.of("amount", AMOUNT, "refund_amount_available", REFUND_AMOUNT_AVAILABLE)); postRefundRequest(payload); } @Test public void createRefundWithNoRefundAmountAvailable_shouldGetAcceptedResponse() { String payload = new GsonBuilder().create().toJson(Map.of("amount", AMOUNT)); connectorMockClient.respondWithChargeFound(null, GATEWAY_ACCOUNT_ID, aCreateOrGetChargeResponseFromConnector() .withAmount(AMOUNT) .withChargeId(CHARGE_ID) .withLanguage(SupportedLanguage.ENGLISH) .withDelayedCapture(false) .withRefundSummary(new RefundSummary("available", 9000, 1000)) .withCardDetails(CARD_DETAILS) .withGatewayTransactionId("gatewayTransactionId") .build()); postRefundRequest(payload); } @Test public void createRefundWhenChargeNotFound_shouldReturn404() { String payload = new GsonBuilder().create().toJson(Map.of("amount", AMOUNT)); connectorMockClient.respondChargeNotFound(CHARGE_ID, GATEWAY_ACCOUNT_ID, "Not found"); postRefunds(payload) .then() .statusCode(NOT_FOUND.getStatusCode()); } @Test public void createRefundWhenRefundAmountAvailableMismatch_shouldReturn412Response() { String payload = new GsonBuilder().create().toJson( Map.of("amount", AMOUNT, "refund_amount_available", REFUND_AMOUNT_AVAILABLE)); String errorMessage = new GsonBuilder().create().toJson( Map.of("code", "P0604", "description", "Refund amount available mismatch.")); connectorMockClient.respondPreconditionFailed_whenCreateRefund(GATEWAY_ACCOUNT_ID, errorMessage, CHARGE_ID); postRefunds(payload) .then() .statusCode(PRECONDITION_FAILED.getStatusCode()) .contentType(JSON) .body("code", is("P0604")) .body("description", is("Refund amount available mismatch.")); } @Test public void createRefund_shouldGetNonAuthorized_whenPublicAuthRespondsUnauthorised() { publicAuthMockClient.respondUnauthorised(); postRefunds("{\"amount\": 1000}") .then() .statusCode(401); } private void postRefundRequest(String payload) { String refundStatus = "available"; connectorMockClient.respondAccepted_whenCreateARefund(AMOUNT, REFUND_AMOUNT_AVAILABLE, GATEWAY_ACCOUNT_ID, CHARGE_ID, REFUND_ID, refundStatus, CREATED_DATE); postRefunds(payload) .then() .statusCode(ACCEPTED.getStatusCode()) .contentType(JSON) .body("refund_id", is(REFUND_ID)) .body("amount", is(AMOUNT)) .body("status", is(refundStatus)) .body("created_date", is(CREATED_DATE)) .body("_links.self.href", is(paymentRefundLocationFor(CHARGE_ID, REFUND_ID))) .body("_links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))); } private Response postRefunds(String payload) { return given().port(app.getLocalPort()) .header(AUTHORIZATION, "Bearer " + API_KEY) .header(CONTENT_TYPE, APPLICATION_JSON) .body(payload) .post(format("/v1/payments/%s/refunds", CHARGE_ID)); } private ValidatableResponse getPaymentRefundByIdResponse(String paymentId, String refundId) { String defaultConnectorStrategy = ""; return getPaymentRefundByIdResponse(paymentId, refundId, defaultConnectorStrategy); } private ValidatableResponse getPaymentRefundByIdResponse(String paymentId, String refundId, String strategy) { return given().port(app.getLocalPort()) .header("X-Ledger", strategy) .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) .get(format("/v1/payments/%s/refunds/%s", paymentId, refundId)) .then(); } private ValidatableResponse getPaymentRefundsResponse(String paymentId) { return getPaymentRefundsResponse(paymentId, "default"); } private ValidatableResponse getPaymentRefundsResponse(String paymentId, String strategy) { return given().port(app.getLocalPort()) .header("X-Ledger", strategy) .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) .get(format("/v1/payments/%s/refunds", paymentId)) .then(); } }