From 8b250ddfb09e7a9737bdc905b90790646e89992c Mon Sep 17 00:00:00 2001 From: crowlandsCH <119855149+crowlandsCH@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:39:49 +0000 Subject: [PATCH] DSND-3113: Change delete operation order for pscs, add kind and delta_at logic (strict) (#157) * DSND-3113: WIP - Adds kind property to the delete PSC delta Requires changes to use the `kind` argument from the DELETE request header * DSND-3113: WIP Implement Kind logic for delete deltas * kind logic for upsert is very different from for delete. * implemented kind logic coming from delta instead of being retrieved from mongo * implemented kind and delta_at headers within controller method * implemented delta_at check similar to filing history * fixed all compilation errors * DSND-3113: Implement chs-kafka-api call and fix any remaining test cases. * add needed exclusions to pom for test functionality * made kind variables more accurate across the project * added logic for even if document deleted chs-kafka is still called * added additional tests for kind and fixed any existing broken ones * DSND-3113: Unused imports and wildcard imports fixed. * DSND-3113: Address PR comments * DSND-3113: Upgrade versions in Pom * DSND-3113: Revert spring boot upgrade in the Pom * DSND-3113: Upgrade sdk version in pom to enable header handlers for kind and delta_at * DSND-3113: Address PR comments 2 * DSND-3113: Add additional iTest class for deltaAt conflict test case * DSND-3113: Upgrade springboot versions appropriately * DSND-3113: Address PR comment * use handmade exception * add unit testcase for deltaAt check * DSND-3113: Add ExceptionHandlerConfig case and relevant testcases * DSND-3113: Remove catch statement in the controller * DSND-3113: Address PR comment * some small test name changes * DSND-3113: Address PR comment * test name changes * string concatenation --------- Co-authored-by: johncookch --- pom.xml | 18 +- .../pscdataapi/steps/PscDataSteps.java | 234 ++++++++++++------ .../resources/features/delete_psc.feature | 16 +- .../pscdataapi/api/ChsKafkaApiService.java | 24 +- .../config/ExceptionHandlerConfig.java | 21 ++ .../controller/CompanyPscController.java | 12 +- .../CompanyPscFullRecordGetController.java | 1 - .../exceptions/ConflictException.java | 8 + .../pscdataapi/models/PscDeleteRequest.java | 47 ++++ .../pscdataapi/service/CompanyPscService.java | 71 +++--- .../transform/CompanyPscTransformer.java | 25 +- .../pscdataapi/util/DateUtils.java | 21 ++ .../api/ChsKafkaApiServiceTest.java | 70 +++++- ...ServiceAspectFeatureFlagDisabledITest.java | 6 +- ...iServiceAspectFeatureFlagEnabledITest.java | 6 +- .../config/ExceptionHandlerConfigTest.java | 9 + .../controller/CompanyPscControllerTest.java | 52 +++- .../service/CompanyPscServiceTest.java | 75 ++++-- .../pscdataapi/util/TestHelper.java | 8 +- 19 files changed, 542 insertions(+), 182 deletions(-) create mode 100644 src/main/java/uk/gov/companieshouse/pscdataapi/exceptions/ConflictException.java create mode 100644 src/main/java/uk/gov/companieshouse/pscdataapi/models/PscDeleteRequest.java create mode 100644 src/main/java/uk/gov/companieshouse/pscdataapi/util/DateUtils.java diff --git a/pom.xml b/pom.xml index 484c5d4b..ad4a34f0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ uk.gov.companieshouse companies-house-parent - 2.1.6 + 2.1.10 psc-data-api @@ -16,8 +16,8 @@ 21 uk.gov.companieshouse.pscdataapi.PscDataApiApplication - 3.3.1 - 3.3.1 + 3.3.6 + 3.3.6 ${java.version} ${java.version} 3.3.0 @@ -25,8 +25,8 @@ 0.8.12 - 3.0.8 - 4.0.241 + 3.0.20 + 4.0.249 1.0.6 2.0.5 3.0.5 @@ -71,7 +71,7 @@ log4j-to-slf4j - + org.springframework.boot spring-boot-starter-actuator @@ -132,6 +132,12 @@ org.springframework.boot spring-boot-starter-test test + + + org.xmlunit + xmlunit-core + + uk.gov.companieshouse diff --git a/src/itest/java/uk/gov/companieshouse/pscdataapi/steps/PscDataSteps.java b/src/itest/java/uk/gov/companieshouse/pscdataapi/steps/PscDataSteps.java index e63380f3..504f4e87 100644 --- a/src/itest/java/uk/gov/companieshouse/pscdataapi/steps/PscDataSteps.java +++ b/src/itest/java/uk/gov/companieshouse/pscdataapi/steps/PscDataSteps.java @@ -71,6 +71,12 @@ public class PscDataSteps { + private static final String KIND = "individual-person-with-significant-control"; + private static final String DELTA_AT = "20240219123045999999"; + private static final String COMPANY_NUMBER = "34777772"; + private static final String NOTIFICATION_ID = "ZfTs9WeeqpXTqf6dc6FZ4C0H0ZZ"; + private static final String CONTEXT_ID = "5234234234"; + @Autowired private ObjectMapper objectMapper; @Autowired @@ -89,12 +95,6 @@ public class PscDataSteps { @InjectMocks private CompanyPscService companyPscService; - - - private final String COMPANY_NUMBER = "34777772"; - private final String NOTIFICATION_ID = "ZfTs9WeeqpXTqf6dc6FZ4C0H0ZZ"; - private final String contextId = "5234234234"; - private AutoCloseable autoCloseable; @Before @@ -144,12 +144,12 @@ public void chs_kafka_api_not_invoked() { @Then("the CHS Kafka API is not invoked with a DELETE event") public void chs_kafka_api_not_invoked_for_delete() { - verify(chsKafkaApiService, times(0)).invokeChsKafkaApiWithDeleteEvent(any(), any(), any(), any(), any()); + verify(chsKafkaApiService, times(0)).invokeChsKafkaApiWithDeleteEvent(any(), any()); } @Then("the CHS Kafka API is invoked with a DELETE event") public void chs_kafka_api_is_invoked_for_delete() { - verify(chsKafkaApiService, times(1)).invokeChsKafkaApiWithDeleteEvent(any(), any(), any(), any(), any()); + verify(chsKafkaApiService, times(1)).invokeChsKafkaApiWithDeleteEvent(any(), any()); } @And("the CHS Kafka API service is not invoked") @@ -162,8 +162,8 @@ public void i_send_psc_record_put_request_with_payload(String dataFile, String c String data = FileReaderUtil.readFile("src/itest/resources/json/input/" + dataFile + ".json"); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -181,8 +181,8 @@ public void i_send_psc_record_put_request_with_payload(String dataFile, String n HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -200,8 +200,8 @@ public void i_send_psc_data_put_request_with_payload(String dataFile, String not HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -237,8 +237,8 @@ public void aDELETERequestIsSentForWithoutValidERICHeaders(String companyNumber) HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); HttpEntity request = new HttpEntity<>(null, headers); String uri = "/company/{company_number}/persons-with-significant-control/{notification_id}/full_record"; @@ -249,15 +249,37 @@ public void aDELETERequestIsSentForWithoutValidERICHeaders(String companyNumber) @When("a DELETE request is sent for {string}") - public void aDELETERequestIsSentFor(String companyNumber) { + public void aDeleteRequestIsSentFor(String companyNumber) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); + headers.set("ERIC-Identity", "TEST-IDENTITY"); + headers.set("ERIC-Identity-Type", "key"); + headers.set("ERIC-Authorised-Key-Roles", "*"); + headers.set("x-kind", KIND); + headers.set("x-delta-at", DELTA_AT); + + HttpEntity request = new HttpEntity<>(null, headers); + String uri = "/company/%s/persons-with-significant-control/%s/full_record".formatted(companyNumber, NOTIFICATION_ID); + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.DELETE, request, Void.class); + + CucumberContext.CONTEXT.set("statusCode", response.getStatusCode().value()); + } + + @When("a DELETE request is sent for {string} with a stale {string}") + public void aDeleteRequestIsSentFor(String companyNumber, String deltaAt) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); + headers.set("x-kind", KIND); + headers.set("x-delta-at", deltaAt); HttpEntity request = new HttpEntity<>(null, headers); String uri = "/company/%s/persons-with-significant-control/%s/full_record".formatted(companyNumber, NOTIFICATION_ID); @@ -279,7 +301,7 @@ public void theDatabaseIsDown() { @When("the chs kafka api is not available") public void theChsKafkaApiIsNotAvailable() { doThrow(ServiceUnavailableException.class).when(chsKafkaApiService).invokeChsKafkaApi(any(), any(), any(), any()); - doThrow(ServiceUnavailableException.class).when(chsKafkaApiService).invokeChsKafkaApiWithDeleteEvent(any(), any(), any(), any(), any()); + doThrow(ServiceUnavailableException.class).when(chsKafkaApiService).invokeChsKafkaApiWithDeleteEvent(any(), any()); } @And("a PSC {string} exists for {string} for Super Secure") @@ -311,8 +333,8 @@ public void aGetRequestIsSentForAndForSuperSecure(String companyNumber, String n HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -343,8 +365,8 @@ public void aGetRequestIsSentForAndWithoutERICHeadersForSuperSecure(String compa HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); HttpEntity request = new HttpEntity<>(null, headers); String uri = @@ -361,8 +383,8 @@ public void aGetRequestHasBeenSentForAndForSuperSecure(String companyNumber, Str HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -405,8 +427,8 @@ public void aGetRequestIsSentForAndForSuperSecureBeneficialOwner(String companyN HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -438,8 +460,8 @@ public void aGetRequestIsSentForAndWithoutERICHeadersForSuperSecureBeneficialOwn HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); HttpEntity request = new HttpEntity<>(null, headers); String uri = @@ -456,8 +478,8 @@ public void aGetRequestHasBeenSentForAndForSuperSecureBeneficialOwner(String com HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -521,8 +543,8 @@ public void aGetRequestIsSentForAndForCorporateEntity(String companyNumber, Stri HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -553,8 +575,8 @@ public void aGetRequestHasBeenSentForAndForCorporateEntity(String companyNumber, HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -573,8 +595,8 @@ public void aGetRequestIsSentForAndWithoutERICHeadersForCorporateEntity(String c HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); HttpEntity request = new HttpEntity<>(null, headers); String uri = @@ -641,13 +663,69 @@ public void aPSCExistsFor(String dataFile, String companyNumber) throws JsonProc assertThat(companyPscRepository.findById(NOTIFICATION_ID)).isNotEmpty(); } + @And("a PSC {string} exists for {string} for Individual with {string}") + public void pscExistsWithDeltaAt(String dataFile, String companyNumber, String deltaAt) throws JsonProcessingException { + String pscDataFile = FileReaderUtil.readFile("src/itest/resources/json/input/" + dataFile + ".json"); + PscData pscData = objectMapper.readValue(pscDataFile, PscData.class); + PscSensitiveData pscSensitiveData = objectMapper.readValue(pscDataFile, PscSensitiveData.class); + PscDocument document = new PscDocument(); + + document.setId(NOTIFICATION_ID); + document.setCompanyNumber(companyNumber); + document.setPscId(NOTIFICATION_ID); + document.setDeltaAt("20231120084745378000"); + pscData.setEtag("string"); + pscData.setCeasedOn(LocalDate.from(LocalDateTime.now())); + pscData.setKind("individual-person-with-significant-control"); + pscData.setCountryOfResidence("United Kingdom"); + pscData.setName(companyNumber); + NameElements nameElements = new NameElements(); + nameElements.setTitle("Mr"); + nameElements.setForename("PHIL"); + nameElements.setMiddleName("tom"); + nameElements.setSurname("JONES"); + pscData.setNameElements(nameElements); + DateOfBirth dateOfBirth = new DateOfBirth(); + dateOfBirth.setDay(2); + dateOfBirth.setMonth(3); + dateOfBirth.setYear(1994); + pscSensitiveData.setDateOfBirth(dateOfBirth); + document.setSensitiveData(pscSensitiveData); + Links links = new Links(); + links.setSelf("/company/" + companyNumber + "/persons-with-significant-control/individual/" + NOTIFICATION_ID); + links.setStatement("string"); + pscData.setLinks(links); + pscData.setNationality("British"); + Address address = new Address(); + address.setAddressLine1("ura_line1"); + address.setAddressLine2("ura_line2"); + address.setCareOf("ura_care_of"); + address.setCountry("United Kingdom"); + address.setLocality("Cardiff"); + address.setPoBox("ura_po"); + address.setPostalCode("CF2 1B6"); + address.setPremises("URA"); + address.setRegion("ura_region"); + pscData.setAddress(address); + pscSensitiveData.setUsualResidentialAddress(address); + pscSensitiveData.setResidentialAddressIsSameAsServiceAddress(true); + List list = new ArrayList<>(); + list.add("part-right-to-share-surplus-assets-75-to-100-percent"); + pscData.setNaturesOfControl(list); + document.setData(pscData); + document.setDeltaAt(deltaAt); + + mongoTemplate.save(document); + assertThat(companyPscRepository.findById(NOTIFICATION_ID)).isNotEmpty(); + } + @When("a Get request is sent for {string} and {string} for Individual") public void aGetRequestIsSentForAnd(String companyNumber, String notification_id) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -664,7 +742,7 @@ public void aGetRequestIsSentForAnd(String companyNumber, String notification_id @When("an {string} Get request is sent for {string} and {string} for Individual Full Record") public void aGetFullRecordRequestIsSentForAnd(final String auth, final String companyNumber, final String notification_id) { final HttpHeaders headers = setupHeaders(!"unauthenticated".equals(auth), "authorized".equals(auth) ? "*": ""); - CucumberContext.CONTEXT.set("contextId", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); final HttpEntity request = new HttpEntity<>(null, headers); @@ -681,7 +759,7 @@ private HttpHeaders setupHeaders(final boolean includeEric, final String keyRole headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - headers.set("x-request-id", this.contextId); + headers.set("x-request-id", CONTEXT_ID); if (includeEric) { headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); @@ -697,8 +775,8 @@ public void aGetFullRecordRequestIsSentWithoutEricHeadersForAnd(final String com headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -743,8 +821,8 @@ public void aGetRequestIsSentForAndWithoutERICHeaders(String companyNumber, Stri HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); HttpEntity request = new HttpEntity<>(null, headers); String uri = "/company/{company_number}/persons-with-significant-control/individual/{notification_id}"; @@ -760,8 +838,8 @@ public void aGetRequestHasBeenSentForAnd(String companyNumber, String notificati HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -807,8 +885,8 @@ public void aGetRequestIsSentForAndForIndividualBeneficialOwner(String companyNu HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -839,8 +917,8 @@ public void aGetRequestHasBeenSentForAndForIndividualBeneficialOwner(String comp HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -859,8 +937,8 @@ public void aGetRequestIsSentForAndWithoutERICHeadersForIndividualBeneficialOwne HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); HttpEntity request = new HttpEntity<>(null, headers); String uri = "/company/{company_number}/persons-with-significant-control/individual-beneficial-owner/{notification_id}"; @@ -909,8 +987,8 @@ public void aGetRequestIsSentForAndForCorporateEntityBeneficialOwner(String comp HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -943,8 +1021,8 @@ public void aGetRequestIsSentForAndWithoutERICHeadersForCorporateEntityBeneficia HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); HttpEntity request = new HttpEntity<>(null, headers); String uri = "/company/{company_number}/persons-with-significant-control/corporate-entity-beneficial-owner/{notification_id}"; @@ -960,8 +1038,8 @@ public void aGetRequestHasBeenSentForAndForCorporateEntityBeneficialOwner(String HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -1002,8 +1080,8 @@ public void aGetRequestIsSentForAndForLegalPerson(String companyNumber, String n HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -1034,8 +1112,8 @@ public void aGetRequestIsSentForAndWithoutERICHeadersForLegalPerson(String compa HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); HttpEntity request = new HttpEntity<>(null, headers); String uri = "/company/{company_number}/persons-with-significant-control/legal-person/{notification_id}"; @@ -1051,8 +1129,8 @@ public void aGetRequestHasBeenSentForAndForLegalPerson(String companyNumber, Str HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -1103,8 +1181,8 @@ public void aGetRequestIsSentForAndForLegalPersonBeneficialOwner(String companyN HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -1137,8 +1215,8 @@ public void aGetRequestIsSentForAndWithoutERICHeadersForLegalPersonBeneficialOwn HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); HttpEntity request = new HttpEntity<>(null, headers); String uri = "/company/{company_number}/persons-with-significant-control/legal-person-beneficial-owner/{notification_id}"; @@ -1154,8 +1232,8 @@ public void aGetRequestHasBeenSentForAndForLegalPersonBeneficialOwner(String com HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -1247,8 +1325,8 @@ public void aGetRequestIsSentForForListSummary(String companyNumber) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -1268,8 +1346,8 @@ public void aGetRequestIsSentForForListSummaryRegisterView(String companyNumber) HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); headers.set("ERIC-Identity", "TEST-IDENTITY"); headers.set("ERIC-Identity-Type", "key"); headers.set("ERIC-Authorised-Key-Roles", "*"); @@ -1301,8 +1379,8 @@ public void aGetRequestIsSentForWithoutERICHeadersForListSummary(String companyN HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - CucumberContext.CONTEXT.set("contextId", this.contextId); - headers.set("x-request-id", this.contextId); + CucumberContext.CONTEXT.set("contextId", CONTEXT_ID); + headers.set("x-request-id", CONTEXT_ID); HttpEntity request = new HttpEntity<>(null, headers); String uri = diff --git a/src/itest/resources/features/delete_psc.feature b/src/itest/resources/features/delete_psc.feature index 8ab70b8c..61a52f39 100644 --- a/src/itest/resources/features/delete_psc.feature +++ b/src/itest/resources/features/delete_psc.feature @@ -21,10 +21,11 @@ Feature: Delete PSC | company_number | | 34777772 | - Scenario Outline: Delete PSC unsuccessfully - PSC resource does not exist + Scenario Outline: Delete PSC unsuccessfully but chs-kafka-api invoked - PSC resource does not exist Given a PSC does not exist for "" When a DELETE request is sent for "" - Then I should receive 404 status code + Then the CHS Kafka API is invoked with a DELETE event + And I should receive 200 status code Examples: | company_number | @@ -41,3 +42,14 @@ Feature: Delete PSC Examples: | company_number | | 34777772 | + + Scenario Outline: Delete PSC throws conflict exception if deltaAt is stale + Given Psc data api service is running + And a PSC "" exists for "" for Individual with "" + When a DELETE request is sent for "" with a stale "" + Then I should receive 409 status code + And the CHS Kafka API is not invoked with a DELETE event + + Examples: + | data | company_number | existingDeltaAt | deltaAt | + | get_individual | 34777772 | 20231020084745378999 | 20230724093435661593 | \ No newline at end of file diff --git a/src/main/java/uk/gov/companieshouse/pscdataapi/api/ChsKafkaApiService.java b/src/main/java/uk/gov/companieshouse/pscdataapi/api/ChsKafkaApiService.java index eb68bffd..9aa1e9d5 100644 --- a/src/main/java/uk/gov/companieshouse/pscdataapi/api/ChsKafkaApiService.java +++ b/src/main/java/uk/gov/companieshouse/pscdataapi/api/ChsKafkaApiService.java @@ -7,7 +7,6 @@ import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.HashMap; - import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import uk.gov.companieshouse.api.InternalApiClient; @@ -19,6 +18,7 @@ import uk.gov.companieshouse.logging.Logger; import uk.gov.companieshouse.pscdataapi.exceptions.SerDesException; import uk.gov.companieshouse.pscdataapi.exceptions.ServiceUnavailableException; +import uk.gov.companieshouse.pscdataapi.models.PscDeleteRequest; import uk.gov.companieshouse.pscdataapi.models.PscDocument; import uk.gov.companieshouse.pscdataapi.transform.CompanyPscTransformer; import uk.gov.companieshouse.pscdataapi.util.PscTransformationHelper; @@ -44,7 +44,7 @@ public class ChsKafkaApiService { private String resourceChangedUri; public ChsKafkaApiService(InternalApiClient internalApiClient, Logger logger, - ObjectMapper objectMapper, CompanyPscTransformer companyPscTransformer) { + ObjectMapper objectMapper, CompanyPscTransformer companyPscTransformer) { this.internalApiClient = internalApiClient; this.logger = logger; this.objectMapper = objectMapper; @@ -62,7 +62,7 @@ public ChsKafkaApiService(InternalApiClient internalApiClient, Logger logger, @StreamEvents public ApiResponse invokeChsKafkaApi(String contextId, String companyNumber, - String notificationId, String kind) { + String notificationId, String kind) { internalApiClient.setBasePath(chsKafkaApiUrl); PrivateChangedResourcePost changedResourcePost = internalApiClient .privateChangedResourceHandler().postChangedResource(resourceChangedUri, @@ -74,28 +74,22 @@ public ApiResponse invokeChsKafkaApi(String contextId, String companyNumbe /** * Creates a ChangedResource object to send a delete request to the chs kafka api. * - * @param contextId chs kafka id - * @param companyNumber company number of psc - * @param notificationId mongo id * @return passes request to api response handling */ @StreamEvents - public ApiResponse invokeChsKafkaApiWithDeleteEvent(String contextId, - String companyNumber, - String notificationId, - String kind, PscDocument pscDocument) { + public ApiResponse invokeChsKafkaApiWithDeleteEvent(PscDeleteRequest deleteRequest, PscDocument pscDocument) { + internalApiClient.setBasePath(chsKafkaApiUrl); PrivateChangedResourcePost changedResourcePost = internalApiClient.privateChangedResourceHandler() .postChangedResource(resourceChangedUri, - mapChangedResource(contextId, companyNumber, - notificationId, kind, true, pscDocument)); + mapChangedResource(deleteRequest.contextId(), deleteRequest.companyNumber(), + deleteRequest.notificationId(), deleteRequest.kind(), true, pscDocument)); return handleApiCall(changedResourcePost); } - private ChangedResource mapChangedResource(String contextId, String companyNumber, - String notificationId, - String kind, boolean isDelete, PscDocument pscDocument) { + private ChangedResource mapChangedResource(String contextId, String companyNumber, String notificationId, + String kind, boolean isDelete, PscDocument pscDocument) { ChangedResourceEvent event = new ChangedResourceEvent(); ChangedResource changedResource = new ChangedResource(); event.setPublishedAt(PUBLISHED_AT_FORMAT.format(Instant.now())); diff --git a/src/main/java/uk/gov/companieshouse/pscdataapi/config/ExceptionHandlerConfig.java b/src/main/java/uk/gov/companieshouse/pscdataapi/config/ExceptionHandlerConfig.java index 35527897..67561010 100644 --- a/src/main/java/uk/gov/companieshouse/pscdataapi/config/ExceptionHandlerConfig.java +++ b/src/main/java/uk/gov/companieshouse/pscdataapi/config/ExceptionHandlerConfig.java @@ -16,6 +16,7 @@ import org.springframework.web.context.request.WebRequest; import uk.gov.companieshouse.logging.Logger; import uk.gov.companieshouse.pscdataapi.exceptions.BadRequestException; +import uk.gov.companieshouse.pscdataapi.exceptions.ConflictException; import uk.gov.companieshouse.pscdataapi.exceptions.MethodNotAllowedException; import uk.gov.companieshouse.pscdataapi.exceptions.SerDesException; import uk.gov.companieshouse.pscdataapi.exceptions.ServiceUnavailableException; @@ -133,4 +134,24 @@ public ResponseEntity handleBadRequestException(Exception ex, WebRequest request.setAttribute(EXCEPTION_ATTRIBUTE, ex, 0); return new ResponseEntity<>(responseBody, HttpStatus.BAD_REQUEST); } + + + /** + * Conflict exception handler. + * Thrown when data is given in the wrong format. + * + * @param ex exception to handle. + * @param request request. + * @return error response to return. + */ + @ExceptionHandler(value = {ConflictException.class}) + public ResponseEntity handleConflictException(Exception ex, WebRequest request) { + logger.error(String.format("Conflict, response code: %s", HttpStatus.CONFLICT), ex); + + Map responseBody = new LinkedHashMap<>(); + responseBody.put(TIMESTAMP, LocalDateTime.now()); + responseBody.put(MESSAGE, "Conflict."); + request.setAttribute(EXCEPTION_ATTRIBUTE, ex, 0); + return new ResponseEntity<>(responseBody, HttpStatus.CONFLICT); + } } diff --git a/src/main/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscController.java b/src/main/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscController.java index 5f8f7d1d..b48ccbcd 100644 --- a/src/main/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscController.java +++ b/src/main/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscController.java @@ -28,6 +28,7 @@ import uk.gov.companieshouse.pscdataapi.exceptions.ResourceNotFoundException; import uk.gov.companieshouse.pscdataapi.exceptions.ServiceUnavailableException; import uk.gov.companieshouse.pscdataapi.logging.DataMapHolder; +import uk.gov.companieshouse.pscdataapi.models.PscDeleteRequest; import uk.gov.companieshouse.pscdataapi.service.CompanyPscService; @@ -88,14 +89,21 @@ public ResponseEntity submitPscData(@RequestHeader("x-request-id") String public ResponseEntity deletePscData( @PathVariable("company_number") String companyNumber, @PathVariable("notification_id") String notificationId, - @RequestHeader("x-request-id") String contextId) { + @RequestHeader("x-request-id") String contextId, + @RequestHeader("x-kind") String kind, + @RequestHeader("x-delta-at") String deltaAt) { DataMapHolder.get() .companyNumber(companyNumber) .itemId(notificationId); LOGGER.info(String.format("Deleting PSC data with company number %s", companyNumber), DataMapHolder.getLogMap()); try { - pscService.deletePsc(companyNumber, notificationId, contextId); + pscService.deletePsc(PscDeleteRequest.builder() + .companyNumber(companyNumber) + .notificationId(notificationId) + .contextId(contextId).kind(kind) + .deltaAt(deltaAt) + .build()); LOGGER.info(String.format("Successfully deleted PSC with company number %s", companyNumber), DataMapHolder.getLogMap()); return ResponseEntity.status(HttpStatus.OK).build(); diff --git a/src/main/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscFullRecordGetController.java b/src/main/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscFullRecordGetController.java index 6f4dee18..234f115b 100644 --- a/src/main/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscFullRecordGetController.java +++ b/src/main/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscFullRecordGetController.java @@ -1,7 +1,6 @@ package uk.gov.companieshouse.pscdataapi.controller; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.dao.DataAccessException; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; diff --git a/src/main/java/uk/gov/companieshouse/pscdataapi/exceptions/ConflictException.java b/src/main/java/uk/gov/companieshouse/pscdataapi/exceptions/ConflictException.java new file mode 100644 index 00000000..9a21a85b --- /dev/null +++ b/src/main/java/uk/gov/companieshouse/pscdataapi/exceptions/ConflictException.java @@ -0,0 +1,8 @@ +package uk.gov.companieshouse.pscdataapi.exceptions; + +public class ConflictException extends RuntimeException { + + public ConflictException(String message) { + super(message); + } +} diff --git a/src/main/java/uk/gov/companieshouse/pscdataapi/models/PscDeleteRequest.java b/src/main/java/uk/gov/companieshouse/pscdataapi/models/PscDeleteRequest.java new file mode 100644 index 00000000..6a337130 --- /dev/null +++ b/src/main/java/uk/gov/companieshouse/pscdataapi/models/PscDeleteRequest.java @@ -0,0 +1,47 @@ +package uk.gov.companieshouse.pscdataapi.models; + +public record PscDeleteRequest (String companyNumber, String notificationId, String contextId, String kind, String deltaAt) { + + public static Builder builder() { return new Builder(); } + + public static final class Builder { + + private String companyNumber; + private String notificationId; + private String contextId; + private String kind; + private String deltaAt; + + private Builder() { + } + + public Builder companyNumber(String companyNumber) { + this.companyNumber = companyNumber; + return this; + } + + public Builder notificationId(String notificationId) { + this.notificationId = notificationId; + return this; + } + + public Builder contextId(String contextId) { + this.contextId = contextId; + return this; + } + + public Builder kind(String kind) { + this.kind = kind; + return this; + } + + public Builder deltaAt(String deltaAt) { + this.deltaAt = deltaAt; + return this; + } + + public PscDeleteRequest build() { + return new PscDeleteRequest(companyNumber, notificationId, contextId, kind, deltaAt); + } + } +} diff --git a/src/main/java/uk/gov/companieshouse/pscdataapi/service/CompanyPscService.java b/src/main/java/uk/gov/companieshouse/pscdataapi/service/CompanyPscService.java index 17b606d3..89dea714 100644 --- a/src/main/java/uk/gov/companieshouse/pscdataapi/service/CompanyPscService.java +++ b/src/main/java/uk/gov/companieshouse/pscdataapi/service/CompanyPscService.java @@ -1,5 +1,7 @@ package uk.gov.companieshouse.pscdataapi.service; +import static uk.gov.companieshouse.pscdataapi.util.DateUtils.isDeltaStale; + import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; @@ -10,23 +12,35 @@ import java.util.Optional; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import uk.gov.companieshouse.api.api.CompanyExemptionsApiService; import uk.gov.companieshouse.api.api.CompanyMetricsApiService; import uk.gov.companieshouse.api.exemptions.CompanyExemptions; import uk.gov.companieshouse.api.metrics.MetricsApi; import uk.gov.companieshouse.api.metrics.RegisterApi; import uk.gov.companieshouse.api.metrics.RegistersApi; -import uk.gov.companieshouse.api.psc.*; +import uk.gov.companieshouse.api.psc.CorporateEntity; +import uk.gov.companieshouse.api.psc.CorporateEntityBeneficialOwner; +import uk.gov.companieshouse.api.psc.FullRecordCompanyPSCApi; +import uk.gov.companieshouse.api.psc.Individual; +import uk.gov.companieshouse.api.psc.IndividualBeneficialOwner; +import uk.gov.companieshouse.api.psc.IndividualFullRecord; +import uk.gov.companieshouse.api.psc.LegalPerson; +import uk.gov.companieshouse.api.psc.LegalPersonBeneficialOwner; +import uk.gov.companieshouse.api.psc.ListSummary; +import uk.gov.companieshouse.api.psc.PscList; +import uk.gov.companieshouse.api.psc.SuperSecure; +import uk.gov.companieshouse.api.psc.SuperSecureBeneficialOwner; import uk.gov.companieshouse.logging.Logger; import uk.gov.companieshouse.pscdataapi.api.ChsKafkaApiService; import uk.gov.companieshouse.pscdataapi.exceptions.BadRequestException; +import uk.gov.companieshouse.pscdataapi.exceptions.ConflictException; import uk.gov.companieshouse.pscdataapi.exceptions.ResourceNotFoundException; import uk.gov.companieshouse.pscdataapi.exceptions.ServiceUnavailableException; import uk.gov.companieshouse.pscdataapi.logging.DataMapHolder; import uk.gov.companieshouse.pscdataapi.models.Created; import uk.gov.companieshouse.pscdataapi.models.Links; import uk.gov.companieshouse.pscdataapi.models.PscData; +import uk.gov.companieshouse.pscdataapi.models.PscDeleteRequest; import uk.gov.companieshouse.pscdataapi.models.PscDocument; import uk.gov.companieshouse.pscdataapi.repository.CompanyPscRepository; import uk.gov.companieshouse.pscdataapi.transform.CompanyPscTransformer; @@ -37,8 +51,6 @@ public class CompanyPscService { private static final String NOT_ON_PUBLIC_REGISTER = "not-on-public-register"; private static final String UNEXPECTED_ERROR_OCCURRED_WHILE_FETCHING_PSC_DOCUMENT = "Unexpected error occurred while fetching PSC document"; - private static final String RESOURCE_NOT_FOUND_FOR_COMPANY_NUMBER = - "Resource not found for company number: %s"; public static final String COMPANY_NOT_ON_PUBLIC_REGISTER = "company %s not on public register"; private final DateTimeFormatter dateTimeFormatter = @@ -144,36 +156,24 @@ private Created getCreatedFromCurrentRecord(String notificationId) { } } - private PscDocument getPscDocument(String companyNumber, String notificationId) - throws ResourceNotFoundException { - Optional pscDocument = - repository.getPscByCompanyNumberAndId(companyNumber, notificationId); - return pscDocument.orElseThrow(() -> - new ResourceNotFoundException(HttpStatus.NOT_FOUND, String.format( - RESOURCE_NOT_FOUND_FOR_COMPANY_NUMBER, companyNumber))); - } - - /** - * Delete PSC record. - * - * @param companyNumber Company number. - * @param notificationId Mongo Id. - */ - @Transactional - public void deletePsc(String companyNumber, String notificationId, String contextId) + public void deletePsc(PscDeleteRequest deleteRequest) throws ResourceNotFoundException, ServiceUnavailableException { - PscDocument pscDocument = getPscDocument(companyNumber, notificationId); - String kind = pscDocument.getData().getKind(); - repository.delete(pscDocument); - try { - chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent(contextId, - companyNumber, notificationId, kind, pscDocument); - } catch (Exception exception) { - throw new ServiceUnavailableException(exception.getMessage()); + logger.info(String.format("Deleting PSC record with company number %s", + deleteRequest.companyNumber()), DataMapHolder.getLogMap()); + + Optional pscDocument = repository.getPscByCompanyNumberAndId(deleteRequest.companyNumber(), + deleteRequest.notificationId()); + PscDocument document = null; + if (pscDocument.isPresent()) { + document = pscDocument.get(); + deltaAtCheck(deleteRequest.deltaAt(), document); + repository.delete(document); + chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent(deleteRequest, document); + } else { + logger.info("No document to delete, calling resource-changed with empty deleted data", + DataMapHolder.getLogMap()); + chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent(deleteRequest, document); } - - logger.info(String.format("PSC record with company number %s has been deleted", - companyNumber), DataMapHolder.getLogMap()); } /** @@ -687,4 +687,11 @@ private boolean hasActivePscExemptions(String companyNumber) { .anyMatch(e -> e.getExemptTo()==null)))).isPresent(); } + private void deltaAtCheck(String requestDeltaAt, PscDocument document) { + if (isDeltaStale(requestDeltaAt, document.getDeltaAt())) { + logger.error("Stale delta received; request delta_at: [%s] is not after existing delta_at: [%s]".formatted( + requestDeltaAt, document.getDeltaAt()), DataMapHolder.getLogMap()); + throw new ConflictException("Stale delta for delete"); + } + } } diff --git a/src/main/java/uk/gov/companieshouse/pscdataapi/transform/CompanyPscTransformer.java b/src/main/java/uk/gov/companieshouse/pscdataapi/transform/CompanyPscTransformer.java index a77a8992..2268c235 100644 --- a/src/main/java/uk/gov/companieshouse/pscdataapi/transform/CompanyPscTransformer.java +++ b/src/main/java/uk/gov/companieshouse/pscdataapi/transform/CompanyPscTransformer.java @@ -5,15 +5,36 @@ import java.util.List; import org.springframework.stereotype.Component; -import uk.gov.companieshouse.api.psc.*; +import uk.gov.companieshouse.api.psc.Identification; +import uk.gov.companieshouse.api.psc.CorporateEntity; +import uk.gov.companieshouse.api.psc.CorporateEntityBeneficialOwner; +import uk.gov.companieshouse.api.psc.Data; +import uk.gov.companieshouse.api.psc.ExternalData; +import uk.gov.companieshouse.api.psc.FullRecordCompanyPSCApi; +import uk.gov.companieshouse.api.psc.Individual; +import uk.gov.companieshouse.api.psc.IndividualBeneficialOwner; +import uk.gov.companieshouse.api.psc.IndividualFullRecord; +import uk.gov.companieshouse.api.psc.InternalData; +import uk.gov.companieshouse.api.psc.ItemLinkTypes; +import uk.gov.companieshouse.api.psc.LegalPerson; +import uk.gov.companieshouse.api.psc.LegalPersonBeneficialOwner; +import uk.gov.companieshouse.api.psc.ListSummary; +import uk.gov.companieshouse.api.psc.SensitiveData; +import uk.gov.companieshouse.api.psc.SuperSecure; +import uk.gov.companieshouse.api.psc.SuperSecureBeneficialOwner; import uk.gov.companieshouse.logging.Logger; import uk.gov.companieshouse.pscdataapi.data.IndividualPscRoles; import uk.gov.companieshouse.pscdataapi.data.SecurePscRoles; import uk.gov.companieshouse.pscdataapi.logging.DataMapHolder; import uk.gov.companieshouse.pscdataapi.models.Address; import uk.gov.companieshouse.pscdataapi.models.DateOfBirth; +import uk.gov.companieshouse.pscdataapi.models.Links; import uk.gov.companieshouse.pscdataapi.models.NameElements; -import uk.gov.companieshouse.pscdataapi.models.*; +import uk.gov.companieshouse.pscdataapi.models.PscData; +import uk.gov.companieshouse.pscdataapi.models.PscDocument; +import uk.gov.companieshouse.pscdataapi.models.PscIdentification; +import uk.gov.companieshouse.pscdataapi.models.PscSensitiveData; +import uk.gov.companieshouse.pscdataapi.models.Updated; import uk.gov.companieshouse.pscdataapi.util.PscTransformationHelper; @Component diff --git a/src/main/java/uk/gov/companieshouse/pscdataapi/util/DateUtils.java b/src/main/java/uk/gov/companieshouse/pscdataapi/util/DateUtils.java new file mode 100644 index 00000000..576f7fb8 --- /dev/null +++ b/src/main/java/uk/gov/companieshouse/pscdataapi/util/DateUtils.java @@ -0,0 +1,21 @@ +package uk.gov.companieshouse.pscdataapi.util; + +import static java.time.ZoneOffset.UTC; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import org.apache.commons.lang.StringUtils; + +public final class DateUtils { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSSSSS") + .withZone(UTC); + + private DateUtils() { + } + + public static boolean isDeltaStale(final String requestDeltaAt, final String existingDeltaAt) { + return StringUtils.isNotBlank(existingDeltaAt) && OffsetDateTime.parse(requestDeltaAt, FORMATTER) + .isBefore(OffsetDateTime.parse(existingDeltaAt, FORMATTER)); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/companieshouse/pscdataapi/api/ChsKafkaApiServiceTest.java b/src/test/java/uk/gov/companieshouse/pscdataapi/api/ChsKafkaApiServiceTest.java index 789f341c..2dd6d431 100644 --- a/src/test/java/uk/gov/companieshouse/pscdataapi/api/ChsKafkaApiServiceTest.java +++ b/src/test/java/uk/gov/companieshouse/pscdataapi/api/ChsKafkaApiServiceTest.java @@ -2,11 +2,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static uk.gov.companieshouse.pscdataapi.util.TestHelper.COMPANY_NUMBER; +import static uk.gov.companieshouse.pscdataapi.util.TestHelper.NOTIFICATION_ID; +import static uk.gov.companieshouse.pscdataapi.util.TestHelper.X_REQUEST_ID; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -16,6 +20,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; @@ -27,9 +33,17 @@ import uk.gov.companieshouse.api.handler.chskafka.PrivateChangedResourceHandler; import uk.gov.companieshouse.api.handler.chskafka.request.PrivateChangedResourcePost; import uk.gov.companieshouse.api.model.ApiResponse; -import uk.gov.companieshouse.api.psc.*; +import uk.gov.companieshouse.api.psc.CorporateEntity; +import uk.gov.companieshouse.api.psc.CorporateEntityBeneficialOwner; +import uk.gov.companieshouse.api.psc.Individual; +import uk.gov.companieshouse.api.psc.IndividualBeneficialOwner; +import uk.gov.companieshouse.api.psc.LegalPerson; +import uk.gov.companieshouse.api.psc.LegalPersonBeneficialOwner; +import uk.gov.companieshouse.api.psc.SuperSecure; +import uk.gov.companieshouse.api.psc.SuperSecureBeneficialOwner; import uk.gov.companieshouse.logging.Logger; import uk.gov.companieshouse.pscdataapi.exceptions.ServiceUnavailableException; +import uk.gov.companieshouse.pscdataapi.models.PscDeleteRequest; import uk.gov.companieshouse.pscdataapi.models.PscDocument; import uk.gov.companieshouse.pscdataapi.transform.CompanyPscTransformer; import uk.gov.companieshouse.pscdataapi.util.TestHelper; @@ -39,6 +53,8 @@ class ChsKafkaApiServiceTest { private static final String EVENT_TYPE_CHANGED = "changed"; private static final String EVENT_TYPE_DELETED = "deleted"; + private static final String DELTA_AT = "20240219123045999999"; + private static final String PSC_URI = "/company/%s/persons-with-significant-control/%s/%s"; private static final DateTimeFormatter ROUNDED_TO_SECONDS_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'hh:mm:ss"); @@ -97,7 +113,7 @@ void invokeChsKafkaEndpointWithDeleteForIndividual() throws ApiErrorResponseExce // when ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( - TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.INDIVIDUAL_KIND, + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.INDIVIDUAL_KIND, DELTA_AT), document); assertThat(apiResponse).isNotNull(); @@ -127,7 +143,7 @@ void invokeChsKafkaEndpointWithDeleteForIndividualBeneficialOwner() throws ApiEr // when ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( - TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.INDIVIDUAL_KIND, + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.INDIVIDUAL_KIND, DELTA_AT), document); assertThat(apiResponse).isNotNull(); @@ -157,7 +173,7 @@ void invokeChsKafkaEndpointWithDeleteForLegalPerson() throws ApiErrorResponseExc // when ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( - TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.LEGAL_KIND, + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.LEGAL_KIND, DELTA_AT), document); assertThat(apiResponse).isNotNull(); @@ -190,7 +206,7 @@ void invokeChsKafkaEndpointWithDeleteForLegalPersonBeneficialOwner() throws ApiE // when ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( - TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.LEGAL_BO_KIND, + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.LEGAL_BO_KIND, DELTA_AT), document); assertThat(apiResponse).isNotNull(); @@ -220,7 +236,7 @@ void invokeChsKafkaEndpointWithDeleteForSuperSecure() throws ApiErrorResponseExc // when ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( - TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.SECURE_KIND, + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.SECURE_KIND, DELTA_AT), document); assertThat(apiResponse).isNotNull(); @@ -250,7 +266,7 @@ void invokeChsKafkaEndpointWithDeleteForSuperSecureBO() throws ApiErrorResponseE // when ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( - TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.SECURE_BO_KIND, + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.SECURE_BO_KIND, DELTA_AT), document); assertThat(apiResponse).isNotNull(); @@ -280,7 +296,7 @@ void invokeChsKafkaEndpointWithDeleteForCorporateEntity() throws ApiErrorRespons // when ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( - TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.CORPORATE_KIND, + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.CORPORATE_KIND, DELTA_AT), document); assertThat(apiResponse).isNotNull(); @@ -310,7 +326,7 @@ void invokeChsKafkaEndpointWithDeleteForCorporateEntityBO() throws ApiErrorRespo // when ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( - TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.CORPORATE_BO_KIND, + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, TestHelper.CORPORATE_BO_KIND, DELTA_AT), document); assertThat(apiResponse).isNotNull(); @@ -347,7 +363,7 @@ void invokeChsKafkaEndpointWithDeleteThrowsApiErrorException() throws ApiErrorRe when(privateChangedResourcePost.execute()).thenThrow(exception); Executable executable = () -> chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( - TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "kind", any()); + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "kind", "deltaAt"), any()); assertThrows(ServiceUnavailableException.class, executable); verify(internalApiClient, times(1)).privateChangedResourceHandler(); @@ -381,7 +397,7 @@ void invokeChsKafkaEndpointWithDeleteThrowsRuntimeException() throws ApiErrorRes when(privateChangedResourcePost.execute()).thenThrow(exception); Executable executable = () -> chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( - TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "kind", any()); + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "kind", "deltaAt"), any()); assertThrows(RuntimeException.class, executable); verify(internalApiClient, times(1)).privateChangedResourceHandler(); @@ -389,4 +405,36 @@ void invokeChsKafkaEndpointWithDeleteThrowsRuntimeException() throws ApiErrorRes verify(privateChangedResourcePost, times(1)).execute(); assertThat(changedResourceCaptor.getValue().getEvent().getType()).isEqualTo(EVENT_TYPE_DELETED); } + + @ParameterizedTest + @CsvSource({ + "individual-person-with-significant-control, individual, company-psc-individual", + "legal-person-person-with-significant-control, legal-person, company-psc-legal", + "corporate-entity-person-with-significant-control, corporate-entity, company-psc-corporate", + "super-secure-person-with-significant-control, super-secure, company-psc-supersecure", + "individual-beneficial-owner, individual-beneficial-owner, individual-beneficial-owner", + "legal-person-beneficial-owner, legal-person-beneficial-owner, legal-person-beneficial-owner", + "corporate-entity-beneficial-owner, corporate-entity-beneficial-owner, corporate-entity-beneficial-owner", + "super-secure-beneficial-owner, super-secure-beneficial-owner, super-secure-beneficial-owner" + }) + void invokeChsKafkaApiWithDeleteMongoDocumentAlreadyDeleted (String kind, String expectedUriKind, String expectedResourceKind) throws ApiErrorResponseException { + // given + when(internalApiClient.privateChangedResourceHandler()).thenReturn(privateChangedResourceHandler); + when(privateChangedResourceHandler.postChangedResource(any(), any())).thenReturn(privateChangedResourcePost); + when(privateChangedResourcePost.execute()).thenReturn(response); + PscDeleteRequest deleteRequest = new PscDeleteRequest(COMPANY_NUMBER, NOTIFICATION_ID, X_REQUEST_ID, kind, DELTA_AT); + String expectedUri = PSC_URI.formatted(COMPANY_NUMBER, expectedUriKind, NOTIFICATION_ID); + + // when + chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent(deleteRequest, null); + + // then + verify(internalApiClient, times(1)).privateChangedResourceHandler(); + verify(privateChangedResourceHandler, times(1)).postChangedResource(any(), changedResourceCaptor.capture()); + verify(privateChangedResourcePost, times(1)).execute(); + assertEquals(EVENT_TYPE_DELETED, changedResourceCaptor.getValue().getEvent().getType()); + assertEquals(expectedResourceKind, changedResourceCaptor.getValue().getResourceKind()); + assertEquals(expectedUri, changedResourceCaptor.getValue().getResourceUri()); + + } } diff --git a/src/test/java/uk/gov/companieshouse/pscdataapi/api/ResourceChangedApiServiceAspectFeatureFlagDisabledITest.java b/src/test/java/uk/gov/companieshouse/pscdataapi/api/ResourceChangedApiServiceAspectFeatureFlagDisabledITest.java index 9c9ff49f..d63f9a33 100644 --- a/src/test/java/uk/gov/companieshouse/pscdataapi/api/ResourceChangedApiServiceAspectFeatureFlagDisabledITest.java +++ b/src/test/java/uk/gov/companieshouse/pscdataapi/api/ResourceChangedApiServiceAspectFeatureFlagDisabledITest.java @@ -23,6 +23,7 @@ import uk.gov.companieshouse.api.http.HttpClient; import uk.gov.companieshouse.api.model.ApiResponse; import uk.gov.companieshouse.api.sdk.ApiClientService; +import uk.gov.companieshouse.pscdataapi.models.PscDeleteRequest; import uk.gov.companieshouse.pscdataapi.util.TestHelper; @SpringBootTest @@ -73,7 +74,7 @@ void testThatKafkaApiShouldBeCalledWhenFeatureFlagDisabled() changedResourcePost); when(changedResourcePost.execute()).thenReturn(response); - ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApi(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "kind"); + ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApi(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "individual-person-with-significant-control"); Assertions.assertThat(apiResponse).isNotNull(); @@ -92,7 +93,8 @@ void testThatKafkaApiShouldBeCalledOnDeleteWhenFeatureFlagDisabled() changedResourcePost); when(changedResourcePost.execute()).thenReturn(response); - ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "kind", + ApiResponse apiResponse = chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "individual-person-with-significant-control", "deltaAt" ), TestHelper.buildPscDocument("individual-persons-with-significant-control")); Assertions.assertThat(apiResponse).isNotNull(); diff --git a/src/test/java/uk/gov/companieshouse/pscdataapi/api/ResourceChangedApiServiceAspectFeatureFlagEnabledITest.java b/src/test/java/uk/gov/companieshouse/pscdataapi/api/ResourceChangedApiServiceAspectFeatureFlagEnabledITest.java index 8755ba59..a68133af 100644 --- a/src/test/java/uk/gov/companieshouse/pscdataapi/api/ResourceChangedApiServiceAspectFeatureFlagEnabledITest.java +++ b/src/test/java/uk/gov/companieshouse/pscdataapi/api/ResourceChangedApiServiceAspectFeatureFlagEnabledITest.java @@ -19,6 +19,7 @@ import uk.gov.companieshouse.api.request.RequestExecutor; import uk.gov.companieshouse.api.sdk.ApiClientService; import uk.gov.companieshouse.pscdataapi.exceptions.ServiceUnavailableException; +import uk.gov.companieshouse.pscdataapi.models.PscDeleteRequest; import uk.gov.companieshouse.pscdataapi.util.TestHelper; @SpringBootTest(properties = {"feature.seeding_collection_enabled=true"}) @@ -55,7 +56,7 @@ void testThatAspectShouldNotProceedWhenFeatureFlagEnabled() throws ServiceUnavai changedResourcePost); when(changedResourcePost.execute()).thenReturn(response); - chsKafkaApiService.invokeChsKafkaApi(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "kind"); + chsKafkaApiService.invokeChsKafkaApi(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "individual-person-with-significant-control"); verifyNoInteractions(apiClientService); verifyNoInteractions(internalApiClient); @@ -72,7 +73,8 @@ void testThatAspectShouldNotProceedOnDeleteWhenFeatureFlagEnabled() throws Servi changedResourcePost); when(changedResourcePost.execute()).thenReturn(response); - chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "kind", + chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent( + new PscDeleteRequest(TestHelper.X_REQUEST_ID, TestHelper.COMPANY_NUMBER, TestHelper.NOTIFICATION_ID, "individual-person-with-significant-control", "deltaAt"), TestHelper.buildPscDocument("individual-persons-with-significant-control")); verifyNoInteractions(apiClientService); diff --git a/src/test/java/uk/gov/companieshouse/pscdataapi/config/ExceptionHandlerConfigTest.java b/src/test/java/uk/gov/companieshouse/pscdataapi/config/ExceptionHandlerConfigTest.java index 45b4a9b5..a9b3d8f3 100644 --- a/src/test/java/uk/gov/companieshouse/pscdataapi/config/ExceptionHandlerConfigTest.java +++ b/src/test/java/uk/gov/companieshouse/pscdataapi/config/ExceptionHandlerConfigTest.java @@ -74,4 +74,13 @@ void handleBadRequestException() { assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); assertThat(responseBody.get("message"), is("Bad request.")); } + + @Test + void handleConflictException() { + ResponseEntity response = exceptionHandlerConfigConfig.handleConflictException(new Exception("exception"), request); + Map responseBody = (Map) response.getBody(); + assertThat(response, is(not(nullValue()))); + assertThat(response.getStatusCode(), is(HttpStatus.CONFLICT)); + assertThat(responseBody.get("message"), is("Conflict.")); + } } diff --git a/src/test/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscControllerTest.java b/src/test/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscControllerTest.java index 2a2f5a07..30f4b382 100644 --- a/src/test/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscControllerTest.java +++ b/src/test/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscControllerTest.java @@ -12,11 +12,11 @@ import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static uk.gov.companieshouse.pscdataapi.util.TestHelper.STALE_DELTA_AT; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -28,7 +28,6 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - import uk.gov.companieshouse.api.psc.CorporateEntity; import uk.gov.companieshouse.api.psc.CorporateEntityBeneficialOwner; import uk.gov.companieshouse.api.psc.FullRecordCompanyPSCApi; @@ -39,9 +38,10 @@ import uk.gov.companieshouse.api.psc.PscList; import uk.gov.companieshouse.api.psc.SuperSecure; import uk.gov.companieshouse.api.psc.SuperSecureBeneficialOwner; +import uk.gov.companieshouse.pscdataapi.exceptions.ConflictException; import uk.gov.companieshouse.pscdataapi.exceptions.ResourceNotFoundException; import uk.gov.companieshouse.pscdataapi.exceptions.ServiceUnavailableException; -import uk.gov.companieshouse.pscdataapi.models.PscDocument; +import uk.gov.companieshouse.pscdataapi.models.PscDeleteRequest; import uk.gov.companieshouse.pscdataapi.service.CompanyPscService; import uk.gov.companieshouse.pscdataapi.util.TestHelper; @@ -52,6 +52,8 @@ class CompanyPscControllerTest { private static final String X_REQUEST_ID = "123456"; private static final String MOCK_COMPANY_NUMBER = "1234567"; private static final String MOCK_NOTIFICATION_ID = "123456789"; + private static final String KIND = "individual-person-with-significant-control"; + private static final String DELTA_AT = "20240219123045999999"; private static final Boolean MOCK_REGISTER_VIEW_TRUE = true; private static final Boolean MOCK_REGISTER_VIEW_FALSE = false; private static final String ERIC_IDENTITY = "Test-Identity"; @@ -346,14 +348,15 @@ void deletePSCWhenNoApiKeyPresent() throws Exception { mockMvc.perform(delete(PUT_URL)).andExpect(status().isUnauthorized()); verify(companyPscService - , times(0)).deletePsc(MOCK_COMPANY_NUMBER, MOCK_NOTIFICATION_ID, ""); + , times(0)).deletePsc(new PscDeleteRequest(MOCK_COMPANY_NUMBER, MOCK_NOTIFICATION_ID, "", KIND, DELTA_AT)); } @Test void callPscDeleteRequest() throws Exception { + PscDeleteRequest deleteRequest = new PscDeleteRequest(MOCK_COMPANY_NUMBER, MOCK_NOTIFICATION_ID, X_REQUEST_ID, KIND, DELTA_AT); doNothing() - .when(companyPscService).deletePsc(MOCK_COMPANY_NUMBER, MOCK_NOTIFICATION_ID, X_REQUEST_ID); + .when(companyPscService).deletePsc(deleteRequest); mockMvc.perform(delete(DELETE_URL) .header("ERIC-Identity", ERIC_IDENTITY) @@ -361,17 +364,20 @@ void callPscDeleteRequest() throws Exception { .contentType(APPLICATION_JSON) .header("x-request-id", X_REQUEST_ID) .header("ERIC-Authorised-Key-Roles", ERIC_PRIVILEGES) - .header("ERIC-Authorised-Key-Privileges", ERIC_AUTH)) + .header("ERIC-Authorised-Key-Privileges", ERIC_AUTH) + .header("x-kind", KIND) + .header("x-delta-at", DELTA_AT)) .andExpect(status().isOk()); - verify(companyPscService, times(1)).deletePsc(MOCK_COMPANY_NUMBER, MOCK_NOTIFICATION_ID, X_REQUEST_ID); + verify(companyPscService, times(1)).deletePsc(deleteRequest); } @Test void callPscDeleteRequestAndReturn404() throws Exception { + PscDeleteRequest deleteRequest = new PscDeleteRequest(MOCK_COMPANY_NUMBER, MOCK_NOTIFICATION_ID, X_REQUEST_ID, KIND, DELTA_AT); doThrow(ResourceNotFoundException.class) - .when(companyPscService).deletePsc(MOCK_COMPANY_NUMBER, MOCK_NOTIFICATION_ID, X_REQUEST_ID); + .when(companyPscService).deletePsc(deleteRequest); mockMvc.perform(delete(DELETE_URL) .header("ERIC-Identity", ERIC_IDENTITY) @@ -379,17 +385,41 @@ void callPscDeleteRequestAndReturn404() throws Exception { .contentType(APPLICATION_JSON) .header("x-request-id", X_REQUEST_ID) .header("ERIC-Authorised-Key-Roles", ERIC_PRIVILEGES) - .header("ERIC-Authorised-Key-Privileges", ERIC_AUTH)) + .header("ERIC-Authorised-Key-Privileges", ERIC_AUTH) + .header("x-kind", KIND) + .header("x-delta-at", DELTA_AT)) .andExpect(status().isNotFound()); - verify(companyPscService, times(1)).deletePsc(MOCK_COMPANY_NUMBER, MOCK_NOTIFICATION_ID, X_REQUEST_ID); + verify(companyPscService, times(1)).deletePsc(deleteRequest); + } + + @Test + void callPscDeleteShouldReturn409WhenStaleDeltaAt () throws Exception { + PscDeleteRequest deleteRequest = new PscDeleteRequest(MOCK_COMPANY_NUMBER, MOCK_NOTIFICATION_ID, X_REQUEST_ID, KIND, STALE_DELTA_AT); + + doThrow(ConflictException.class) + .when(companyPscService).deletePsc(deleteRequest); + + mockMvc.perform(delete(DELETE_URL) + .header("ERIC-Identity", ERIC_IDENTITY) + .header("ERIC-Identity-Type", ERIC_IDENTITY_TYPE) + .contentType(APPLICATION_JSON) + .header("x-request-id", X_REQUEST_ID) + .header("ERIC-Authorised-Key-Roles", ERIC_PRIVILEGES) + .header("ERIC-Authorised-Key-Privileges", ERIC_AUTH) + .header("x-kind", KIND) + .header("x-delta-at", STALE_DELTA_AT)) + .andExpect(status().isConflict()); + + verify(companyPscService, times(1)).deletePsc(deleteRequest); } @Test void callPscDeleteRequestWhenServiceUnavailableAndReturn503() throws Exception { + PscDeleteRequest deleteRequest = new PscDeleteRequest(MOCK_COMPANY_NUMBER, MOCK_NOTIFICATION_ID, X_REQUEST_ID, KIND, DELTA_AT); doThrow(ServiceUnavailableException.class) - .when(companyPscService).deletePsc(MOCK_COMPANY_NUMBER, MOCK_NOTIFICATION_ID, X_REQUEST_ID); + .when(companyPscService).deletePsc(deleteRequest); mockMvc.perform(delete(DELETE_URL) .header("ERIC-Identity", ERIC_IDENTITY) diff --git a/src/test/java/uk/gov/companieshouse/pscdataapi/service/CompanyPscServiceTest.java b/src/test/java/uk/gov/companieshouse/pscdataapi/service/CompanyPscServiceTest.java index 69d33d53..4cd61fc7 100644 --- a/src/test/java/uk/gov/companieshouse/pscdataapi/service/CompanyPscServiceTest.java +++ b/src/test/java/uk/gov/companieshouse/pscdataapi/service/CompanyPscServiceTest.java @@ -14,6 +14,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static uk.gov.companieshouse.pscdataapi.util.TestHelper.DELTA_AT; +import static uk.gov.companieshouse.pscdataapi.util.TestHelper.INDIVIDUAL_KIND; +import static uk.gov.companieshouse.pscdataapi.util.TestHelper.STALE_DELTA_AT; import java.time.LocalDateTime; import java.time.OffsetDateTime; @@ -30,7 +33,6 @@ import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.dao.NonTransientDataAccessException; import org.springframework.http.HttpStatus; @@ -58,10 +60,13 @@ import uk.gov.companieshouse.logging.Logger; import uk.gov.companieshouse.pscdataapi.api.ChsKafkaApiService; import uk.gov.companieshouse.pscdataapi.exceptions.BadRequestException; +import uk.gov.companieshouse.pscdataapi.exceptions.ConflictException; import uk.gov.companieshouse.pscdataapi.exceptions.ResourceNotFoundException; +import uk.gov.companieshouse.pscdataapi.exceptions.ServiceUnavailableException; import uk.gov.companieshouse.pscdataapi.models.Created; import uk.gov.companieshouse.pscdataapi.models.Links; import uk.gov.companieshouse.pscdataapi.models.PscData; +import uk.gov.companieshouse.pscdataapi.models.PscDeleteRequest; import uk.gov.companieshouse.pscdataapi.models.PscDocument; import uk.gov.companieshouse.pscdataapi.repository.CompanyPscRepository; import uk.gov.companieshouse.pscdataapi.transform.CompanyPscTransformer; @@ -89,7 +94,6 @@ class CompanyPscServiceTest { CompanyExemptionsApiService companyExemptionsApiService; @Captor private ArgumentCaptor dateCaptor; - @Spy @InjectMocks private CompanyPscService service; @Mock @@ -207,37 +211,76 @@ void insertNewCreatedWhenCreatedCallToMongoFails() { @DisplayName("When company number & notification id is provided, delete PSC") void testDeletePSC() { when(repository.getPscByCompanyNumberAndId(COMPANY_NUMBER, NOTIFICATION_ID)).thenReturn(Optional.ofNullable(pscDocument)); - service.deletePsc(COMPANY_NUMBER, NOTIFICATION_ID, ""); + service.deletePsc(new PscDeleteRequest(COMPANY_NUMBER, NOTIFICATION_ID, "", INDIVIDUAL_KIND, DELTA_AT)); verify(repository, times(1)).getPscByCompanyNumberAndId(COMPANY_NUMBER, NOTIFICATION_ID); verify(repository, times(1)).delete(pscDocument); - verify(chsKafkaApiService).invokeChsKafkaApiWithDeleteEvent(any(), any(), any(), any(), any()); + verify(chsKafkaApiService).invokeChsKafkaApiWithDeleteEvent(any(), any()); } @Test - @DisplayName("When company number is null throw ResourceNotFound Exception") - void testDeletePSCThrowsResourceNotFoundException() { - when(repository.getPscByCompanyNumberAndId("", NOTIFICATION_ID)).thenReturn(Optional.empty()); + @DisplayName("When company number is null throw Bad Request Exception") + void testDeletePSCThrowsResourceBadRequestException() { + when(repository.getPscByCompanyNumberAndId("", NOTIFICATION_ID)).thenThrow(BadRequestException.class); - assertThrows(ResourceNotFoundException.class, () -> service.deletePsc("", NOTIFICATION_ID, "")); + assertThrows(BadRequestException.class, () -> service.deletePsc(new PscDeleteRequest("", NOTIFICATION_ID, "", INDIVIDUAL_KIND, DELTA_AT))); verify(repository, times(1)).getPscByCompanyNumberAndId("", NOTIFICATION_ID); verify(repository, never()).delete(any()); - verify(chsKafkaApiService, never()).invokeChsKafkaApiWithDeleteEvent(any(), any(), any(), any(), any()); + verify(chsKafkaApiService, never()).invokeChsKafkaApiWithDeleteEvent(any(), any()); } @Test - @DisplayName("When company number and id is null throw ResourceNotFound Exception") - void testDeletePSCThrowsNotFoundExceptionWhenCompanyNumberAndNotificationIdIsNull() { - when(repository.getPscByCompanyNumberAndId("", "")).thenReturn(Optional.empty()); + @DisplayName("When company number and id is null throw BadRequestException") + void testDeletePSCThrowsBadRequestExceptionWhenCompanyNumberAndNotificationIdIsNull() { + when(repository.getPscByCompanyNumberAndId("", "")).thenThrow(BadRequestException.class); - assertThrows(ResourceNotFoundException.class, () -> service.deletePsc("", "", "")); + assertThrows(BadRequestException.class, () -> service.deletePsc(new PscDeleteRequest("", "", "", INDIVIDUAL_KIND, DELTA_AT))); verify(repository, times(1)).getPscByCompanyNumberAndId("", ""); verify(repository, never()).delete(any()); - verify(chsKafkaApiService, never()).invokeChsKafkaApiWithDeleteEvent(any(), any(), any(), any(), any()); + verify(chsKafkaApiService, never()).invokeChsKafkaApiWithDeleteEvent(any(), any()); } + @Test + @DisplayName("When Kafka notification fails throw ServiceUnavailableException") + void testDeletePSCThrowsServiceUnavailableExceptionWhenKafkaNotification() { + when(repository.getPscByCompanyNumberAndId(COMPANY_NUMBER, NOTIFICATION_ID)).thenReturn(Optional.of(pscDocument)); + when(chsKafkaApiService.invokeChsKafkaApiWithDeleteEvent(any(), any())) + .thenThrow(new ServiceUnavailableException("message")); + + assertThrows(ServiceUnavailableException.class, () -> service.deletePsc(new PscDeleteRequest(COMPANY_NUMBER, NOTIFICATION_ID, "", INDIVIDUAL_KIND, DELTA_AT))); + + verify(repository).getPscByCompanyNumberAndId(COMPANY_NUMBER, NOTIFICATION_ID); + verify(repository).delete(pscDocument); + verify(chsKafkaApiService).invokeChsKafkaApiWithDeleteEvent(any(), any()); + } + + @Test + @DisplayName("Kafka notification succeeds on retry and after document deleted") + void testKafkaNotificationSucceedsOnRetryAfterDocumentDeleted() { + when(repository.getPscByCompanyNumberAndId(COMPANY_NUMBER, NOTIFICATION_ID)).thenReturn(Optional.empty()); + + service.deletePsc(new PscDeleteRequest(COMPANY_NUMBER, NOTIFICATION_ID, "", INDIVIDUAL_KIND, DELTA_AT)); + + verify(repository).getPscByCompanyNumberAndId(COMPANY_NUMBER, NOTIFICATION_ID); + verify(chsKafkaApiService).invokeChsKafkaApiWithDeleteEvent(any(), any()); + + } + + @Test + void deleteIndividualFullRecordThrowsConflictWhenDeltaAtCheckFails() { + PscDocument document = new PscDocument(); + document.setDeltaAt(DELTA_AT); + when(repository.getPscByCompanyNumberAndId(COMPANY_NUMBER, NOTIFICATION_ID)).thenReturn(Optional.of(document)); + + assertThrows(ConflictException.class, () -> service.deletePsc(new PscDeleteRequest(COMPANY_NUMBER, NOTIFICATION_ID, "", INDIVIDUAL_KIND, STALE_DELTA_AT))); + + verify(repository).getPscByCompanyNumberAndId(COMPANY_NUMBER, NOTIFICATION_ID); + verify(chsKafkaApiService, never()).invokeChsKafkaApiWithDeleteEvent(any(), any()); + } + + @Test void GetIndividualPscReturns404WhenRegisterViewIsTrueAndNoMetrics() { when(repository.getPscByCompanyNumberAndId(COMPANY_NUMBER, NOTIFICATION_ID)) @@ -743,7 +786,7 @@ void whenNoMetricsDataFoundForCompanyInRegisterViewShouldReturnEmptyList() throw } @Test - void whenCompanyNotInPublicRegisterGetPSCListShouldThrow() throws ResourceNotFoundException { + void whenCompanyNotInPublicRegisterGetPSCListShouldThrowNotFound() throws ResourceNotFoundException { MetricsApi metricsApi = TestHelper.createMetrics(); RegistersApi registersApi = new RegistersApi(); metricsApi.setRegisters(registersApi); @@ -757,7 +800,6 @@ void whenCompanyNotInPublicRegisterGetPSCListShouldThrow() throws ResourceNotFou String actualMessage = ex.getMessage(); assertNotNull(actualMessage); assertTrue(actualMessage.contains(expectedMessage)); - verify(service, times(1)).retrievePscListSummaryFromDb(COMPANY_NUMBER, 0, true, 25); verify(repository, times(0)).getListSummaryRegisterView(COMPANY_NUMBER, 0, OffsetDateTime.parse("2020-12-20T06:00Z"), 25); } @@ -880,5 +922,4 @@ void getIndividualFullRecordShouldThrowWhenTransformFails() { assertThat(exception.getStatusCode(), is(HttpStatus.NOT_FOUND)); assertThat(exception.getReason(), is("Failed to transform PSCDocument to Individual Full Record")); } - } \ No newline at end of file diff --git a/src/test/java/uk/gov/companieshouse/pscdataapi/util/TestHelper.java b/src/test/java/uk/gov/companieshouse/pscdataapi/util/TestHelper.java index 93a62f77..6e409f98 100644 --- a/src/test/java/uk/gov/companieshouse/pscdataapi/util/TestHelper.java +++ b/src/test/java/uk/gov/companieshouse/pscdataapi/util/TestHelper.java @@ -12,7 +12,11 @@ import java.util.Collections; import java.util.List; import org.springframework.util.FileCopyUtils; -import uk.gov.companieshouse.api.exemptions.*; +import uk.gov.companieshouse.api.exemptions.CompanyExemptions; +import uk.gov.companieshouse.api.exemptions.ExemptionItem; +import uk.gov.companieshouse.api.exemptions.Exemptions; +import uk.gov.companieshouse.api.exemptions.PscExemptAsTradingOnRegulatedMarketItem; +import uk.gov.companieshouse.api.exemptions.PscExemptAsTradingOnUkRegulatedMarketItem; import uk.gov.companieshouse.api.metrics.CountsApi; import uk.gov.companieshouse.api.metrics.MetricsApi; import uk.gov.companieshouse.api.metrics.PscApi; @@ -58,6 +62,8 @@ public class TestHelper { public static final String LEGAL_BO_KIND = LegalPersonBeneficialOwner.KindEnum.LEGAL_PERSON_BENEFICIAL_OWNER.toString(); public static final String SECURE_BO_KIND = SuperSecureBeneficialOwner.KindEnum.SUPER_SECURE_BENEFICIAL_OWNER.toString(); + public static final String DELTA_AT = "20240219123045999999"; + public static final String STALE_DELTA_AT = "20240119123045999999"; public static final String COMPANY_NUMBER = "companyNumber"; public static final String NOTIFICATION_ID = "notificationId"; public static final String PSC_ID = "pscId";