Repository: companieshouse/limited-partnerships-api
Pull request #91: Feature/lp 438 general partner patch nationality date
Author: login: lduranteau
Created: 2025-03-06T15:44:42Z | Merged: 2025-03-06T16:55:47Z

### JIRA link
https://companieshouse.atlassian.net/browse/LP-438

### Change description
patch nationality check

### Work checklist

* [x] Bug fix
* [ ] 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 1cb65258..741281ea 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/GeneralPartnerController.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/GeneralPartnerController.java
@@ -71,7 +71,7 @@ public ResponseEntity<Object> updateGeneralPartner(
             @PathVariable(URL_PARAM_GENERAL_PARTNER_ID) String generalPartnerId,
             @Valid @RequestBody GeneralPartnerDataDto generalPartnerDataDto,
             @RequestHeader(value = ERIC_REQUEST_ID_KEY) String requestId,
-            @RequestHeader(value = ERIC_IDENTITY) String userId) {
+            @RequestHeader(value = ERIC_IDENTITY) String userId) throws MethodArgumentNotValidException, NoSuchMethodException {
 
         String transactionId = transaction.getId();
         HashMap<String, Object> logMap = new HashMap<>();
diff --git a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/mapper/GeneralPartnerMapper.java b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/mapper/GeneralPartnerMapper.java
index 02a45dc7..7aa9672c 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/mapper/GeneralPartnerMapper.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/mapper/GeneralPartnerMapper.java
@@ -3,6 +3,7 @@
 import org.mapstruct.InheritConfiguration;
 import org.mapstruct.Mapper;
 import org.mapstruct.MappingTarget;
+import org.mapstruct.NullValuePropertyMappingStrategy;
 import org.springframework.stereotype.Component;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.Country;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.Nationality;
@@ -11,7 +12,9 @@
 import uk.gov.companieshouse.limitedpartnershipsapi.model.dto.GeneralPartnerDto;
 
 @Component
-@Mapper(componentModel = "spring")
+@Mapper(uses = JsonNullableMapper.class,
+        nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
+        componentModel = "spring")
 public interface GeneralPartnerMapper {
     GeneralPartnerDto daoToDto(GeneralPartnerDao dao);
     GeneralPartnerDao dtoToDao(GeneralPartnerDto dto);
diff --git a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/model/dto/GeneralPartnerDataDto.java b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/model/dto/GeneralPartnerDataDto.java
index 52bcfe67..ddb9e982 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/model/dto/GeneralPartnerDataDto.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/model/dto/GeneralPartnerDataDto.java
@@ -2,6 +2,7 @@
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import jakarta.validation.Valid;
+import jakarta.validation.constraints.Past;
 import jakarta.validation.constraints.Pattern;
 import jakarta.validation.constraints.Size;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.Country;
@@ -53,6 +54,7 @@ public class GeneralPartnerDataDto {
     private String surname;
 
     @JsonProperty(DATE_OF_BIRTH_FIELD)
+    @Past(message = "Date of birth must be in the past")
     private LocalDate dateOfBirth;
 
     @JsonProperty(NATIONALITY1_FIELD)
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 2ac72335..9813d10c 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerService.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerService.java
@@ -1,6 +1,9 @@
 package uk.gov.companieshouse.limitedpartnershipsapi.service;
 
+import org.springframework.core.MethodParameter;
 import org.springframework.stereotype.Service;
+import org.springframework.validation.BeanPropertyBindingResult;
+import org.springframework.validation.BindingResult;
 import org.springframework.web.bind.MethodArgumentNotValidException;
 import uk.gov.companieshouse.GenerateEtagUtil;
 import uk.gov.companieshouse.api.model.transaction.Resource;
@@ -89,13 +92,15 @@ private void updateTransactionWithGeneralPartnerTransactionResourceLinks(
         transactionService.updateTransaction(transaction, requestId);
     }
 
-    public void updateGeneralPartner(String generalPartnerId, GeneralPartnerDataDto generalPartnerDataDto, String requestId, String userId) throws ServiceException {
+    public void updateGeneralPartner(String generalPartnerId, GeneralPartnerDataDto generalPartnerDataDto, String requestId, String userId) throws ServiceException, MethodArgumentNotValidException, NoSuchMethodException {
         var generalPartnerDaoBeforePatch = repository.findById(generalPartnerId).orElseThrow(() -> new ResourceNotFoundException(String.format("Submission with id %s not found", generalPartnerId)));
 
         var generalPartnerDto = mapper.daoToDto(generalPartnerDaoBeforePatch);
 
         mapper.update(generalPartnerDataDto, generalPartnerDto.getData());
 
+        isSecondNationalityDifferent(generalPartnerDto);
+
         var generalPartnerDaoAfterPatch = mapper.dtoToDao(generalPartnerDto);
 
         // Need to ensure we don't lose the meta-data already set on the Mongo document (but lost when DAO is mapped to a DTO)
@@ -108,6 +113,16 @@ public void updateGeneralPartner(String generalPartnerId, GeneralPartnerDataDto
         repository.save(generalPartnerDaoAfterPatch);
     }
 
+    private void isSecondNationalityDifferent(GeneralPartnerDto generalPartnerDto) throws NoSuchMethodException, MethodArgumentNotValidException {
+        var methodParameter = new MethodParameter(GeneralPartnerDataDto.class.getConstructor(), -1);
+        BindingResult bindingResult = new BeanPropertyBindingResult(generalPartnerDto, GeneralPartnerDataDto.class.getName());
+        generalPartnerValidator.isSecondNationalityDifferent(generalPartnerDto, bindingResult);
+
+        if (bindingResult.hasErrors()) {
+            throw new MethodArgumentNotValidException(methodParameter, bindingResult);
+        }
+    }
+
     private void copyMetaDataForUpdate(GeneralPartnerDao generalPartnerDaoBeforePatch,
                                        GeneralPartnerDao generalPartnerDaoAfterPatch) {
         generalPartnerDaoAfterPatch.setId(generalPartnerDaoBeforePatch.getId());
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 6a325256..6928aeb2 100644
--- a/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerValidator.java
+++ b/src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerValidator.java
@@ -85,7 +85,7 @@ private void checkNotNullPerson(GeneralPartnerDataDto generalPartnerDataDto,
         }
     }
 
-    private void isSecondNationalityDifferent(GeneralPartnerDto generalPartnerDto, BindingResult bindingResult) {
+    public void isSecondNationalityDifferent(GeneralPartnerDto generalPartnerDto, BindingResult bindingResult) {
         String nationality1 = generalPartnerDto.getData().getNationality1();
         String nationality2 = generalPartnerDto.getData().getNationality2();
 
diff --git a/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerServiceUpdateTest.java b/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerServiceUpdateTest.java
index f57ee97b..4eddad30 100644
--- a/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerServiceUpdateTest.java
+++ b/src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerServiceUpdateTest.java
@@ -8,8 +8,10 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.web.bind.MethodArgumentNotValidException;
 import uk.gov.companieshouse.limitedpartnershipsapi.exception.ServiceException;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.Country;
+import uk.gov.companieshouse.limitedpartnershipsapi.model.Nationality;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.dao.GeneralPartnerDao;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.dao.GeneralPartnerDataDao;
 import uk.gov.companieshouse.limitedpartnershipsapi.model.dto.AddressDto;
@@ -17,10 +19,12 @@
 import uk.gov.companieshouse.limitedpartnershipsapi.repository.GeneralPartnerRepository;
 
 import java.time.LocalDate;
+import java.util.Objects;
 import java.util.Optional;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -78,7 +82,7 @@ private GeneralPartnerDao createGeneralPartnerLegalEntityDao() {
     }
 
     @Test
-    void shouldUpdateTheDaoWithPrincipalOfficeAddress() throws ServiceException {
+    void shouldUpdateTheDaoWithPrincipalOfficeAddress() throws ServiceException, MethodArgumentNotValidException, NoSuchMethodException {
         GeneralPartnerDao generalPartnerDao = createGeneralPartnerPersonDao();
 
         AddressDto principalOfficeAddress = new AddressDto();
@@ -103,6 +107,7 @@ void shouldUpdateTheDaoWithPrincipalOfficeAddress() throws ServiceException {
 
         GeneralPartnerDao sentSubmission = submissionCaptor.getValue();
 
+        assertEquals("John", sentSubmission.getData().getForename());
         assertEquals("DUNCALF STREET", sentSubmission.getData().getPrincipalOfficeAddress().getAddressLine1());
         assertEquals("GB-ENG", sentSubmission.getData().getPrincipalOfficeAddress().getCountry());
         assertEquals("STOKE-ON-TRENT", sentSubmission.getData().getPrincipalOfficeAddress().getLocality());
@@ -111,7 +116,24 @@ void shouldUpdateTheDaoWithPrincipalOfficeAddress() throws ServiceException {
     }
 
     @Test
-    void shouldUpdateTheDaoWithLegalEntityRegistrationLocation() throws ServiceException {
+    void shouldFailUpdateIfNationalitiesAreSame() {
+        GeneralPartnerDao generalPartnerDao = createGeneralPartnerPersonDao();
+
+        GeneralPartnerDataDto generalPartnerDataDto = new GeneralPartnerDataDto();
+        generalPartnerDataDto.setNationality2(Nationality.AMERICAN);
+
+        when(repository.findById(generalPartnerDao.getId())).thenReturn(Optional.of(generalPartnerDao));
+
+        MethodArgumentNotValidException exception = assertThrows(MethodArgumentNotValidException.class, () ->
+                service.updateGeneralPartner(GENERAL_PARTNER_ID, generalPartnerDataDto, REQUEST_ID, USER_ID)
+        );
+
+        assertEquals("Second nationality must be different from the first", Objects.requireNonNull(exception.getBindingResult().getFieldError("nationality2")).getDefaultMessage());
+
+    }
+
+    @Test
+    void shouldUpdateTheDaoWithLegalEntityRegistrationLocation() throws ServiceException, MethodArgumentNotValidException, NoSuchMethodException {
         GeneralPartnerDao generalPartnerDao = createGeneralPartnerLegalEntityDao();
 
         GeneralPartnerDataDto generalPartnerDataDto = new GeneralPartnerDataDto();


Reviews:
user: login: mattch1
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T15:59:05Z
user: login: markpit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:12:56Z
user: login: lduranteau
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:40:33Z
user: login: lduranteau
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:41:42Z
user: login: lduranteau
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:43:14Z
user: login: markpit
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:55:20Z

Review comments:
diff_hunk: @@ -111,7 +116,24 @@ void shouldUpdateTheDaoWithPrincipalOfficeAddress() throws ServiceException {
     }
 
     @Test
-    void shouldUpdateTheDaoWithLegalEntityRegistrationLocation() throws ServiceException {
+    void shouldFailUpdateIfNationalitiesAreSame() throws ServiceException, MethodArgumentNotValidException, NoSuchMethodException {
path: src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerServiceUpdateTest.java
user: login: markpit
body: the throws exceptions aren't needed on this test signature
created_at: 2025-03-06T16:12:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,7 +116,24 @@ void shouldUpdateTheDaoWithPrincipalOfficeAddress() throws ServiceException {
     }
 
     @Test
-    void shouldUpdateTheDaoWithLegalEntityRegistrationLocation() throws ServiceException {
+    void shouldFailUpdateIfNationalitiesAreSame() throws ServiceException, MethodArgumentNotValidException, NoSuchMethodException {
path: src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerServiceUpdateTest.java
user: login: lduranteau
body: intellij reports it as an error and the tests stop running (crash)
created_at: 2025-03-06T16:40:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,7 +116,24 @@ void shouldUpdateTheDaoWithPrincipalOfficeAddress() throws ServiceException {
     }
 
     @Test
-    void shouldUpdateTheDaoWithLegalEntityRegistrationLocation() throws ServiceException {
+    void shouldFailUpdateIfNationalitiesAreSame() throws ServiceException, MethodArgumentNotValidException, NoSuchMethodException {
path: src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerServiceUpdateTest.java
user: login: lduranteau
body: wait not that one
I will remove them

created_at: 2025-03-06T16:41:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,7 +116,24 @@ void shouldUpdateTheDaoWithPrincipalOfficeAddress() throws ServiceException {
     }
 
     @Test
-    void shouldUpdateTheDaoWithLegalEntityRegistrationLocation() throws ServiceException {
+    void shouldFailUpdateIfNationalitiesAreSame() throws ServiceException, MethodArgumentNotValidException, NoSuchMethodException {
path: src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/service/GeneralPartnerServiceUpdateTest.java
user: login: lduranteau
body: removed
created_at: 2025-03-06T16:43:14Z
author_association: CONTRIBUTOR

PR comments:
user: login: ch-code-analysis
created_at: 2025-03-06T16:37:50Z
body: CI: Security warnings found!
author_association: NONE
user: login: chsonarqubeprchecks[bot]
created_at: 2025-03-06T16:47:58Z
body: **SonarQube Quality Gate**

[![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](http://code-analysis.platform.aws.chdev.org:9000/dashboard?id=groupId%3Alimited-partnerships-api&pullRequest=91)

[![Bug](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/common/bug-16px.png 'Bug')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=groupId%3Alimited-partnerships-api&pullRequest=91&resolved=false&types=BUG) [![A](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/RatingBadge/A-16px.png 'A')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=groupId%3Alimited-partnerships-api&pullRequest=91&resolved=false&types=BUG) [0 Bugs](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=groupId%3Alimited-partnerships-api&pullRequest=91&resolved=false&types=BUG)  
[![Vulnerability](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/common/vulnerability-16px.png 'Vulnerability')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=groupId%3Alimited-partnerships-api&pullRequest=91&resolved=false&types=VULNERABILITY) [![A](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/RatingBadge/A-16px.png 'A')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=groupId%3Alimited-partnerships-api&pullRequest=91&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=groupId%3Alimited-partnerships-api&pullRequest=91&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/common/security_hotspot-16px.png 'Security Hotspot')](http://code-analysis.platform.aws.chdev.org:9000/security_hotspots?id=groupId%3Alimited-partnerships-api&pullRequest=91) [![A](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/RatingBadge/A-16px.png 'A')](http://code-analysis.platform.aws.chdev.org:9000/security_hotspots?id=groupId%3Alimited-partnerships-api&pullRequest=91) [0 Security Hotspots](http://code-analysis.platform.aws.chdev.org:9000/security_hotspots?id=groupId%3Alimited-partnerships-api&pullRequest=91)  
[![Code Smell](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/common/code_smell-16px.png 'Code Smell')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=groupId%3Alimited-partnerships-api&pullRequest=91&resolved=false&types=CODE_SMELL) [![A](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/RatingBadge/A-16px.png 'A')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=groupId%3Alimited-partnerships-api&pullRequest=91&resolved=false&types=CODE_SMELL) [0 Code Smells](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=groupId%3Alimited-partnerships-api&pullRequest=91&resolved=false&types=CODE_SMELL)

[![100.0%](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/CoverageChart/100-16px.png '100.0%')](http://code-analysis.platform.aws.chdev.org:9000/component_measures?id=groupId%3Alimited-partnerships-api&pullRequest=91&metric=new_coverage&view=list) [100.0% Coverage](http://code-analysis.platform.aws.chdev.org:9000/component_measures?id=groupId%3Alimited-partnerships-api&pullRequest=91&metric=new_coverage&view=list)  
[![0.0%](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/Duplications/3-16px.png '0.0%')](http://code-analysis.platform.aws.chdev.org:9000/component_measures?id=groupId%3Alimited-partnerships-api&pullRequest=91&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](http://code-analysis.platform.aws.chdev.org:9000/component_measures?id=groupId%3Alimited-partnerships-api&pullRequest=91&metric=new_duplicated_lines_density&view=list)


author_association: NONE
user: login: ch-code-analysis
created_at: 2025-03-06T17:06:10Z
body: CI: Security warnings found!
author_association: NONE