Skip to content

Added logic for creation, mangemnet and display of case links#1527

Open
aschalew-at wants to merge 27 commits intomasterfrom
HDPI-4510-create-and-view-proposed-linked-cases
Open

Added logic for creation, mangemnet and display of case links#1527
aschalew-at wants to merge 27 commits intomasterfrom
HDPI-4510-create-and-view-proposed-linked-cases

Conversation

@aschalew-at
Copy link
Copy Markdown

Jira link

See [HDPI-4510](https://tools.hmcts.net/jira/browse/HDPI-4510)
[HDPI-4509](https://tools.hmcts.net/jira/browse/HDPI-4509)
[HDPI-4512](https://tools.hmcts.net/jira/browse/HDPI-4512)

Change description

This is about case linking which is linking of cases based on certain reasons.The PR includes changes for creation, managing and displaying of cases links.

Testing done

Security Vulnerability Assessment

CVE Suppression: Are there any CVEs present in the codebase (either newly introduced or pre-existing) that are being intentionally suppressed or ignored by this commit?

  • Yes
  • No

Checklist

  • commit messages are meaningful and follow good commit message guidelines
  • README and other documentation has been updated / added (if needed)
  • tests have been updated / new tests has been added (if needed)
  • Does this PR introduce a breaking change

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 18, 2026

CCD diff summary

👉 Full report: https://github.com/hmcts/pcs-api/actions/runs/23739028491?check_suite_focus=true

AuthorisationCaseState.json

UserRoleCaseStateIDCRUD
+caseworker-pcs
+AWAITING_SUBMISSION_TO_HMCTS
+R
+caseworker-pcs
+PENDING_CASE_ISSUED
+R

CaseField.json

RegularExpressionMaxSearchableMinFieldTypeParameterHintTextLabelIDFieldType
+
+
+
+
+
+
+Component Launcher (for displaying Linked Cases data)
+LinkedCasesComponentLauncher
+ComponentLauncher
+
+
+
+
+CaseLink
+
+Linked cases
+caseLinks
+Collection

AuthorisationCaseEvent/AuthorisationCaseEvent.json

UserRoleCaseEventIDCRUD
+caseworker-pcs
+createCaseLink
+R
+caseworker-pcs-solicitor
+createCaseLink
+CRU
+caseworker-pcs
+maintainCaseLink
+R
+caseworker-pcs-solicitor
+maintainCaseLink
+CRUD

AuthorisationCaseField/caseworker-pcs.json

UserRoleCaseFieldIDCRUD
+caseworker-pcs
+LinkedCasesComponentLauncher
+R
+caseworker-pcs
+caseHistory
+CRU
+caseworker-pcs
+caseLinks
+R
+caseworker-pcs
+caseTitleMarkdown
+R
+caseworker-pcs
+nextStepsMarkdown
+R
+caseworker-pcs
+nextStepsMarkdownLabel
+R
+caseworker-pcs
+propertyAddress
+R
+caseworker-pcs
+waysToPay
+R

AuthorisationCaseField/caseworker-pcs-solicitor.json

UserRoleCaseFieldIDCRUD
+caseworker-pcs-solicitor
+LinkedCasesComponentLauncher
+CRUD
+caseworker-pcs-solicitor
+caseLinks
+CRUD

CaseEvent/maintainCaseLink.json

DescriptionEndButtonLabelIDNamePostConditionStatePreConditionState(s)PublishShowEventNotesShowSummary
+To manage link related cases
+Save and continue
+maintainCaseLink
+Manage case links
+*
+*
+N
+N
+N

CaseEvent/createCaseLink.json

DescriptionEndButtonLabelIDNamePostConditionStatePreConditionState(s)PublishShowEventNotesShowSummary
+To link related cases
+Save and continue
+createCaseLink
+Link cases
+*
+*
+N
+N
+N

CaseEventToFields/maintainCaseLink.json

CaseEventIDCaseFieldIDDisplayContextDisplayContextParameterPageColumnNumberPageDisplayOrderPageFieldDisplayOrderPageIDPageLabelShowSummaryChangeOption
+maintainCaseLink
+LinkedCasesComponentLauncher
+OPTIONAL
+#ARGUMENT(UPDATE,LinkedCases)
+1
+1
+2
+maintainCaseLink
+Case Link
+Y
+maintainCaseLink
+caseLinks
+OPTIONAL
+LinkedCasesComponentLauncher = "DONOTSHOW"
+1
+1
+1
+maintainCaseLink
+Case Link
+Y
+Y

CaseEventToFields/createCaseLink.json

CaseEventIDCaseFieldIDDisplayContextDisplayContextParameterPageColumnNumberPageDisplayOrderPageFieldDisplayOrderPageIDPageLabelShowSummaryChangeOption
+createCaseLink
+LinkedCasesComponentLauncher
+OPTIONAL
+#ARGUMENT(CREATE,LinkedCases)
+1
+1
+2
+createCaseLink
+Case Link
+Y
+createCaseLink
+caseLinks
+OPTIONAL
+LinkedCasesComponentLauncher = "DONOTSHOW"
+1
+1
+1
+createCaseLink
+Case Link
+Y
+Y

CaseTypeTab/6_caseLinkscaseworker-pcs-solicitor.json

CaseFieldIDChannelDisplayContextParameterTabDisplayOrderTabFieldDisplayOrderTabIDTabLabelUserRole
+LinkedCasesComponentLauncher
+CaseWorker
+#ARGUMENT(LinkedCases)
+6
+1
+caseLinkscaseworker-pcs-solicitor
+Linked cases
+caseworker-pcs-solicitor
+caseLinks
+CaseWorker
+#ARGUMENT(LinkedCases)
+LinkedCasesComponentLauncher!=""
+6
+2
+caseLinkscaseworker-pcs-solicitor
+Linked cases
+

@hmcts-jenkins-j-to-z hmcts-jenkins-j-to-z bot requested a deployment to preview March 18, 2026 12:22 Abandoned
@hmcts-jenkins-j-to-z hmcts-jenkins-j-to-z bot requested a deployment to preview March 18, 2026 14:10 Abandoned
@hmcts-jenkins-j-to-z hmcts-jenkins-j-to-z bot requested a deployment to preview March 18, 2026 16:31 Abandoned
@hmcts-jenkins-j-to-z hmcts-jenkins-j-to-z bot requested a deployment to preview March 19, 2026 10:00 Abandoned
@aschalew-at aschalew-at requested a review from a team as a code owner March 19, 2026 10:46
@hmcts-jenkins-j-to-z hmcts-jenkins-j-to-z bot requested a deployment to preview March 19, 2026 11:02 Abandoned
@hmcts-jenkins-j-to-z hmcts-jenkins-j-to-z bot requested a deployment to preview March 19, 2026 15:30 Abandoned
@hmcts-jenkins-j-to-z hmcts-jenkins-j-to-z bot requested a deployment to preview March 19, 2026 17:19 Abandoned
@hmcts-jenkins-j-to-z hmcts-jenkins-j-to-z bot requested a deployment to preview March 20, 2026 07:32 Abandoned
@hmcts-jenkins-j-to-z hmcts-jenkins-j-to-z bot requested a deployment to preview March 20, 2026 09:32 Abandoned
@hmcts-jenkins-j-to-z hmcts-jenkins-j-to-z bot requested a deployment to preview March 20, 2026 11:01 Abandoned
defendantResponse.setPcsCase(this);
}

public void mergeCaseLinks(List<ListValue<CaseLink>> incomingLinkedCases) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

.decentralisedEvent(EventId.maintainCaseLink.name(), this::submit)
.forAllStates()
.name("Manage case links")
.description("To Manage link related cases")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor - small M on "manage". Also it might read better as "To manage linked cases" or something like that. (Is that actually displayed anywhere btw?)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"To manage linked cases" will be the tool tip on the event "Manage case links".

.orElseThrow(() -> new CaseNotFoundException(caseReference));
}

public void patchCase(long caseReference, PCSCase pcsCase) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method name isn't clear on what it is doing. We used to have a single patchCase method that would handle all updates if they weren't null but that has since been changed to have specific methods to only update specific fields. So can you rename this to something like patchCaseLinks ?

pcsCaseEntity.mergeCaseLinks(pcsCase.getCaseLinks());
}

pcsCaseRepository.save(pcsCaseEntity);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need to save the pcsCaseEntity as it is already managed in this scenario.

.collect(Collectors.toList());
}

private List<ListValue<CaseLink>> mapAndWrapCaseLinks(PcsCaseEntity pcsCaseEntity) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this logic into a separate class please? Otherwise this PCSCaseView will get quite large. See ClaimView as an example - you can implement a class with the same setCaseFields(...) method signature and call it in the same way as the other *View instance are called, (https://github.com/hmcts/pcs-api/blob/master/src/main/java/uk/gov/hmcts/reform/pcs/ccd/PCSCaseView.java#L117-L127)

-- ============================================
-- ============================================
-- PCS CASE LINK + LINK REASONS
-- ============================================
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is repeated 🙂

DROP TABLE IF EXISTS case_link_reason CASCADE;

-- Drop case_link table
DROP TABLE IF EXISTS case_link CASCADE;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these DROP statements actually needed, given that Flyway will be applying the migrations in the correct order so the tables and indexes shouldn't exist when this migration script is run.


-- Table: case_link
CREATE TABLE case_link (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor - we rely on the ORM to assign these, so for consistency with the other table definitions could you remove the default?

CREATE TABLE case_link (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
case_id UUID NOT NULL REFERENCES pcs_case(id) ON DELETE CASCADE, -- Source case
linked_case_id BIGINT NOT NULL, -- Linked case number (BIGINT)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that these comments are not needed - the column definition is clear enough.

build.gradle Outdated
"**/uk/gov/hmcts/reform/pcs/ccd/page/**/*.java," +
"**/uk/gov/hmcts/reform/pcs/ccd/entity/**/*.java," +
"**/uk/gov/hmcts/reform/pcs/ccd/event/**/*.java," +
"**/uk/gov/hmcts/reform/pcs/ccd/service/**/*.java," +
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you revert these changes please? We should have code coverage on those packages.

-- ============================================
CREATE TABLE case_link (
id UUID PRIMARY KEY,
case_link_reference UUID NOT NULL REFERENCES pcs_case(id) ON DELETE CASCADE,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This FK is called case_id in all other tables that have it, so it would be better to be consistent with the existing naming convention.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed case_link_reference to case_id and linked_case_id to linked_case_reference.

if (incomingLinkedCases == null) {
this.caseLinks.clear();
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should an empty incoming list remove the existing ones, if this is a merge operation?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole if block can be removed, as incomingLinkedCases can never be null, as this is being checked in the calling method (patchCaseLinks())

caseLinkReasonEntities.add(caseLinkReasonEntity);
}
caseLinkEntity.getReasons().clear();
caseLinkEntity.getReasons().addAll(caseLinkReasonEntities);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to have a setReasons method on the caseLinkEntity rather than fetching the collection and manipulating it, since you don't know whether it is immutable or not.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the reasons field in in CaseLinkEntity is annotated with @onetomany with orphanRemoval set to true, Hibernate tracks the original collection instance and expects the same collection to be mutated and not swapped out.

}

this.caseLinks.clear();
this.caseLinks.addAll(result);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for not just assigning result, (which could have a better name btw), to this.caseLinks ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. The caseLinks field in PcsCaseEntity is annotated with @onetomany with orphanRemoval set to true, Hibernate tracks the original collection instance and expects the same collection to be mutated and not swapped out. result renamed to mergedCaseLinkEntities.

@Override
public SetMultimap<HasRole, Permission> getGrants() {
SetMultimap<HasRole, Permission> grants = HashMultimap.create();
grants.putAll(PCS_SOLICITOR, Permission.CRU);
Copy link
Copy Markdown
Contributor

@guygrewal77 guygrewal77 Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the Delete missing for permissions ? In the event - CreateCaseLink class, solicitor has full CRUD access.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The access profile in CaseLinkinAccess is applied to the fields in PCSCase (for updating cases) do not need the Delete permission in association with the events CreateCaseLink and MaintainLinkCase. In the event class CreateCaseLink, we only need the permissions CRU, whereas in the event class MaintainLinkCase, we need the Delete permission as well. Hence I will remove the Delete permission in CreateCaseLink and keep it in MaintainLinkCase. I will keep CRU in CaseLinkinAccess class

Copy link
Copy Markdown
Contributor

@tvr-hmcts tvr-hmcts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments for consideration

.build();
caseLinkReasonEntities.add(caseLinkReasonEntity);
}
caseLinkEntity.getReasons().clear();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this call needed again here?

}

@Test
void shouldAddCaseLinkReason() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just testing a setter no? Guess I am not understanding the value.


if (dto.getReasonForLink() != null) {
List<CaseLinkReasonEntity> caseLinkReasonEntities = new ArrayList<>();
for (ListValue<LinkReason> incomingLinkReason : dto.getReasonForLink()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be streamed into the reasons collection within the entity rather than create this new arraylist.

@Component
public class CaseLinkView {

public void setCaseFields(PCSCase pcsCase, PcsCaseEntity pcsCaseEntity) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not understanding the point in this wrapper. The same check is made within the other method also. So the other can just be public


private final PcsCaseService pcsCaseService;


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra space

}

@Test
void shouldPatchCaseData() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need another test for when the pcsCase.getCaseLinks() != null

// Given
PcsCaseEntity pcsCaseEntity = mock(PcsCaseEntity.class);
PCSCase caseData = PCSCase.builder().build();
when(pcsCaseRepository.findByCaseReference(CASE_REFERENCE)).thenReturn(java.util.Optional.of(pcsCaseEntity));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import java.util.Optional

underTest.patchCaseLinks(CASE_REFERENCE, caseData);

// Then
verify(caseLinkService, atLeastOnce()).mergeCaseLinks(caseData.getCaseLinks(), pcsCaseEntity);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The number of times can be asserted more strictly here.

}

@Test
void shouldModifyCaseLinksInSubmitCallback() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic in here is a duplication of the other test CreateCaseLinkTest

// Then
List<ListValue<CaseLink>> mappedCaseLinks = pcsCase.getCaseLinks();
assertThat(mappedCaseLinks).hasSize(1);
assertThat(mappedCaseLinks.getFirst().getValue().getCaseReference()).isEqualTo("1234");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mappedCaseLinks.getFirst().getValue() can be extracted and the variable used in the other asserts rather than call the same chain each time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants