diff --git a/src/main/java/uk/gov/pay/connector/charge/dao/ChargeDao.java b/src/main/java/uk/gov/pay/connector/charge/dao/ChargeDao.java index 8ac717c190..598e7d22cf 100644 --- a/src/main/java/uk/gov/pay/connector/charge/dao/ChargeDao.java +++ b/src/main/java/uk/gov/pay/connector/charge/dao/ChargeDao.java @@ -213,7 +213,14 @@ public Optional findChargeToExpunge(int minimumAgeOfChargeInDays, } public void expungeCharge(Long id) { - ChargeEntity chargeEntity = entityManager.get().find(ChargeEntity.class, id); - entityManager.get().remove(chargeEntity); + entityManager.get() + .createNativeQuery("delete from charge_events where charge_id = ?1") + .setParameter(1, id) + .executeUpdate(); + + entityManager.get() + .createNativeQuery("delete from charges where id = ?1") + .setParameter(1, id) + .executeUpdate(); } } diff --git a/src/main/java/uk/gov/pay/connector/expunge/service/ChargeExpungeService.java b/src/main/java/uk/gov/pay/connector/expunge/service/ChargeExpungeService.java index 401d79bb63..b0d35d1804 100644 --- a/src/main/java/uk/gov/pay/connector/expunge/service/ChargeExpungeService.java +++ b/src/main/java/uk/gov/pay/connector/expunge/service/ChargeExpungeService.java @@ -1,5 +1,6 @@ package uk.gov.pay.connector.expunge.service; +import com.google.inject.persist.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.gov.pay.connector.app.ConnectorConfiguration; @@ -21,7 +22,7 @@ public class ChargeExpungeService { private final ChargeDao chargeDao; private final ExpungeConfig expungeConfig; private final ParityCheckService parityCheckService; - + @Inject public ChargeExpungeService(ChargeDao chargeDao, ConnectorConfiguration connectorConfiguration, ParityCheckService parityCheckService) { @@ -30,6 +31,10 @@ public ChargeExpungeService(ChargeDao chargeDao, ConnectorConfiguration connecto this.parityCheckService = parityCheckService; } + private static boolean inTerminalState(ChargeEntity chargeEntity) { + return ChargeStatus.fromString(chargeEntity.getStatus()).isExpungeable(); + } + public void expunge(Integer noOfChargesToExpungeQueryParam) { if (!expungeConfig.isExpungeChargesEnabled()) { logger.info("Charge expunging feature is disabled. No charges have been expunged"); @@ -54,12 +59,12 @@ private int getNumberOfChargesToExpunge(Integer noOfChargesToExpungeQueryParam) private void parityCheckAndExpungeIfMet(ChargeEntity chargeEntity) { boolean hasChargeBeenParityCheckedBefore = chargeEntity.getParityCheckDate() != null; - + if (!inTerminalState(chargeEntity)) { logger.info("Charge not expunged because it is not in a terminal state {}", kv(PAYMENT_EXTERNAL_ID, chargeEntity.getExternalId())); } else if (parityCheckService.parityCheckChargeForExpunger(chargeEntity)) { - chargeDao.expungeCharge(chargeEntity.getId()); + expungeCharge(chargeEntity); logger.info("Charge expunged from connector {}", kv(PAYMENT_EXTERNAL_ID, chargeEntity.getExternalId())); } else { @@ -73,8 +78,9 @@ private void parityCheckAndExpungeIfMet(ChargeEntity chargeEntity) { } } - private static boolean inTerminalState(ChargeEntity chargeEntity) { - return ChargeStatus.fromString(chargeEntity.getStatus()).isExpungeable(); + @Transactional + public void expungeCharge(ChargeEntity chargeEntity) { + chargeDao.expungeCharge(chargeEntity.getId()); } - + } diff --git a/src/main/java/uk/gov/pay/connector/tasks/ParityCheckService.java b/src/main/java/uk/gov/pay/connector/tasks/ParityCheckService.java index 00170d70c9..4219f15b1a 100644 --- a/src/main/java/uk/gov/pay/connector/tasks/ParityCheckService.java +++ b/src/main/java/uk/gov/pay/connector/tasks/ParityCheckService.java @@ -7,12 +7,17 @@ import org.slf4j.MDC; import uk.gov.pay.connector.charge.model.AddressEntity; import uk.gov.pay.connector.charge.model.CardDetailsEntity; +import uk.gov.pay.connector.charge.model.ChargeResponse; import uk.gov.pay.connector.charge.model.FirstDigitsCardNumber; import uk.gov.pay.connector.charge.model.LastDigitsCardNumber; +import uk.gov.pay.connector.charge.model.domain.Charge; import uk.gov.pay.connector.charge.model.domain.ChargeEntity; import uk.gov.pay.connector.charge.model.domain.ChargeStatus; import uk.gov.pay.connector.charge.model.domain.ParityCheckStatus; import uk.gov.pay.connector.charge.service.ChargeService; +import uk.gov.pay.connector.chargeevent.model.domain.ChargeEventEntity; +import uk.gov.pay.connector.common.model.api.ExternalChargeRefundAvailability; +import uk.gov.pay.connector.gateway.util.DefaultExternalRefundAvailabilityCalculator; import uk.gov.pay.connector.gatewayaccount.model.GatewayAccountEntity; import uk.gov.pay.connector.paritycheck.Address; import uk.gov.pay.connector.paritycheck.CardDetails; @@ -20,21 +25,27 @@ import uk.gov.pay.connector.paritycheck.LedgerTransaction; import uk.gov.pay.connector.refund.dao.RefundDao; import uk.gov.pay.connector.refund.model.domain.RefundEntity; +import uk.gov.pay.connector.util.DateTimeUtils; import uk.gov.pay.connector.wallets.WalletType; +import java.time.ZonedDateTime; import java.util.List; import java.util.Objects; import java.util.Optional; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; -import static java.util.Optional.of; import static java.util.Optional.ofNullable; import static net.logstash.logback.argument.StructuredArguments.kv; import static uk.gov.pay.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; +import static uk.gov.pay.connector.charge.model.domain.ChargeStatus.CAPTURED; +import static uk.gov.pay.connector.charge.model.domain.ChargeStatus.CAPTURE_SUBMITTED; +import static uk.gov.pay.connector.charge.model.domain.ChargeStatus.CREATED; +import static uk.gov.pay.connector.charge.model.domain.ChargeStatus.PAYMENT_NOTIFICATION_CREATED; import static uk.gov.pay.connector.charge.model.domain.ParityCheckStatus.DATA_MISMATCH; import static uk.gov.pay.connector.charge.model.domain.ParityCheckStatus.EXISTS_IN_LEDGER; import static uk.gov.pay.connector.charge.model.domain.ParityCheckStatus.MISSING_IN_LEDGER; +import static uk.gov.pay.connector.charge.util.CorporateCardSurchargeCalculator.getTotalAmountFor; import static uk.gov.pay.logging.LoggingKeys.PAYMENT_EXTERNAL_ID; public class ParityCheckService { @@ -130,6 +141,8 @@ private ParityCheckStatus checkParity(ChargeEntity chargeEntity, LedgerTransacti fieldsMatch = fieldsMatch && matchCardDetails(chargeEntity.getCardDetails(), transaction.getCardDetails()); fieldsMatch = fieldsMatch && matchGatewayAccountFields(chargeEntity.getGatewayAccount(), transaction); fieldsMatch = fieldsMatch && matchFeatureSpecificFields(chargeEntity, transaction); + fieldsMatch = fieldsMatch && matchCaptureFields(chargeEntity, transaction); + fieldsMatch = fieldsMatch && matchRefundSummary(chargeEntity, transaction); if (fieldsMatch) { parityCheckStatus = EXISTS_IN_LEDGER; @@ -151,7 +164,8 @@ private boolean matchCommonFields(ChargeEntity chargeEntity, LedgerTransaction t fieldsMatch = fieldsMatch && isEquals(chargeEntity.getReturnUrl(), transaction.getReturnUrl(), "return_url"); fieldsMatch = fieldsMatch && isEquals(chargeEntity.getGatewayTransactionId(), transaction.getGatewayTransactionId(), "gateway_transaction_id"); fieldsMatch = fieldsMatch && isEquals( - ISO_INSTANT_MILLISECOND_PRECISION.format(chargeEntity.getCreatedDate()), + getChargeEventDate(chargeEntity, List.of(CREATED, PAYMENT_NOTIFICATION_CREATED)) + .map(ISO_INSTANT_MILLISECOND_PRECISION::format).orElse(null), transaction.getCreatedDate(), "created_date"); String chargeExternalStatus = ChargeStatus.fromString(chargeEntity.getStatus()).toExternal().getStatusV2(); @@ -231,15 +245,48 @@ private boolean matchFeatureSpecificFields(ChargeEntity chargeEntity, LedgerTran transaction.getCorporateCardSurcharge(), "corporate_surcharge"); fieldsMatch = fieldsMatch && isEquals(chargeEntity.getNetAmount().orElse(null), transaction.getNetAmount(), "net_amount"); - + fieldsMatch = fieldsMatch && isEquals(getTotalAmountFor(chargeEntity), + transaction.getTotalAmount(), "total_amount"); + fieldsMatch = fieldsMatch && - isEquals(Optional.ofNullable(chargeEntity.getWalletType()) + isEquals(ofNullable(chargeEntity.getWalletType()) .map(WalletType::toString) .orElse(null), transaction.getWalletType(), "wallet_type"); return fieldsMatch; } + private boolean matchCaptureFields(ChargeEntity chargeEntity, LedgerTransaction transaction) { + boolean fieldsMatch = isEquals( + getChargeEventDate(chargeEntity, List.of(CAPTURED)).map(DateTimeUtils::toUTCDateString).orElse(null), + ofNullable(transaction.getSettlementSummary()).map(ChargeResponse.SettlementSummary::getCapturedDate).orElse(null), + "captured_date"); + fieldsMatch &= isEquals( + getChargeEventDate(chargeEntity, List.of(CAPTURE_SUBMITTED)).map(ISO_INSTANT_MILLISECOND_PRECISION::format).orElse(null), + ofNullable(transaction.getSettlementSummary()).map(ChargeResponse.SettlementSummary::getCaptureSubmitTime).orElse(null), + "capture_submit_time"); + + return fieldsMatch; + } + + private boolean matchRefundSummary(ChargeEntity chargeEntity, LedgerTransaction transaction) { + List refundsList = refundDao.findRefundsByChargeExternalId(chargeEntity.getExternalId()); + + DefaultExternalRefundAvailabilityCalculator defaultExternalRefundAvailabilityCalculator = new DefaultExternalRefundAvailabilityCalculator(); + ExternalChargeRefundAvailability refundAvailability = defaultExternalRefundAvailabilityCalculator.calculate(Charge.from(chargeEntity), refundsList); + + return isEquals(refundAvailability.getStatus(), + ofNullable(transaction.getRefundSummary()).map(refundSummary -> refundSummary.getStatus()).orElse(null), "refund_summary.status"); + } + + private Optional getChargeEventDate(ChargeEntity chargeEntity, List chargeEventStatuses) { + return chargeEntity.getEvents() + .stream() + .filter(chargeEventEntity -> chargeEventStatuses.contains(chargeEventEntity.getStatus())) + .findFirst() + .map(ChargeEventEntity::getUpdated); + } + private boolean isEquals(Object value1, Object value2, String fieldName) { if (Objects.equals(value1, value2)) { return true; diff --git a/src/test/java/uk/gov/pay/connector/expunge/service/LedgerStub.java b/src/test/java/uk/gov/pay/connector/expunge/service/LedgerStub.java index c58ad4bd0c..18112d636d 100644 --- a/src/test/java/uk/gov/pay/connector/expunge/service/LedgerStub.java +++ b/src/test/java/uk/gov/pay/connector/expunge/service/LedgerStub.java @@ -3,10 +3,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import uk.gov.pay.connector.charge.model.ChargeResponse; import uk.gov.pay.connector.it.dao.DatabaseFixtures; -import uk.gov.pay.connector.it.util.ChargeUtils; -import uk.gov.pay.connector.model.domain.LedgerTransactionFixture; -import uk.gov.pay.connector.paritycheck.LedgerTransaction; import java.util.HashMap; import java.util.Map; @@ -33,7 +31,7 @@ public void returnLedgerTransactionWithMismatch(String externalId, DatabaseFixtu ledgerTransactionFields.put("description", "This is a mismatch"); stubResponse(externalId, ledgerTransactionFields); } - + private void stubResponse(String externalId, Map ledgerTransactionFields) throws JsonProcessingException { ResponseDefinitionBuilder responseDefBuilder = aResponse() .withHeader(CONTENT_TYPE, APPLICATION_JSON) @@ -47,8 +45,8 @@ private void stubResponse(String externalId, Map ledgerTransacti ) ); } - - private static Map testChargeToLedgerTransactionJson(DatabaseFixtures.TestCharge testCharge) { + + private static Map testChargeToLedgerTransactionJson(DatabaseFixtures.TestCharge testCharge) { var map = new HashMap(); Optional.ofNullable(testCharge.getExternalChargeId()).ifPresent(value -> map.put("id", value)); Optional.of(testCharge.getAmount()).ifPresent(value -> map.put("amount", String.valueOf(value))); @@ -79,6 +77,12 @@ private static Map testChargeToLedgerTransactionJson(DatabaseFix account)); Optional.ofNullable(testCharge.getTestAccount().getPaymentProvider()).ifPresent(account -> map.put("payment_provider", account)); + + ChargeResponse.RefundSummary refundSummary = new ChargeResponse.RefundSummary(); + refundSummary.setStatus("available"); + Optional.ofNullable(testCharge.getTestAccount().getPaymentProvider()).ifPresent(account -> map.put("refund_summary", + refundSummary)); + map.put("live", false); return map; } diff --git a/src/test/java/uk/gov/pay/connector/it/resources/ExpungeResourceIT.java b/src/test/java/uk/gov/pay/connector/it/resources/ExpungeResourceIT.java index 995b8f74e0..7d62aab7c9 100644 --- a/src/test/java/uk/gov/pay/connector/it/resources/ExpungeResourceIT.java +++ b/src/test/java/uk/gov/pay/connector/it/resources/ExpungeResourceIT.java @@ -31,6 +31,7 @@ import static org.hamcrest.core.IsNot.not; import static uk.gov.pay.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; import static uk.gov.pay.commons.model.SupportedLanguage.ENGLISH; +import static uk.gov.pay.connector.charge.model.domain.ChargeStatus.CREATED; import static uk.gov.pay.connector.junit.DropwizardJUnitRunner.WIREMOCK_PORT; @RunWith(DropwizardJUnitRunner.class) @@ -64,7 +65,6 @@ private void insertTestAccount() { .insert(); } - @Test public void shouldExpungeCharge() throws JsonProcessingException { var chargedId = ThreadLocalRandom.current().nextLong(); @@ -84,6 +84,7 @@ public void shouldExpungeCharge() throws JsonProcessingException { .withReturnUrl("https://www.test.test/") .withChargeStatus(ChargeStatus.CAPTURED) .insert(); + insertChargeEvent(expungeableCharge1); ledgerStub.returnLedgerTransaction("external_charge_id", expungeableCharge1); var charge = databaseTestHelper.containsChargeWithExternalId("external_charge_id"); assertThat(charge, is(true)); @@ -95,7 +96,16 @@ public void shouldExpungeCharge() throws JsonProcessingException { var postCharge = databaseTestHelper.containsChargeWithExternalId("external_charge_id"); assertThat(postCharge, is(false)); } - + + private void insertChargeEvent(DatabaseFixtures.TestCharge charge) { + DatabaseFixtures.withDatabaseTestHelper(databaseTestHelper).aTestChargeEvent() + .withChargeStatus(CREATED) + .withDate(charge.getCreatedDate()) + .withChargeId(charge.getChargeId()) + .withTestCharge(charge) + .insert(); + } + @Test public void shouldUpdateTheParityCheckedDateOfNonCriteriaMatchedCharge() throws JsonProcessingException { var chargedId = ThreadLocalRandom.current().nextLong(); @@ -110,6 +120,7 @@ public void shouldUpdateTheParityCheckedDateOfNonCriteriaMatchedCharge() throws .withAmount(2500) .withChargeStatus(ChargeStatus.CAPTURED) .insert(); + insertChargeEvent(expungeableCharge1); ledgerStub.returnLedgerTransactionWithMismatch("external_charge_id", expungeableCharge1); var charge = databaseTestHelper.containsChargeWithExternalId("external_charge_id"); assertThat(charge, is(true)); @@ -142,6 +153,7 @@ public void shouldNotExpungeChargeThatIsNotOldEnoughToBeExpunged() throws JsonPr .withReturnUrl("https://www.test.test/") .withChargeStatus(ChargeStatus.CAPTURED) .insert(); + insertChargeEvent(expungeableCharge1); ledgerStub.returnLedgerTransaction("external_charge_id", expungeableCharge1); var charge = databaseTestHelper.containsChargeWithExternalId("external_charge_id_2"); assertThat(charge, is(true)); @@ -175,6 +187,7 @@ public void shouldExpungeChargesMeetingCriteriaButNotThoseThatDont() throws Json .withReturnUrl("https://www.test.test/") .withChargeStatus(ChargeStatus.CAPTURED) .insert(); + insertChargeEvent(expungeableCharge1); ledgerStub.returnLedgerTransaction("external_charge_id_10", expungeableCharge1); var chargedId2 = ThreadLocalRandom.current().nextLong(); @@ -195,6 +208,7 @@ public void shouldExpungeChargesMeetingCriteriaButNotThoseThatDont() throws Json .withReturnUrl("https://www.test.test/") .withChargeStatus(ChargeStatus.CAPTURED) .insert(); + insertChargeEvent(nonExpungeableCharge1); ledgerStub.returnLedgerTransaction("external_charge_id_11", nonExpungeableCharge1); var chargedId3 = ThreadLocalRandom.current().nextLong(); @@ -215,6 +229,7 @@ public void shouldExpungeChargesMeetingCriteriaButNotThoseThatDont() throws Json .withReturnUrl("https://www.test.test/") .withChargeStatus(ChargeStatus.CAPTURED) .insert(); + insertChargeEvent(expungeableCharge2); ledgerStub.returnLedgerTransaction("external_charge_id_12", expungeableCharge2); var chargedId4 = ThreadLocalRandom.current().nextLong(); @@ -235,6 +250,7 @@ public void shouldExpungeChargesMeetingCriteriaButNotThoseThatDont() throws Json .withReturnUrl("https://www.test.test/") .withChargeStatus(ChargeStatus.CAPTURED) .insert(); + insertChargeEvent(nonExpungeableCharge2); ledgerStub.returnLedgerTransaction("external_charge_id_13", nonExpungeableCharge2); given().port(testContext.getPort()) diff --git a/src/test/java/uk/gov/pay/connector/model/domain/LedgerTransactionFixture.java b/src/test/java/uk/gov/pay/connector/model/domain/LedgerTransactionFixture.java index 035019c0e7..968d2ed1de 100644 --- a/src/test/java/uk/gov/pay/connector/model/domain/LedgerTransactionFixture.java +++ b/src/test/java/uk/gov/pay/connector/model/domain/LedgerTransactionFixture.java @@ -3,22 +3,33 @@ import uk.gov.pay.commons.model.Source; import uk.gov.pay.commons.model.SupportedLanguage; import uk.gov.pay.connector.charge.model.CardDetailsEntity; +import uk.gov.pay.connector.charge.model.ChargeResponse; import uk.gov.pay.connector.charge.model.FirstDigitsCardNumber; import uk.gov.pay.connector.charge.model.LastDigitsCardNumber; +import uk.gov.pay.connector.charge.model.domain.Charge; import uk.gov.pay.connector.charge.model.domain.ChargeEntity; import uk.gov.pay.connector.charge.model.domain.ChargeStatus; +import uk.gov.pay.connector.chargeevent.model.domain.ChargeEventEntity; +import uk.gov.pay.connector.common.model.api.ExternalChargeRefundAvailability; +import uk.gov.pay.connector.gateway.util.DefaultExternalRefundAvailabilityCalculator; import uk.gov.pay.connector.gatewayaccount.model.GatewayAccountEntity; import uk.gov.pay.connector.paritycheck.Address; import uk.gov.pay.connector.paritycheck.CardDetails; import uk.gov.pay.connector.paritycheck.LedgerTransaction; import uk.gov.pay.connector.paritycheck.TransactionState; +import uk.gov.pay.connector.refund.model.domain.RefundEntity; import uk.gov.pay.connector.wallets.WalletType; import java.time.ZonedDateTime; +import java.util.List; import static java.util.Objects.nonNull; import static java.util.Optional.ofNullable; import static uk.gov.pay.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; +import static uk.gov.pay.connector.charge.model.domain.ChargeStatus.CAPTURED; +import static uk.gov.pay.connector.charge.model.domain.ChargeStatus.CAPTURE_SUBMITTED; +import static uk.gov.pay.connector.charge.model.domain.ChargeStatus.CREATED; +import static uk.gov.pay.connector.charge.util.CorporateCardSurchargeCalculator.getTotalAmountFor; public class LedgerTransactionFixture { private String status = "created"; @@ -42,17 +53,20 @@ public class LedgerTransactionFixture { private Long corporateCardSurcharge; private Long fee; private boolean moto; + private Long totalAmount; + private ZonedDateTime captureSubmittedDate; + private ZonedDateTime capturedDate; + private ChargeResponse.RefundSummary refundSummary; public static LedgerTransactionFixture aValidLedgerTransaction() { return new LedgerTransactionFixture(); } - public static LedgerTransactionFixture from(ChargeEntity chargeEntity) { + public static LedgerTransactionFixture from(ChargeEntity chargeEntity, List refundsList) { LedgerTransactionFixture ledgerTransactionFixture = aValidLedgerTransaction() .withStatus(ChargeStatus.fromString(chargeEntity.getStatus()).toExternal().getStatusV2()) .withExternalId(chargeEntity.getExternalId()) - .withCreatedDate(chargeEntity.getCreatedDate()) .withAmount(chargeEntity.getAmount()) .withDescription(chargeEntity.getDescription()) .withReference(chargeEntity.getReference().toString()) @@ -66,8 +80,10 @@ public static LedgerTransactionFixture from(ChargeEntity chargeEntity) { .withFee(chargeEntity.getFeeAmount().orElse(null)) .withCorporateCardSurcharge(chargeEntity.getCorporateSurcharge().orElse(null)) .withWalletType(chargeEntity.getWalletType()) + .withTotalAmount(getTotalAmountFor(chargeEntity)) .withNetAmount(chargeEntity.getNetAmount().orElse(null)); + ledgerTransactionFixture.withCreatedDate(getEventDate(chargeEntity.getEvents(), CREATED)); if (chargeEntity.getGatewayAccount() != null) { GatewayAccountEntity gatewayAccount = chargeEntity.getGatewayAccount(); ledgerTransactionFixture.withPaymentProvider(gatewayAccount.getGatewayName()); @@ -99,10 +115,32 @@ public static LedgerTransactionFixture from(ChargeEntity chargeEntity) { ledgerTransactionFixture.withCardDetails(cardDetails); } + ledgerTransactionFixture.withCapturedDate(getEventDate(chargeEntity.getEvents(), CAPTURED)); + ledgerTransactionFixture.withCaptureSubmittedDate(getEventDate(chargeEntity.getEvents(), CAPTURE_SUBMITTED)); + + ChargeResponse.RefundSummary refundSummary = new ChargeResponse.RefundSummary(); + ExternalChargeRefundAvailability refundAvailability; + if (refundsList != null) { + refundAvailability = new DefaultExternalRefundAvailabilityCalculator() + .calculate(Charge.from(chargeEntity), refundsList); + } else { + refundAvailability = new DefaultExternalRefundAvailabilityCalculator() + .calculate(Charge.from(chargeEntity), List.of()); + } + refundSummary.setStatus(refundAvailability.getStatus()); + ledgerTransactionFixture.withRefundSummary(refundSummary); return ledgerTransactionFixture; } + private static ZonedDateTime getEventDate(List chargeEventEntities, ChargeStatus status) { + return ofNullable(chargeEventEntities).flatMap(entities -> entities.stream() + .filter(chargeEvent -> status.equals(chargeEvent.getStatus())) + .findFirst() + .map(ChargeEventEntity::getUpdated)) + .orElse(null); + } + public LedgerTransaction build() { var ledgerTransaction = new LedgerTransaction(); ledgerTransaction.setState(new TransactionState(status)); @@ -129,8 +167,15 @@ public LedgerTransaction build() { ledgerTransaction.setFee(fee); ledgerTransaction.setCorporateCardSurcharge(corporateCardSurcharge); ledgerTransaction.setNetAmount(netAmount); + ledgerTransaction.setTotalAmount(totalAmount); ledgerTransaction.setWalletType(walletType); - + + ChargeResponse.SettlementSummary settlementSummary = new ChargeResponse.SettlementSummary(); + settlementSummary.setCapturedTime(capturedDate); + settlementSummary.setCaptureSubmitTime(captureSubmittedDate); + ledgerTransaction.setSettlementSummary(settlementSummary); + ledgerTransaction.setRefundSummary(refundSummary); + return ledgerTransaction; } @@ -239,4 +284,24 @@ public LedgerTransactionFixture withDelayedCapture(boolean delayedCapture) { return this; } + public LedgerTransactionFixture withTotalAmount(Long totalAmount) { + this.totalAmount = totalAmount; + return this; + } + + public LedgerTransactionFixture withCaptureSubmittedDate(ZonedDateTime captureSubmittedDate) { + this.captureSubmittedDate = captureSubmittedDate; + return this; + } + + public LedgerTransactionFixture withCapturedDate(ZonedDateTime capturedDate) { + this.capturedDate = capturedDate; + return this; + } + + public LedgerTransactionFixture withRefundSummary(ChargeResponse.RefundSummary refundSummary) { + this.refundSummary = refundSummary; + return this; + } + } diff --git a/src/test/java/uk/gov/pay/connector/tasks/ParityCheckServiceTest.java b/src/test/java/uk/gov/pay/connector/tasks/ParityCheckServiceTest.java index e63d5809c8..40b5f505be 100644 --- a/src/test/java/uk/gov/pay/connector/tasks/ParityCheckServiceTest.java +++ b/src/test/java/uk/gov/pay/connector/tasks/ParityCheckServiceTest.java @@ -6,16 +6,22 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import uk.gov.pay.connector.charge.model.domain.ChargeEntity; +import uk.gov.pay.connector.charge.model.domain.ChargeStatus; import uk.gov.pay.connector.charge.service.ChargeService; +import uk.gov.pay.connector.chargeevent.model.domain.ChargeEventEntity; import uk.gov.pay.connector.paritycheck.CardDetails; import uk.gov.pay.connector.paritycheck.LedgerService; import uk.gov.pay.connector.paritycheck.LedgerTransaction; import uk.gov.pay.connector.refund.dao.RefundDao; +import uk.gov.pay.connector.refund.model.domain.RefundEntity; +import java.util.List; import java.util.Optional; +import static java.time.ZonedDateTime.parse; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static uk.gov.pay.commons.model.Source.CARD_API; @@ -24,9 +30,12 @@ import static uk.gov.pay.connector.charge.model.domain.ChargeEntityFixture.defaultCardDetails; import static uk.gov.pay.connector.charge.model.domain.ChargeEntityFixture.defaultGatewayAccountEntity; import static uk.gov.pay.connector.charge.model.domain.ChargeStatus.CAPTURED; +import static uk.gov.pay.connector.charge.model.domain.ChargeStatus.CAPTURE_SUBMITTED; +import static uk.gov.pay.connector.charge.model.domain.ChargeStatus.CREATED; import static uk.gov.pay.connector.charge.model.domain.ParityCheckStatus.DATA_MISMATCH; import static uk.gov.pay.connector.model.domain.LedgerTransactionFixture.aValidLedgerTransaction; import static uk.gov.pay.connector.model.domain.LedgerTransactionFixture.from; +import static uk.gov.pay.connector.pact.ChargeEventEntityFixture.aValidChargeEventEntity; import static uk.gov.pay.connector.wallets.WalletType.APPLE_PAY; import static uk.gov.pay.connector.wallets.WalletType.GOOGLE_PAY; @@ -43,11 +52,14 @@ public class ParityCheckServiceTest { @Mock private HistoricalEventEmitter mockHistoricalEventEmitter; private ChargeEntity chargeEntity; + private List refundEntities = List.of(); @Before public void setUp() { - parityCheckService = new ParityCheckService(mockLedgerService, mockChargeService, mockRefundDao, - mockHistoricalEventEmitter); + ChargeEventEntity chargeEventCreated = createChargeEventEntity(CREATED, "2016-01-25T13:23:55Z"); + ChargeEventEntity chargeEventCaptured = createChargeEventEntity(CAPTURED, "2016-01-26T14:23:55Z"); + ChargeEventEntity chargeEventCaptureSubmitted = createChargeEventEntity(CAPTURE_SUBMITTED, + "2016-01-26T13:23:55Z"); chargeEntity = aValidChargeEntity() .withStatus(CAPTURED) @@ -59,12 +71,17 @@ public void setUp() { .withCorporateSurcharge(25L) .withWalletType(APPLE_PAY) .withDelayedCapture(true) + .withEvents(List.of(chargeEventCreated, chargeEventCaptured, chargeEventCaptureSubmitted)) .build(); + + when(mockRefundDao.findRefundsByChargeExternalId(any())).thenReturn(refundEntities); + parityCheckService = new ParityCheckService(mockLedgerService, mockChargeService, mockRefundDao, + mockHistoricalEventEmitter); } @Test public void parityCheckChargeForExpunger_shouldReturnTrueIfChargeMatchesFieldsWithLedger() { - LedgerTransaction transaction = from(chargeEntity).build(); + LedgerTransaction transaction = from(chargeEntity, refundEntities).build(); when(mockLedgerService.getTransaction(chargeEntity.getExternalId())).thenReturn(Optional.of(transaction)); boolean matchesWithLedger = parityCheckService.parityCheckChargeForExpunger(chargeEntity); @@ -75,7 +92,7 @@ public void parityCheckChargeForExpunger_shouldReturnTrueIfChargeMatchesFieldsWi @Test public void parityCheckChargeForExpunger_shouldReturnFalseIfCardDetailsDoesNotMatchWithLedger() { chargeEntity.getCardDetails().setBillingAddress(null); - LedgerTransaction transaction = from(chargeEntity) + LedgerTransaction transaction = from(chargeEntity, refundEntities) .withCardDetails(new CardDetails("test-name", null, "test-brand", "6666", "123656", "11/88", null)) .build(); @@ -88,7 +105,7 @@ public void parityCheckChargeForExpunger_shouldReturnFalseIfCardDetailsDoesNotMa @Test public void parityCheckChargeForExpunger_shouldReturnFalseIfGatewayAccountDetailsDoesNotMatch() { - LedgerTransaction transaction = from(chargeEntity) + LedgerTransaction transaction = from(chargeEntity, refundEntities) .withGatewayAccountId(345345L) .isLive(true) .withPaymentProvider("test-paymemt-provider") @@ -104,7 +121,7 @@ public void parityCheckChargeForExpunger_shouldReturnFalseIfGatewayAccountDetail @Test public void parityCheckChargeForExpunger_shouldReturnFalseIfFeatureSpecificFieldsDoesNotMatch() { - LedgerTransaction transaction = from(chargeEntity) + LedgerTransaction transaction = from(chargeEntity, refundEntities) .withSource(CARD_API) .withMoto(false) .withDelayedCapture(false) @@ -122,6 +139,35 @@ public void parityCheckChargeForExpunger_shouldReturnFalseIfFeatureSpecificField verify(mockChargeService).updateChargeParityStatus(chargeEntity.getExternalId(), DATA_MISMATCH); } + @Test + public void parityCheckChargeForExpunger_shouldReturnFalseIfCaptureFieldsDoesnotMatchWithLedger() { + LedgerTransaction transaction = from(chargeEntity, refundEntities) + .withCapturedDate(parse("2016-01-26T14:23:55Z")) + .withCaptureSubmittedDate(parse("2016-01-26T14:23:55Z")) + .build(); + when(mockLedgerService.getTransaction(chargeEntity.getExternalId())).thenReturn(Optional.of(transaction)); + + boolean matchesWithLedger = parityCheckService.parityCheckChargeForExpunger(chargeEntity); + + assertThat(matchesWithLedger, is(false)); + verify(mockHistoricalEventEmitter).processPaymentEvents(chargeEntity, true); + verify(mockChargeService).updateChargeParityStatus(chargeEntity.getExternalId(), DATA_MISMATCH); + } + + @Test + public void parityCheckChargeForExpunger_shouldReturnFalseIfRefundSummaryStatusDoesnotMatchWithLedger() { + LedgerTransaction transaction = from(chargeEntity, refundEntities) + .withRefundSummary(null) + .build(); + when(mockLedgerService.getTransaction(chargeEntity.getExternalId())).thenReturn(Optional.of(transaction)); + + boolean matchesWithLedger = parityCheckService.parityCheckChargeForExpunger(chargeEntity); + + assertThat(matchesWithLedger, is(false)); + verify(mockHistoricalEventEmitter).processPaymentEvents(chargeEntity, true); + verify(mockChargeService).updateChargeParityStatus(chargeEntity.getExternalId(), DATA_MISMATCH); + } + @Test public void parityCheckChargeForExpunger_shouldReturnFalseIfChargeDoesNotMatchWithLedger() { LedgerTransaction transaction = aValidLedgerTransaction().withStatus("pending").build(); @@ -132,4 +178,12 @@ public void parityCheckChargeForExpunger_shouldReturnFalseIfChargeDoesNotMatchWi verify(mockHistoricalEventEmitter).processPaymentEvents(chargeEntity, true); verify(mockChargeService).updateChargeParityStatus(chargeEntity.getExternalId(), DATA_MISMATCH); } + + private ChargeEventEntity createChargeEventEntity(ChargeStatus status, String timeStamp) { + return aValidChargeEventEntity() + .withCharge(chargeEntity) + .withChargeStatus(status) + .withTimestamp(parse(timeStamp)) + .build(); + } } diff --git a/src/test/java/uk/gov/pay/connector/tasks/ParityCheckWorkerTest.java b/src/test/java/uk/gov/pay/connector/tasks/ParityCheckWorkerTest.java index ae619d8612..2290691e50 100644 --- a/src/test/java/uk/gov/pay/connector/tasks/ParityCheckWorkerTest.java +++ b/src/test/java/uk/gov/pay/connector/tasks/ParityCheckWorkerTest.java @@ -108,7 +108,7 @@ public void executeSkipsParityCheckForAlreadyCheckedChargesExistingInLedger() { public void executeRecordsParityStatusForChargesExistingInLedger() { when(chargeDao.findMaxId()).thenReturn(1L); when(chargeDao.findById(1L)).thenReturn(Optional.of(chargeEntity)); - when(ledgerService.getTransaction(chargeEntity.getExternalId())).thenReturn(Optional.of(from(chargeEntity).build())); + when(ledgerService.getTransaction(chargeEntity.getExternalId())).thenReturn(Optional.of(from(chargeEntity, null).build())); worker.execute(1L, Optional.empty(), doNotReprocessValidRecords, emptyParityCheckStatus, 1L); @@ -125,7 +125,7 @@ public void executeRecordsParityStatusForChargeAndRefundsExistingInLedger() { when(chargeDao.findMaxId()).thenReturn(1L); when(chargeDao.findById(1L)).thenReturn(Optional.of(chargeEntity)); when(refundDao.findRefundsByChargeExternalId(chargeEntity.getExternalId())).thenReturn(List.of(refundEntity)); - when(ledgerService.getTransaction(chargeEntity.getExternalId())).thenReturn(Optional.of(from(chargeEntity).build())); + when(ledgerService.getTransaction(chargeEntity.getExternalId())).thenReturn(Optional.of(from(chargeEntity, null).build())); when(ledgerService.getTransaction(refundEntity.getExternalId())) .thenReturn(Optional.of(aValidLedgerTransaction().withStatus("submitted").build())); @@ -134,7 +134,7 @@ public void executeRecordsParityStatusForChargeAndRefundsExistingInLedger() { verify(chargeService, times(1)).updateChargeParityStatus(chargeEntity.getExternalId(), ParityCheckStatus.EXISTS_IN_LEDGER); verify(ledgerService, times(2)).getTransaction(any()); verify(ledgerService, times(1)).getTransaction(chargeEntity.getExternalId()); - verify(refundDao, times(1)).findRefundsByChargeExternalId(chargeEntity.getExternalId()); + verify(refundDao, times(2)).findRefundsByChargeExternalId(chargeEntity.getExternalId()); verify(stateTransitionService, never()).offerStateTransition(any(), any(), any()); verify(emittedEventDao, never()).recordEmission(any(), any()); verify(chargeDao, never()).findById(2L); @@ -161,7 +161,7 @@ public void executeEmitsEventAndRecordsEmissionWhenRefundDoesNotExist() { .thenReturn(List.of(aValidRefundEntity().build(), aValidRefundEntity().build())); when(chargeDao.findById(1L)).thenReturn(Optional.of(chargeEntity)); when(ledgerService.getTransaction(any())).thenReturn(Optional.empty()); - when(ledgerService.getTransaction(chargeEntity.getExternalId())).thenReturn(Optional.of(from(chargeEntity).build())); + when(ledgerService.getTransaction(chargeEntity.getExternalId())).thenReturn(Optional.of(from(chargeEntity, null).build())); worker.execute(1L, Optional.empty(), doNotReprocessValidRecords, emptyParityCheckStatus, 1L); @@ -179,7 +179,7 @@ public void executeEmitsEventAndRecordsEmissionWhenRefundWithDifferentStatusInLe when(ledgerService.getTransaction(any())).thenReturn(Optional.empty()); when(ledgerService.getTransaction(any())).thenReturn(Optional.of( aValidLedgerTransaction().withStatus("success").build())); - when(ledgerService.getTransaction(chargeEntity.getExternalId())).thenReturn(Optional.of(from(chargeEntity).build())); + when(ledgerService.getTransaction(chargeEntity.getExternalId())).thenReturn(Optional.of(from(chargeEntity, null).build())); worker.execute(1L, Optional.empty(), doNotReprocessValidRecords, emptyParityCheckStatus, 1L);