Repository: companieshouse/limited-partnerships-api
Pull request #135: LP-717 Add 'validation-status' endpoint and link for Limited Partner
Author: login: mwestacott
Created: 2025-05-08T12:34:43Z | Merged: 2025-05-14T08:03:25Z

### JIRA link

https://companieshouse.atlassian.net/browse/LP-717

### Change description

* 'validation_status' resource link now added to transaction
* New controller endpoint added
* Java Bean validation applied by service
* Hard-coded mandatory field error checks also added for addresses
* Unit-tests added

### Work checklist

* [ ] Bug fix
* [X] New feature
* [ ] Breaking change
* [ ] Infrastructure change

### Pull request checklist

* [X] I have added unit tests for new code that I have added
* [ ] I have added/updated functional tests where appropriate
* [ ] The code follows our [coding standards](https://github.com/companieshouse/styleguides/blob/master/java.md)

__An exhaustive list of peer review checks can be read [here](https://github.com/companieshouse/styleguides/blob/master/java_review.md#developer-actions-prior-to-code-commitreview-started)__
### Merge instructions

We are committed to keeping commit history clean, consistent and linear. To achieve this commit should be structured as follows:

```
<type>[optional scope]: <description>
```

and contain the following structural elements:

- fix: a commit that patches a bug in your codebase (this correlates with PATCH in semantic versioning),
- feat: a commit that introduces a new feature to the codebase (this correlates with MINOR in semantic versioning),
- BREAKING CHANGE: a commit that has a footer `BREAKING CHANGE:` introduces a breaking API change (correlating with MAJOR in semantic versioning). A BREAKING CHANGE can be part of commits of any type,
- types other than `fix:` and `feat:` are allowed, for example `build:`, `chore:`, `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others,
- footers other than `BREAKING CHANGE: <description>` may be provided.


Diff:
diff --git a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/GeneralPartnerController.java b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/GeneralPartnerController.java
index e9f99422..9742c08a 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/GeneralPartnerController.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/GeneralPartnerController.java
@@ -125,7 +125,7 @@ public ResponseEntity<ValidationStatusResponse> getValidationStatus(@RequestAttr
             ApiLogger.errorContext(requestId, String.format("Validation errors: %s",
                     new GsonBuilder().create().toJson(validationErrors)), null, logMap);
             validationStatus.setValid(false);
-            validationStatus.setValidationStatusError(validationErrors.toArray(new ValidationStatusError[0]));
+            validationStatus.setValidationStatusError(validationErrors.toArray(ValidationStatusError[]::new));
         }
 
         return ResponseEntity.ok().body(validationStatus);
diff --git a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerController.java b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerController.java
index cf3571ad..519b1d1b 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerController.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerController.java
@@ -1,5 +1,6 @@
 package uk.gov.companieshouse.limitedpartnershipsapi.controller;
 
+import com.google.gson.GsonBuilder;
 import jakarta.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
@@ -16,6 +17,8 @@
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import uk.gov.companieshouse.api.model.transaction.Transaction;
+import uk.gov.companieshouse.api.model.validationstatus.ValidationStatusError;
+import uk.gov.companieshouse.api.model.validationstatus.ValidationStatusResponse;
 import uk.gov.companieshouse.limitedpartnershipsapi.exception.ResourceNotFoundException;
 import uk.gov.companieshouse.limitedpartnershipsapi.exception.ServiceException;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dto.LimitedPartnerDataDto;
@@ -137,4 +140,30 @@ public ResponseEntity<Object> deleteLimitedPartner(@RequestAttribute(TRANSACTION
 
         return new ResponseEntity<>(HttpStatus.NO_CONTENT);
     }
+
+    @GetMapping("/limited-partner/{" + URL_PARAM_LIMITED_PARTNER_ID + "}/validation-status")
+    public ResponseEntity<ValidationStatusResponse> getValidationStatus(@RequestAttribute(TRANSACTION_KEY) Transaction transaction,
+                                                                        @PathVariable(URL_PARAM_LIMITED_PARTNER_ID) String limitedPartnerId,
+                                                                        @RequestHeader(value = ERIC_REQUEST_ID_KEY) String requestId)
+            throws ServiceException {
+
+        var logMap = new HashMap<String, Object>();
+        logMap.put(URL_PARAM_TRANSACTION_ID, transaction.getId());
+        logMap.put(URL_PARAM_LIMITED_PARTNER_ID, limitedPartnerId);
+
+        ApiLogger.infoContext(requestId, "Calling service to validate a Limited Partner", logMap);
+        var validationStatus = new ValidationStatusResponse();
+        validationStatus.setValid(true);
+
+        var validationErrors = limitedPartnerService.validateLimitedPartner(transaction, limitedPartnerId);
+
+        if (!validationErrors.isEmpty()) {
+            ApiLogger.errorContext(requestId, String.format("Validation errors: %s",
+                    new GsonBuilder().create().toJson(validationErrors)), null, logMap);
+            validationStatus.setValid(false);
+            validationStatus.setValidationStatusError(validationErrors.toArray(ValidationStatusError[]::new));
+        }
+
+        return ResponseEntity.ok().body(validationStatus);
+    }
 }
diff --git a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/PartnershipController.java b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/PartnershipController.java
index 296d614e..372b5a25 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/PartnershipController.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/PartnershipController.java
@@ -136,7 +136,7 @@ public ResponseEntity<ValidationStatusResponse> getValidationStatus(@RequestAttr
                 ApiLogger.errorContext(requestId, String.format("Validation errors: %s",
                         new GsonBuilder().create().toJson(validationErrors)), null, logMap);
                 validationStatus.setValid(false);
-                validationStatus.setValidationStatusError(validationErrors.toArray(new ValidationStatusError[0]));
+                validationStatus.setValidationStatusError(validationErrors.toArray(ValidationStatusError[]::new));
             }
 
             return ResponseEntity.ok().body(validationStatus);
diff --git a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerService.java b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerService.java
index 2cae31b2..0f0fc788 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerService.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerService.java
@@ -55,7 +55,7 @@ public GeneralPartnerService(GeneralPartnerRepository repository,
 
     public String createGeneralPartner(Transaction transaction, GeneralPartnerDto generalPartnerDto, String requestId, String userId) throws ServiceException, MethodArgumentNotValidException, NoSuchMethodException {
 
-        generalPartnerValidator.isValid(generalPartnerDto);
+        generalPartnerValidator.validatePartial(generalPartnerDto);
 
         GeneralPartnerDao dao = mapper.dtoToDao(generalPartnerDto);
         GeneralPartnerDao insertedSubmission = insertDaoWithMetadata(requestId, transaction, userId, dao);
@@ -138,7 +138,7 @@ public List<ValidationStatusError> validateGeneralPartner(Transaction transactio
             throws ServiceException {
         GeneralPartnerDto dto = getGeneralPartner(transaction, generalPartnerId);
 
-        return generalPartnerValidator.validate(dto);
+        return generalPartnerValidator.validateFull(dto);
     }
 
     public List<GeneralPartnerDto> getGeneralPartnerList(Transaction transaction) {
diff --git a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerValidator.java b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerValidator.java
index 44077522..e9972b08 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerValidator.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerValidator.java
@@ -19,16 +19,17 @@
 import java.util.Set;
 
 @Component
-public class GeneralPartnerValidator {
+public class GeneralPartnerValidator extends PartnerValidator {
 
     @Autowired
-    private Validator validator;
-
+    public GeneralPartnerValidator(Validator validator) {
+        super(validator);
+    }
 
-    public List<ValidationStatusError> validate(GeneralPartnerDto generalPartnerDto) throws ServiceException {
+    public List<ValidationStatusError> validateFull(GeneralPartnerDto generalPartnerDto) throws ServiceException {
         List<ValidationStatusError> errorsList = new ArrayList<>();
 
-        executeJavaBeansValidation(generalPartnerDto, errorsList);
+        checkFieldConstraints(generalPartnerDto, errorsList);
 
         var dataDto = generalPartnerDto.getData();
         if (dataDto.isLegalEntity()) {
@@ -48,7 +49,7 @@ public List<ValidationStatusError> validate(GeneralPartnerDto generalPartnerDto)
         return errorsList;
     }
 
-    public void isValid(GeneralPartnerDto generalPartnerDto) throws NoSuchMethodException, MethodArgumentNotValidException {
+    public void validatePartial(GeneralPartnerDto generalPartnerDto) throws NoSuchMethodException, MethodArgumentNotValidException {
         var methodParameter = new MethodParameter(GeneralPartnerDataDto.class.getConstructor(), -1);
         BindingResult bindingResult = new BeanPropertyBindingResult(generalPartnerDto, GeneralPartnerDataDto.class.getName());
 
@@ -135,7 +136,7 @@ private void addError(String fieldName, String defaultMessage, BindingResult bin
         bindingResult.addError(fieldError);
     }
 
-    private void executeJavaBeansValidation(GeneralPartnerDto generalPartnerDto, List<ValidationStatusError> errorsList)
+    private void checkFieldConstraints(GeneralPartnerDto generalPartnerDto, List<ValidationStatusError> errorsList)
             throws ServiceException {
         Set<ConstraintViolation<GeneralPartnerDto>> violations = validator.validate(generalPartnerDto);
 
@@ -143,23 +144,11 @@ private void executeJavaBeansValidation(GeneralPartnerDto generalPartnerDto, Lis
                 errorsList.add(createValidationStatusError(v.getMessage(), v.getPropertyPath().toString())));
 
         try {
-            isValid(generalPartnerDto);
+            validatePartial(generalPartnerDto);
         } catch (MethodArgumentNotValidException e) {
             convertFieldErrorsToValidationStatusErrors(e.getBindingResult(), errorsList);
         } catch (NoSuchMethodException e) {
             throw new ServiceException(e.getMessage());
         }
     }
-
-    private void convertFieldErrorsToValidationStatusErrors(BindingResult bindingResult, List<ValidationStatusError> errorsList) {
-        bindingResult.getFieldErrors().forEach(fe ->
-                errorsList.add(createValidationStatusError(fe.getDefaultMessage(), fe.getField())));
-    }
-
-    private ValidationStatusError createValidationStatusError(String errorMessage, String location) {
-        var error = new ValidationStatusError();
-        error.setError(errorMessage);
-        error.setLocation(location);
-        return error;
-    }
 }
diff --git a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerService.java b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerService.java
index 8474f680..a678bfb5 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerService.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerService.java
@@ -8,6 +8,7 @@
 import uk.gov.companieshouse.GenerateEtagUtil;
 import uk.gov.companieshouse.api.model.transaction.Resource;
 import uk.gov.companieshouse.api.model.transaction.Transaction;
+import uk.gov.companieshouse.api.model.validationstatus.ValidationStatusError;
 import uk.gov.companieshouse.limitedpartnershipsapi.exception.ResourceNotFoundException;
 import uk.gov.companieshouse.limitedpartnershipsapi.exception.ServiceException;
 import uk.gov.companieshouse.limitedpartnershipsapi.mapper.LimitedPartnerMapper;
@@ -27,6 +28,7 @@
 import static uk.gov.companieshouse.limitedpartnershipsapi.utils.Constants.FILING_KIND_LIMITED_PARTNER;
 import static uk.gov.companieshouse.limitedpartnershipsapi.utils.Constants.LINK_SELF;
 import static uk.gov.companieshouse.limitedpartnershipsapi.utils.Constants.URL_GET_LIMITED_PARTNER;
+import static uk.gov.companieshouse.limitedpartnershipsapi.utils.Constants.VALIDATION_STATUS_URI_SUFFIX;
 
 
 @Service
@@ -53,7 +55,7 @@ public LimitedPartnerService(LimitedPartnerRepository repository,
 
     public String createLimitedPartner(Transaction transaction, LimitedPartnerDto limitedPartnerDto, String requestId, String userId) throws ServiceException, MethodArgumentNotValidException, NoSuchMethodException {
 
-        limitedPartnerValidator.validate(limitedPartnerDto);
+        limitedPartnerValidator.validatePartial(limitedPartnerDto);
 
         LimitedPartnerDao dao = mapper.dtoToDao(limitedPartnerDto);
         LimitedPartnerDao insertedSubmission = insertDaoWithMetadata(requestId, transaction, userId, dao);
@@ -87,13 +89,13 @@ private String linkAndSaveDao(Transaction transaction, String submissionId, Limi
 
     private void updateTransactionWithLinksForLimitedPartner(String requestID,
                                                              Transaction transaction,
-                                                             String submissionUri
-                                                             ) throws ServiceException {
+                                                             String submissionUri)
+            throws ServiceException {
         var limitedPartnerResource = new Resource();
 
         Map<String, String> linksMap = new HashMap<>();
         linksMap.put("resource", submissionUri);
-//        linksMap.put("validation_status", submissionUri + VALIDATION_STATUS_URI_SUFFIX); // TODO - add this when validation status is implemented
+        linksMap.put("validation_status", submissionUri + VALIDATION_STATUS_URI_SUFFIX);
 
         limitedPartnerResource.setLinks(linksMap);
         limitedPartnerResource.setKind(FILING_KIND_LIMITED_PARTNER);
@@ -167,6 +169,13 @@ public void deleteLimitedPartner(Transaction transaction, String limitedPartnerI
 
     }
 
+    public List<ValidationStatusError> validateLimitedPartner(Transaction transaction, String limitedPartnerId)
+            throws ServiceException {
+        LimitedPartnerDto dto = getLimitedPartner(transaction, limitedPartnerId);
+
+        return limitedPartnerValidator.validateFull(dto);
+    }
+
     private void isSecondNationalityDifferent(LimitedPartnerDto limitedPartnerDto) throws NoSuchMethodException, MethodArgumentNotValidException {
         var methodParameter = new MethodParameter(LimitedPartnerDataDto.class.getConstructor(), -1);
         BindingResult bindingResult = new BeanPropertyBindingResult(limitedPartnerDto, LimitedPartnerDataDto.class.getName());
diff --git a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerValidator.java b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerValidator.java
index 70112a9f..b34fd13f 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerValidator.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerValidator.java
@@ -1,18 +1,51 @@
 package uk.gov.companieshouse.limitedpartnershipsapi.service;
 
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validator;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.MethodParameter;
 import org.springframework.stereotype.Component;
 import org.springframework.validation.BeanPropertyBindingResult;
 import org.springframework.validation.BindingResult;
 import org.springframework.validation.FieldError;
 import org.springframework.web.bind.MethodArgumentNotValidException;
+import uk.gov.companieshouse.api.model.validationstatus.ValidationStatusError;
+import uk.gov.companieshouse.limitedpartnershipsapi.exception.ServiceException;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dto.LimitedPartnerDataDto;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dto.LimitedPartnerDto;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
 @Component
-public class LimitedPartnerValidator {
+public class LimitedPartnerValidator extends PartnerValidator {
+
+    @Autowired
+    public LimitedPartnerValidator(Validator validator) {
+        super(validator);
+    }
+
+    public List<ValidationStatusError> validateFull(LimitedPartnerDto limitedPartnerDto) throws ServiceException {
+        List<ValidationStatusError> errorsList = new ArrayList<>();
+
+        checkFieldConstraints(limitedPartnerDto, errorsList);
+
+        var dataDto = limitedPartnerDto.getData();
+        if (dataDto.isLegalEntity()) {
+            if (dataDto.getPrincipalOfficeAddress() == null) {
+                errorsList.add(createValidationStatusError("Principal office address is required", LimitedPartnerDataDto.PRINCIPAL_OFFICE_ADDRESS_FIELD));
+            }
+        } else {
+            if (dataDto.getUsualResidentialAddress() == null) {
+                errorsList.add(createValidationStatusError("Usual residential address is required", LimitedPartnerDataDto.USUAL_RESIDENTIAL_ADDRESS_FIELD));
+            }
+        }
 
-    public void validate(LimitedPartnerDto limitedPartnerDto) throws NoSuchMethodException, MethodArgumentNotValidException {
+        return errorsList;
+    }
+
+    public void validatePartial(LimitedPartnerDto limitedPartnerDto) throws NoSuchMethodException, MethodArgumentNotValidException {
         var methodParameter = new MethodParameter(LimitedPartnerDataDto.class.getConstructor(), -1);
         BindingResult bindingResult = new BeanPropertyBindingResult(limitedPartnerDto, LimitedPartnerDataDto.class.getName());
 
@@ -20,7 +53,9 @@ public void validate(LimitedPartnerDto limitedPartnerDto) throws NoSuchMethodExc
 
         if (limitedPartnerDataDto.isLegalEntity()) {
             checkNotNullLegalEntity(limitedPartnerDataDto, bindingResult);
-
+            if (Boolean.FALSE.equals(limitedPartnerDataDto.getLegalPersonalityStatementChecked())) {
+                addError(LimitedPartnerDataDto.LEGAL_PERSONALITY_STATEMENT_CHECKED_FIELD, "Legal Personality Statement must be checked", bindingResult);
+            }
         } else if (limitedPartnerDataDto.getForename() != null || limitedPartnerDataDto.getSurname() != null) {
             checkNotNullPerson(limitedPartnerDataDto, bindingResult);
             isSecondNationalityDifferent(limitedPartnerDto, bindingResult);
@@ -93,5 +128,21 @@ private void addError(String fieldName, String defaultMessage, BindingResult bin
         var fieldError = new FieldError(LimitedPartnerDataDto.class.getName(), fieldName, defaultMessage);
         bindingResult.addError(fieldError);
     }
+
+    private void checkFieldConstraints(LimitedPartnerDto limitedPartnerDto, List<ValidationStatusError> errorsList)
+            throws ServiceException {
+        Set<ConstraintViolation<LimitedPartnerDto>> violations = validator.validate(limitedPartnerDto);
+
+        violations.forEach(v ->
+                errorsList.add(createValidationStatusError(v.getMessage(), v.getPropertyPath().toString())));
+
+        try {
+            validatePartial(limitedPartnerDto);
+        } catch (MethodArgumentNotValidException e) {
+            convertFieldErrorsToValidationStatusErrors(e.getBindingResult(), errorsList);
+        } catch (NoSuchMethodException e) {
+            throw new ServiceException(e.getMessage());
+        }
+    }
 }
 
diff --git a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnershipValidator.java b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnershipValidator.java
index 1cfb1889..2e7922bc 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnershipValidator.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnershipValidator.java
@@ -15,9 +15,12 @@
 @Component
 public class LimitedPartnershipValidator {
 
-    @Autowired
     private Validator validator;
 
+    @Autowired
+    public LimitedPartnershipValidator(Validator validator) {
+        this.validator = validator;
+    }
 
     public List<ValidationStatusError> validate(LimitedPartnershipDto limitedPartnershipDto) {
         Set<ConstraintViolation<LimitedPartnershipDto>> violations = validator.validate(limitedPartnershipDto);
diff --git a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/PartnerValidator.java b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/PartnerValidator.java
new file mode 100644
index 00000000..2f0ef4ab
--- /dev/null
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/PartnerValidator.java
@@ -0,0 +1,30 @@
+package uk.gov.companieshouse.limitedpartnershipsapi.service;
+
+import jakarta.validation.Validator;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.BindingResult;
+import uk.gov.companieshouse.api.model.validationstatus.ValidationStatusError;
+
+import java.util.List;
+
+public abstract class PartnerValidator {
+
+    protected Validator validator;
+
+    @Autowired
+    protected PartnerValidator(Validator validator) {
+        this.validator = validator;
+    }
+
+    protected void convertFieldErrorsToValidationStatusErrors(BindingResult bindingResult, List<ValidationStatusError> errorsList) {
+        bindingResult.getFieldErrors().forEach(fe ->
+                errorsList.add(createValidationStatusError(fe.getDefaultMessage(), fe.getField())));
+    }
+
+    protected ValidationStatusError createValidationStatusError(String errorMessage, String location) {
+        var error = new ValidationStatusError();
+        error.setError(errorMessage);
+        error.setLocation(location);
+        return error;
+    }
+}
diff --git a/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/GeneralPartnerControllerValidationTest.java b/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/GeneralPartnerControllerValidationTest.java
index f46b2c29..3c897966 100644
--- a/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/GeneralPartnerControllerValidationTest.java
+++ b/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/GeneralPartnerControllerValidationTest.java
@@ -37,8 +37,8 @@ class GeneralPartnerControllerValidationTest {
 
     private static final String GENERAL_PARTNER_ID = "93702824-9062-4c63-a694-716acffccdd5";
 
-    private static final String POST_URL = "/transactions/863851-951242-143528/limited-partnership/general-partner";
-    private static final String VALIDATE_STATUS_URL = POST_URL + "/" + GENERAL_PARTNER_ID + "/validation-status";
+    private static final String BASE_URL = "/transactions/863851-951242-143528/limited-partnership/general-partner";
+    private static final String VALIDATE_STATUS_URL = BASE_URL + "/" + GENERAL_PARTNER_ID + "/validation-status";
 
     // PERSON
     private static final String JSON_CORRECT = """
@@ -136,7 +136,7 @@ void setUp() {
             JSON_INVALID_SECOND_NATIONALITY + "$ data.nationality2 $ Second nationality must be valid"
     }, delimiter = '$')
     void shouldReturn400(String body, String field, String errorMessage) throws Exception {
-        mockMvc.perform(post(GeneralPartnerControllerValidationTest.POST_URL)
+        mockMvc.perform(post(GeneralPartnerControllerValidationTest.BASE_URL)
                         .contentType(MediaType.APPLICATION_JSON)
                         .characterEncoding(StandardCharsets.UTF_8)
                         .headers(httpHeaders)
@@ -148,7 +148,7 @@ void shouldReturn400(String body, String field, String errorMessage) throws Exce
 
     @Test
     void shouldReturn201() throws Exception {
-        mockMvc.perform(post(GeneralPartnerControllerValidationTest.POST_URL)
+        mockMvc.perform(post(GeneralPartnerControllerValidationTest.BASE_URL)
                         .contentType(MediaType.APPLICATION_JSON)
                         .characterEncoding(StandardCharsets.UTF_8)
                         .headers(httpHeaders)
@@ -159,7 +159,7 @@ void shouldReturn201() throws Exception {
 
     @Test
     void shouldReturn201WhenCreatingGeneralPartnerLegalEntity() throws Exception {
-        mockMvc.perform(post(GeneralPartnerControllerValidationTest.POST_URL)
+        mockMvc.perform(post(GeneralPartnerControllerValidationTest.BASE_URL)
                         .contentType(MediaType.APPLICATION_JSON)
                         .characterEncoding(StandardCharsets.UTF_8)
                         .headers(httpHeaders)
@@ -170,7 +170,7 @@ void shouldReturn201WhenCreatingGeneralPartnerLegalEntity() throws Exception {
 
     @Test
     void shouldReturn400WhenCreatingGeneralPartnerLegalEntityWithWrongCountry() throws Exception {
-        mockMvc.perform(post(GeneralPartnerControllerValidationTest.POST_URL)
+        mockMvc.perform(post(GeneralPartnerControllerValidationTest.BASE_URL)
                         .contentType(MediaType.APPLICATION_JSON)
                         .characterEncoding(StandardCharsets.UTF_8)
                         .headers(httpHeaders)
@@ -181,7 +181,7 @@ void shouldReturn400WhenCreatingGeneralPartnerLegalEntityWithWrongCountry() thro
     }
 
     @Nested
-    class ValidatePartnership {
+    class ValidatePartner {
         @Test
         void shouldReturn200IfNoErrors() throws Exception {
             mockMvc.perform(get(VALIDATE_STATUS_URL)
diff --git a/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerControllerValidationTest.java b/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerControllerValidationTest.java
index 392f813d..2fe65d2a 100644
--- a/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerControllerValidationTest.java
+++ b/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerControllerValidationTest.java
@@ -1,6 +1,7 @@
 package uk.gov.companieshouse.limitedpartnershipsapi.controller;
 
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
@@ -13,11 +14,18 @@
 import org.springframework.test.web.servlet.MockMvc;
 import uk.gov.companieshouse.api.interceptor.TransactionInterceptor;
 import uk.gov.companieshouse.api.model.transaction.Transaction;
+import uk.gov.companieshouse.api.model.validationstatus.ValidationStatusError;
 import uk.gov.companieshouse.limitedpartnershipsapi.exception.GlobalExceptionHandler;
+import uk.gov.companieshouse.limitedpartnershipsapi.exception.ResourceNotFoundException;
+import uk.gov.companieshouse.limitedpartnershipsapi.exception.ServiceException;
 import uk.gov.companieshouse.limitedpartnershipsapi.service.LimitedPartnerService;
 
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
 
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -27,7 +35,10 @@
 @WebMvcTest(controllers = {LimitedPartnerController.class})
 class LimitedPartnerControllerValidationTest {
 
-    private static final String POST_URL = "/transactions/863851-951242-143528/limited-partnership/limited-partner";
+    private static final String LIMITED_PARTNER_ID = "863851-951242-143528";
+
+    private static final String BASE_URL = "/transactions/" + LIMITED_PARTNER_ID + "/limited-partnership/limited-partner";
+    private static final String VALIDATE_STATUS_URL = BASE_URL + "/" + LIMITED_PARTNER_ID + "/validation-status";
 
     // PERSON
     private static final String JSON_CORRECT = """
@@ -124,7 +135,7 @@ void setUp() {
             JSON_INVALID_SECOND_NATIONALITY + "$ data.nationality2 $ Second nationality must be valid"
     }, delimiter = '$')
     void shouldReturn400(String body, String field, String errorMessage) throws Exception {
-        mockMvc.perform(post(LimitedPartnerControllerValidationTest.POST_URL)
+        mockMvc.perform(post(LimitedPartnerControllerValidationTest.BASE_URL)
                         .contentType(MediaType.APPLICATION_JSON)
                         .characterEncoding(StandardCharsets.UTF_8)
                         .headers(httpHeaders)
@@ -136,7 +147,7 @@ void shouldReturn400(String body, String field, String errorMessage) throws Exce
 
     @Test
     void shouldReturn201() throws Exception {
-        mockMvc.perform(post(LimitedPartnerControllerValidationTest.POST_URL)
+        mockMvc.perform(post(LimitedPartnerControllerValidationTest.BASE_URL)
                         .contentType(MediaType.APPLICATION_JSON)
                         .characterEncoding(StandardCharsets.UTF_8)
                         .headers(httpHeaders)
@@ -147,7 +158,7 @@ void shouldReturn201() throws Exception {
 
     @Test
     void shouldReturn201WhenCreatingLimitedPartnerLegalEntity() throws Exception {
-        mockMvc.perform(post(LimitedPartnerControllerValidationTest.POST_URL)
+        mockMvc.perform(post(LimitedPartnerControllerValidationTest.BASE_URL)
                         .contentType(MediaType.APPLICATION_JSON)
                         .characterEncoding(StandardCharsets.UTF_8)
                         .headers(httpHeaders)
@@ -158,7 +169,7 @@ void shouldReturn201WhenCreatingLimitedPartnerLegalEntity() throws Exception {
 
     @Test
     void shouldReturn400WhenCreatingLimitedPartnerLegalEntityWithWrongCountry() throws Exception {
-        mockMvc.perform(post(LimitedPartnerControllerValidationTest.POST_URL)
+        mockMvc.perform(post(LimitedPartnerControllerValidationTest.BASE_URL)
                         .contentType(MediaType.APPLICATION_JSON)
                         .characterEncoding(StandardCharsets.UTF_8)
                         .headers(httpHeaders)
@@ -167,4 +178,68 @@ void shouldReturn400WhenCreatingLimitedPartnerLegalEntityWithWrongCountry() thro
                 .andExpect(status().isBadRequest())
                 .andExpect(jsonPath("$.['errors'].['data.legalEntityRegistrationLocation']").value("Legal entity registration location must be valid"));
     }
+
+    @Nested
+    class ValidatePartner {
+        @Test
+        void shouldReturn200IfNoErrors() throws Exception {
+            mockMvc.perform(get(VALIDATE_STATUS_URL)
+                            .contentType(MediaType.APPLICATION_JSON)
+                            .characterEncoding(StandardCharsets.UTF_8)
+                            .headers(httpHeaders)
+                            .requestAttr("transaction", transaction)
+                            .content(""))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("is_valid").value("true"));
+        }
+
+        @Test
+        void shouldReturn200AndErrorDetailsIfErrors() throws Exception {
+            List<ValidationStatusError> errorsList = new ArrayList<>();
+            errorsList.add(new ValidationStatusError("Forename must be greater than 1", "here", null, null));
+            errorsList.add(new ValidationStatusError("First nationality must be valid", "there", null, null));
+            when(limitedPartnerService.validateLimitedPartner(transaction, LIMITED_PARTNER_ID)).thenReturn(errorsList);
+
+            mockMvc.perform(get(VALIDATE_STATUS_URL)
+                            .contentType(MediaType.APPLICATION_JSON)
+                            .characterEncoding(StandardCharsets.UTF_8)
+                            .headers(httpHeaders)
+                            .requestAttr("transaction", transaction)
+                            .content(""))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("is_valid").value("false"))
+                    .andExpect(jsonPath("$.['errors'][0].['location']").value("here"))
+                    .andExpect(jsonPath("$.['errors'][0].['error']").value("Forename must be greater than 1"))
+                    .andExpect(jsonPath("$.['errors'][1].['location']").value("there"))
+                    .andExpect(jsonPath("$.['errors'][1].['error']").value("First nationality must be valid"));
+        }
+
+        @Test
+        void shouldReturn404IfGeneralPartnerNotFound() throws Exception {
+            when(limitedPartnerService.validateLimitedPartner(transaction, LIMITED_PARTNER_ID))
+                    .thenThrow(new ResourceNotFoundException("Error"));
+
+            mockMvc.perform(get(VALIDATE_STATUS_URL)
+                            .contentType(MediaType.APPLICATION_JSON)
+                            .characterEncoding(StandardCharsets.UTF_8)
+                            .headers(httpHeaders)
+                            .requestAttr("transaction", transaction)
+                            .content(""))
+                    .andExpect(status().isNotFound());
+        }
+
+        @Test
+        void shouldReturn500IfUnexpectedServiceExceptionThrown() throws Exception {
+            when(limitedPartnerService.validateLimitedPartner(transaction, LIMITED_PARTNER_ID))
+                    .thenThrow(new ServiceException("Error"));
+
+            mockMvc.perform(get(VALIDATE_STATUS_URL)
+                            .contentType(MediaType.APPLICATION_JSON)
+                            .characterEncoding(StandardCharsets.UTF_8)
+                            .headers(httpHeaders)
+                            .requestAttr("transaction", transaction)
+                            .content(""))
+                    .andExpect(status().isInternalServerError());
+        }
+    }
 }
diff --git a/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerServiceValidateTest.java b/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerServiceValidateTest.java
new file mode 100644
index 00000000..2f69be07
--- /dev/null
+++ b/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerServiceValidateTest.java
@@ -0,0 +1,243 @@
+package uk.gov.companieshouse.limitedpartnershipsapi.service;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import uk.gov.companieshouse.api.model.transaction.Resource;
+import uk.gov.companieshouse.api.model.transaction.Transaction;
+import uk.gov.companieshouse.api.model.validationstatus.ValidationStatusError;
+import uk.gov.companieshouse.limitedpartnershipsapi.exception.ResourceNotFoundException;
+import uk.gov.companieshouse.limitedpartnershipsapi.exception.ServiceException;
+import uk.gov.companieshouse.limitedpartnershipsapi.model.common.Country;
+import uk.gov.companieshouse.limitedpartnershipsapi.model.common.Nationality;
+import uk.gov.companieshouse.limitedpartnershipsapi.model.common.dao.AddressDao;
+import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dao.LimitedPartnerDao;
+import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dao.LimitedPartnerDataDao;
+import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dto.LimitedPartnerDataDto;
+import uk.gov.companieshouse.limitedpartnershipsapi.repository.LimitedPartnerRepository;
+
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static uk.gov.companieshouse.limitedpartnershipsapi.utils.Constants.FILING_KIND_LIMITED_PARTNER;
+
+@ExtendWith(MockitoExtension.class)
+@SpringBootTest
+class LimitedPartnerServiceValidateTest {
+
+    private static final String LIMITED_PARTNER_ID = "abc-123";
+    private static final String TRANSACTION_ID = "txn-456";
+
+    @Autowired
+    private LimitedPartnerService service;
+
+    @MockitoBean
+    private LimitedPartnerRepository repository;
+
+
+    @Test
+    void shouldReturnNoErrorsWhenLimitedPartnerDataIsValid() throws ServiceException {
+        // given
+        LimitedPartnerDao limitedPartnerDao = createPersonDao();
+
+        Transaction transaction = buildTransaction();
+
+        when(repository.findById(limitedPartnerDao.getId())).thenReturn(Optional.of(limitedPartnerDao));
+
+        // when
+        List<ValidationStatusError> results = service.validateLimitedPartner(transaction, LIMITED_PARTNER_ID);
+
+        // then
+        verify(repository).findById(limitedPartnerDao.getId());
+        assertEquals(0, results.size());
+    }
+
+    @Test
+    void shouldReturnErrorsWhenLimitedPartnerDataIsInvalidAndJavaBeanChecksFail() throws ServiceException {
+        // given
+        LimitedPartnerDao limitedPartnerDao = createPersonDao();
+        limitedPartnerDao.getData().setDateOfBirth(LocalDate.of(3000, 10, 3));
+        limitedPartnerDao.getData().setNationality1("INVALID-COUNTRY");
+
+        Transaction transaction = buildTransaction();
+
+        when(repository.findById(limitedPartnerDao.getId())).thenReturn(Optional.of(limitedPartnerDao));
+
+        // when
+        List<ValidationStatusError> results = service.validateLimitedPartner(transaction, LIMITED_PARTNER_ID);
+
+        // then
+        verify(repository).findById(limitedPartnerDao.getId());
+        assertThat(results)
+                .extracting(ValidationStatusError::getError, ValidationStatusError::getLocation)
+                .containsExactlyInAnyOrder(
+                        tuple("Date of birth must be in the past", "data.dateOfBirth"),
+                        tuple("First nationality must be valid", "data.nationality1"));
+    }
+
+    @Test
+    void shouldReturnErrorsWhenLimitedPartnerPersonDataIsInvalidAndCustomChecksFail() throws ServiceException {
+        // given
+        LimitedPartnerDao limitedPartnerDao = createPersonDao();
+        limitedPartnerDao.getData().setDateOfBirth(null);
+        limitedPartnerDao.getData().setNationality2(Nationality.EMIRATI.getDescription());
+        limitedPartnerDao.getData().setUsualResidentialAddress(null);
+
+        Transaction transaction = buildTransaction();
+
+        when(repository.findById(limitedPartnerDao.getId())).thenReturn(Optional.of(limitedPartnerDao));
+
+        // when
+        List<ValidationStatusError> results = service.validateLimitedPartner(transaction, LIMITED_PARTNER_ID);
+
+        // then
+        verify(repository).findById(limitedPartnerDao.getId());
+
+        assertThat(results)
+                .extracting(ValidationStatusError::getError, ValidationStatusError::getLocation)
+                .containsExactlyInAnyOrder(
+                        tuple("Date of birth is required", LimitedPartnerDataDto.DATE_OF_BIRTH_FIELD),
+                        tuple("Second nationality must be different from the first", LimitedPartnerDataDto.NATIONALITY2_FIELD),
+                        tuple("Usual residential address is required", LimitedPartnerDataDto.USUAL_RESIDENTIAL_ADDRESS_FIELD));
+    }
+
+    @Test
+    void shouldReturnErrorsWhenLimitedPartnerLegalEntityDataIsInvalidAndCustomChecksFail() throws ServiceException {
+        // given
+        LimitedPartnerDao limitedPartnerDao = createLegalEntityDao();
+        limitedPartnerDao.getData().setGoverningLaw(null);
+        limitedPartnerDao.getData().setLegalPersonalityStatementChecked(false);
+        limitedPartnerDao.getData().setRegisteredCompanyNumber(null);
+        limitedPartnerDao.getData().setPrincipalOfficeAddress(null);
+
+        Transaction transaction = buildTransaction();
+
+        when(repository.findById(limitedPartnerDao.getId())).thenReturn(Optional.of(limitedPartnerDao));
+
+        // when
+        List<ValidationStatusError> results = service.validateLimitedPartner(transaction, LIMITED_PARTNER_ID);
+
+        // then
+        verify(repository).findById(limitedPartnerDao.getId());
+        assertThat(results)
+                .extracting(ValidationStatusError::getError, ValidationStatusError::getLocation)
+                .containsExactlyInAnyOrder(
+                        tuple("Governing Law is required", LimitedPartnerDataDto.GOVERNING_LAW_FIELD),
+                        tuple("Legal Personality Statement must be checked", LimitedPartnerDataDto.LEGAL_PERSONALITY_STATEMENT_CHECKED_FIELD),
+                        tuple("Registered Company Number is required", LimitedPartnerDataDto.REGISTERED_COMPANY_NUMBER_FIELD),
+                        tuple("Principal office address is required", LimitedPartnerDataDto.PRINCIPAL_OFFICE_ADDRESS_FIELD));
+    }
+
+    @Test
+    void shouldReturnErrorsWhenLimitedPartnerLegalEntityDataIsInvalidAndJavaBeanAndCustomChecksFail() throws ServiceException {
+        // given
+        LimitedPartnerDao limitedPartnerDao = createLegalEntityDao();
+        limitedPartnerDao.getData().setRegisteredCompanyNumber("");
+        limitedPartnerDao.getData().setLegalEntityName(null);
+
+        Transaction transaction = buildTransaction();
+
+        when(repository.findById(limitedPartnerDao.getId())).thenReturn(Optional.of(limitedPartnerDao));
+
+        // when
+        List<ValidationStatusError> results = service.validateLimitedPartner(transaction, LIMITED_PARTNER_ID);
+
+        // then
+        verify(repository).findById(limitedPartnerDao.getId());
+        assertThat(results)
+                .extracting(ValidationStatusError::getError, ValidationStatusError::getLocation)
+                .containsExactlyInAnyOrder(
+                        tuple("Registered company number must be greater than 1", "data.registeredCompanyNumber"),
+                        tuple("Legal Entity Name is required", LimitedPartnerDataDto.LEGAL_ENTITY_NAME_FIELD));
+    }
+
+    @Test
+    void shouldThrowResourceNotFoundExceptionWhenLimitedPartnerIdAndTransactionIdDoNotMatch() {
+        // given
+        LimitedPartnerDao limitedPartnerDao = createLegalEntityDao();
+        limitedPartnerDao.getData().setRegisteredCompanyNumber("");
+        limitedPartnerDao.getData().setLegalEntityName(null);
+
+        Transaction transaction = buildTransaction();
+        transaction.getResources().values().forEach(r -> r.setKind("INVALID-KIND"));
+
+        when(repository.findById(limitedPartnerDao.getId())).thenReturn(Optional.of(limitedPartnerDao));
+
+        // when + then
+        assertThrows(ResourceNotFoundException.class, () -> service.validateLimitedPartner(transaction, LIMITED_PARTNER_ID));
+    }
+
+    private Transaction buildTransaction() {
+        Transaction transaction = new Transaction();
+        transaction.setId(TRANSACTION_ID);
+
+        Resource resource = new Resource();
+        resource.setKind(FILING_KIND_LIMITED_PARTNER);
+        Map<String, String> links = new HashMap<>();
+        links.put("resource", "/transactions/txn-456/limited-partnership/limited-partner/abc-123");
+        resource.setLinks(links);
+
+        Map<String, Resource> resourceMap = new HashMap<>();
+        resourceMap.put(String.format("/transactions/%s/limited-partnership/limited-partner/%s", TRANSACTION_ID, LIMITED_PARTNER_ID), resource);
+        transaction.setResources(resourceMap);
+
+        return transaction;
+    }
+
+    private LimitedPartnerDao createPersonDao() {
+        LimitedPartnerDao dao = new LimitedPartnerDao();
+
+        dao.setId(LIMITED_PARTNER_ID);
+        LimitedPartnerDataDao dataDao = new LimitedPartnerDataDao();
+        dataDao.setForename("Jack");
+        dataDao.setSurname("Jones");
+        dataDao.setDateOfBirth(LocalDate.of(2000, 10, 3));
+        dataDao.setNationality1(Nationality.EMIRATI.getDescription());
+        dataDao.setUsualResidentialAddress(createAddressDao());
+        dao.setData(dataDao);
+
+        return dao;
+    }
+
+    private LimitedPartnerDao createLegalEntityDao() {
+        LimitedPartnerDao dao = new LimitedPartnerDao();
+
+        dao.setId(LIMITED_PARTNER_ID);
+        LimitedPartnerDataDao dataDao = new LimitedPartnerDataDao();
+        dataDao.setLegalEntityRegisterName("Shell Company");
+        dataDao.setLegalForm("AA");
+        dataDao.setLegalEntityName("Same");
+        dataDao.setGoverningLaw("UK");
+        dataDao.setLegalEntityRegistrationLocation(Country.UNITED_STATES.getDescription());
+        dataDao.setRegisteredCompanyNumber("LP111222");
+        dataDao.setLegalPersonalityStatementChecked(true);
+        dataDao.setPrincipalOfficeAddress(createAddressDao());
+        dao.setData(dataDao);
+
+        return dao;
+    }
+
+    private AddressDao createAddressDao() {
+        AddressDao dao = new AddressDao();
+
+        dao.setPremises("33");
+        dao.setAddressLine1("Acacia Avenue");
+        dao.setLocality("Birmingham");
+        dao.setCountry("England");
+        dao.setPostalCode("BM1 2EH");
+
+        return dao;
+    }
+}


Reviews:
user: login: markpit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:23:30Z
user: login: markpit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:35:52Z
user: login: dlloyd1-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:37:22Z
user: login: mwestacott
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:45:32Z
user: login: lduranteau
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:06:30Z
user: login: mwestacott
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:15:47Z
user: login: mwestacott
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T15:12:11Z
user: login: mwestacott
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T15:13:14Z
user: login: mwestacott
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T15:13:22Z
user: login: mwestacott
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T15:14:26Z
user: login: markpit
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T15:24:13Z
user: login: dlloyd1-ch
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:02:14Z

Review comments:
diff_hunk: @@ -137,4 +140,30 @@ public ResponseEntity<Object> deleteLimitedPartner(@RequestAttribute(TRANSACTION
 
         return new ResponseEntity<>(HttpStatus.NO_CONTENT);
     }
+
+    @GetMapping("/limited-partner/{" + URL_PARAM_LIMITED_PARTNER_ID + "}/validation-status")
+    public ResponseEntity<ValidationStatusResponse> getValidationStatus(@RequestAttribute(TRANSACTION_KEY) Transaction transaction,
+                                                                        @PathVariable(URL_PARAM_LIMITED_PARTNER_ID) String limitedPartnerId,
+                                                                        @RequestHeader(value = ERIC_REQUEST_ID_KEY) String requestId)
+            throws ServiceException {
+
+        var logMap = new HashMap<String, Object>();
+        logMap.put(URL_PARAM_TRANSACTION_ID, transaction.getId());
+        logMap.put(URL_PARAM_LIMITED_PARTNER_ID, limitedPartnerId);
+
+        ApiLogger.infoContext(requestId, "Calling service to validate a Limited Partner", logMap);
+        var validationStatus = new ValidationStatusResponse();
+        validationStatus.setValid(true);
+
+        var validationErrors = limitedPartnerService.validateLimitedPartner(transaction, limitedPartnerId);
+
+        if (!validationErrors.isEmpty()) {
+            ApiLogger.errorContext(requestId, String.format("Validation errors: %s",
+                    new GsonBuilder().create().toJson(validationErrors)), null, logMap);
+            validationStatus.setValid(false);
+            validationStatus.setValidationStatusError(validationErrors.toArray(new ValidationStatusError[0]));
path: src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerController.java
user: login: markpit
body: validationErrors.toArray(ValidationStatusError[]::new) is a better way to convert apparently (Automatically resizes the array to match the list size)
created_at: 2025-05-13T12:23:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,7 +35,10 @@
 @WebMvcTest(controllers = {LimitedPartnerController.class})
 class LimitedPartnerControllerValidationTest {
 
-    private static final String POST_URL = "/transactions/863851-951242-143528/limited-partnership/limited-partner";
+    private static final String LIMITED_PARTNER_ID = "863851-951242-143528";
+
+    private static final String POST_URL = "/transactions/" + LIMITED_PARTNER_ID + "/limited-partnership/limited-partner";
path: src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerControllerValidationTest.java
user: login: dlloyd1-ch
body: Prefer BASE_URL or something similar instead of POST_URL as it's used to specify the validation-status url which is not POST.
created_at: 2025-05-13T12:32:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,5 +128,21 @@ private void addError(String fieldName, String defaultMessage, BindingResult bin
         var fieldError = new FieldError(LimitedPartnerDataDto.class.getName(), fieldName, defaultMessage);
         bindingResult.addError(fieldError);
     }
+
+    private void executeJavaBeansValidation(LimitedPartnerDto limitedPartnerDto, List<ValidationStatusError> errorsList)
path: src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerValidator.java
user: login: markpit
body: not sure on the JavaBeans in the name, the method seems to be checking for constraintViolations, so would validateFieldConstraints be better?
created_at: 2025-05-13T12:35:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,5 +128,21 @@ private void addError(String fieldName, String defaultMessage, BindingResult bin
         var fieldError = new FieldError(LimitedPartnerDataDto.class.getName(), fieldName, defaultMessage);
         bindingResult.addError(fieldError);
     }
+
+    private void executeJavaBeansValidation(LimitedPartnerDto limitedPartnerDto, List<ValidationStatusError> errorsList)
path: src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerValidator.java
user: login: mwestacott
body: Agree. Will change this.
created_at: 2025-05-13T12:45:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,26 +1,61 @@
 package uk.gov.companieshouse.limitedpartnershipsapi.service;
 
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validator;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.MethodParameter;
 import org.springframework.stereotype.Component;
 import org.springframework.validation.BeanPropertyBindingResult;
 import org.springframework.validation.BindingResult;
 import org.springframework.validation.FieldError;
 import org.springframework.web.bind.MethodArgumentNotValidException;
+import uk.gov.companieshouse.api.model.validationstatus.ValidationStatusError;
+import uk.gov.companieshouse.limitedpartnershipsapi.exception.ServiceException;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dto.LimitedPartnerDataDto;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dto.LimitedPartnerDto;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
 @Component
-public class LimitedPartnerValidator {
+public class LimitedPartnerValidator extends PartnerValidator {
+
+    @Autowired
+    public LimitedPartnerValidator(Validator validator) {
+        super(validator);
+    }
+
+    public List<ValidationStatusError> validate(LimitedPartnerDto limitedPartnerDto) throws ServiceException {
path: src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerValidator.java
user: login: lduranteau
body: it would be a good idea to rename the method validate and isValid as this is confusing
one is for partial validation (creation - details) and the other is for full validation (including addresses)
created_at: 2025-05-13T13:06:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,26 +1,61 @@
 package uk.gov.companieshouse.limitedpartnershipsapi.service;
 
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validator;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.MethodParameter;
 import org.springframework.stereotype.Component;
 import org.springframework.validation.BeanPropertyBindingResult;
 import org.springframework.validation.BindingResult;
 import org.springframework.validation.FieldError;
 import org.springframework.web.bind.MethodArgumentNotValidException;
+import uk.gov.companieshouse.api.model.validationstatus.ValidationStatusError;
+import uk.gov.companieshouse.limitedpartnershipsapi.exception.ServiceException;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dto.LimitedPartnerDataDto;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dto.LimitedPartnerDto;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
 @Component
-public class LimitedPartnerValidator {
+public class LimitedPartnerValidator extends PartnerValidator {
+
+    @Autowired
+    public LimitedPartnerValidator(Validator validator) {
+        super(validator);
+    }
+
+    public List<ValidationStatusError> validate(LimitedPartnerDto limitedPartnerDto) throws ServiceException {
path: src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerValidator.java
user: login: mwestacott
body: Yep, agree. I'll go with `validatePartial()` and `validateFull()` as the method names (like we had in ROE). Makes it clear then.
created_at: 2025-05-13T13:15:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -137,4 +140,30 @@ public ResponseEntity<Object> deleteLimitedPartner(@RequestAttribute(TRANSACTION
 
         return new ResponseEntity<>(HttpStatus.NO_CONTENT);
     }
+
+    @GetMapping("/limited-partner/{" + URL_PARAM_LIMITED_PARTNER_ID + "}/validation-status")
+    public ResponseEntity<ValidationStatusResponse> getValidationStatus(@RequestAttribute(TRANSACTION_KEY) Transaction transaction,
+                                                                        @PathVariable(URL_PARAM_LIMITED_PARTNER_ID) String limitedPartnerId,
+                                                                        @RequestHeader(value = ERIC_REQUEST_ID_KEY) String requestId)
+            throws ServiceException {
+
+        var logMap = new HashMap<String, Object>();
+        logMap.put(URL_PARAM_TRANSACTION_ID, transaction.getId());
+        logMap.put(URL_PARAM_LIMITED_PARTNER_ID, limitedPartnerId);
+
+        ApiLogger.infoContext(requestId, "Calling service to validate a Limited Partner", logMap);
+        var validationStatus = new ValidationStatusResponse();
+        validationStatus.setValid(true);
+
+        var validationErrors = limitedPartnerService.validateLimitedPartner(transaction, limitedPartnerId);
+
+        if (!validationErrors.isEmpty()) {
+            ApiLogger.errorContext(requestId, String.format("Validation errors: %s",
+                    new GsonBuilder().create().toJson(validationErrors)), null, logMap);
+            validationStatus.setValid(false);
+            validationStatus.setValidationStatusError(validationErrors.toArray(new ValidationStatusError[0]));
path: src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerController.java
user: login: mwestacott
body: Done. Change also applied to other controllers where this was being done.
created_at: 2025-05-13T15:12:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,7 +35,10 @@
 @WebMvcTest(controllers = {LimitedPartnerController.class})
 class LimitedPartnerControllerValidationTest {
 
-    private static final String POST_URL = "/transactions/863851-951242-143528/limited-partnership/limited-partner";
+    private static final String LIMITED_PARTNER_ID = "863851-951242-143528";
+
+    private static final String POST_URL = "/transactions/" + LIMITED_PARTNER_ID + "/limited-partnership/limited-partner";
path: src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/LimitedPartnerControllerValidationTest.java
user: login: mwestacott
body: Done. Change also applied to the `GeneralPartnerControllerValidationTest` where the same names were used.
created_at: 2025-05-13T15:13:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,26 +1,61 @@
 package uk.gov.companieshouse.limitedpartnershipsapi.service;
 
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validator;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.MethodParameter;
 import org.springframework.stereotype.Component;
 import org.springframework.validation.BeanPropertyBindingResult;
 import org.springframework.validation.BindingResult;
 import org.springframework.validation.FieldError;
 import org.springframework.web.bind.MethodArgumentNotValidException;
+import uk.gov.companieshouse.api.model.validationstatus.ValidationStatusError;
+import uk.gov.companieshouse.limitedpartnershipsapi.exception.ServiceException;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dto.LimitedPartnerDataDto;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.limitedpartner.dto.LimitedPartnerDto;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
 @Component
-public class LimitedPartnerValidator {
+public class LimitedPartnerValidator extends PartnerValidator {
+
+    @Autowired
+    public LimitedPartnerValidator(Validator validator) {
+        super(validator);
+    }
+
+    public List<ValidationStatusError> validate(LimitedPartnerDto limitedPartnerDto) throws ServiceException {
path: src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerValidator.java
user: login: mwestacott
body: Done.
created_at: 2025-05-13T15:13:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,5 +128,21 @@ private void addError(String fieldName, String defaultMessage, BindingResult bin
         var fieldError = new FieldError(LimitedPartnerDataDto.class.getName(), fieldName, defaultMessage);
         bindingResult.addError(fieldError);
     }
+
+    private void executeJavaBeansValidation(LimitedPartnerDto limitedPartnerDto, List<ValidationStatusError> errorsList)
path: src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/LimitedPartnerValidator.java
user: login: mwestacott
body: Done. I went for `checkFieldConstraints` in the end, for consistency with the other private 'check' methods.
created_at: 2025-05-13T15:14:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: ch-code-analysis
created_at: 2025-05-08T12:43:08Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-05-12T15:50:56Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-05-13T08:03:29Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-05-13T08:23:59Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-05-13T08:31:49Z
body: CI: No security warnings found
author_association: NONE
user: login: chsonarqubeprchecks[bot]
created_at: 2025-05-13T15:09:43Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarqube.companieshouse.gov.uk/dashboard?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135&resolved=false) [0 New issues](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-api&fixedInPullRequest=135) [3 Fixed issues](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-api&fixedInPullRequest=135)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135&issueStatuses=ACCEPTED) [0 Accepted issues](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.companieshouse.gov.uk/security_hotspots?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135) [0 Security Hotspots](https://sonarqube.companieshouse.gov.uk/security_hotspots?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.companieshouse.gov.uk/component_measures?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135&metric=new_coverage&view=list) [97.2% Coverage on New Code](https://sonarqube.companieshouse.gov.uk/component_measures?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135&metric=new_coverage&view=list)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.companieshouse.gov.uk/component_measures?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://sonarqube.companieshouse.gov.uk/component_measures?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://sonarqube.companieshouse.gov.uk/dashboard?id=uk.gov.companieshouse%3Alimited-partnerships-api&pullRequest=135)


author_association: NONE
user: login: ch-code-analysis
created_at: 2025-05-13T15:13:22Z
body: CI: No security warnings found
author_association: NONE