From fde53ab30bf29278cd008ee72550b02b3e304dd0 Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Thu, 18 Dec 2025 10:26:48 +0000 Subject: [PATCH 1/3] add stats export for superusers --- .../caseworker/util/EventConstants.java | 1 + .../hmcts/sptribs/ciccase/model/CaseData.java | 9 + .../event/ViewOpenCasesStatisticsEvent.java | 209 ++++++++++++++++++ .../ViewOpenCasesStatisticsEventTest.java | 89 ++++++++ 4 files changed, 308 insertions(+) create mode 100644 src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java create mode 100644 src/test/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEventTest.java diff --git a/src/main/java/uk/gov/hmcts/sptribs/caseworker/util/EventConstants.java b/src/main/java/uk/gov/hmcts/sptribs/caseworker/util/EventConstants.java index 64bbe3620f..e388025f79 100644 --- a/src/main/java/uk/gov/hmcts/sptribs/caseworker/util/EventConstants.java +++ b/src/main/java/uk/gov/hmcts/sptribs/caseworker/util/EventConstants.java @@ -49,6 +49,7 @@ public final class EventConstants { public static final String CASEWORKER_EDIT_PANEL_COMPOSITION = "caseworker-edit-panel-composition"; public static final String CASEWORKER_UPDATE_CASE_DATA = "update-case-data"; public static final String SUPERUSER_REINDEX_CASES = "superuser-reindex-cases"; + public static final String SUPERUSER_VIEW_OPEN_CASES_STATISTICS = "superuser-view-open-cases-statistics"; public static final String CHANGE_SECURITY_CLASS = "change-security-class"; public static final String CASEWORKER_REFER_TO_JUDGE = "refer-to-judge"; public static final String CREATE_BUNDLE = "createBundle"; diff --git a/src/main/java/uk/gov/hmcts/sptribs/ciccase/model/CaseData.java b/src/main/java/uk/gov/hmcts/sptribs/ciccase/model/CaseData.java index 174e7cc0c5..f967272a20 100644 --- a/src/main/java/uk/gov/hmcts/sptribs/ciccase/model/CaseData.java +++ b/src/main/java/uk/gov/hmcts/sptribs/ciccase/model/CaseData.java @@ -548,6 +548,15 @@ public class CaseData { ) private String deleteFieldName; + @CCD( + label = "Case statistics (copy/paste)", + hint = "Copy and paste this into Excel (tab-separated values).", + typeOverride = TextArea, + access = {SuperUserOnlyAccess.class} + ) + @External + private String openCasesStatisticsTsv; + @CCD( label = "Reindex cases modified since", access = {SuperUserOnlyAccess.class} diff --git a/src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java b/src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java new file mode 100644 index 0000000000..92de76ae5d --- /dev/null +++ b/src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java @@ -0,0 +1,209 @@ +package uk.gov.hmcts.sptribs.common.event; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.DataClassRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import uk.gov.hmcts.ccd.sdk.api.CCDConfig; +import uk.gov.hmcts.ccd.sdk.api.CaseDetails; +import uk.gov.hmcts.ccd.sdk.api.ConfigBuilder; +import uk.gov.hmcts.ccd.sdk.api.callback.AboutToStartOrSubmitResponse; +import uk.gov.hmcts.sptribs.ciccase.model.CaseData; +import uk.gov.hmcts.sptribs.ciccase.model.State; +import uk.gov.hmcts.sptribs.ciccase.model.UserRole; +import uk.gov.hmcts.sptribs.common.ccd.CcdServiceCode; +import uk.gov.hmcts.sptribs.common.ccd.PageBuilder; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static uk.gov.hmcts.sptribs.caseworker.util.EventConstants.SUPERUSER_VIEW_OPEN_CASES_STATISTICS; +import static uk.gov.hmcts.sptribs.ciccase.model.UserRole.SUPER_USER; +import static uk.gov.hmcts.sptribs.ciccase.model.access.Permissions.CREATE_READ_UPDATE_DELETE; + +@Component +@RequiredArgsConstructor +public class ViewOpenCasesStatisticsEvent implements CCDConfig { + + private static final String STATE_CLOSED = "CaseClosed"; + private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + private static final int SQL_LIMIT = 10_000; + + private static final String OPEN_CASES_STATISTICS_SQL = """ + select + reference, + created_date, + state, + coalesce(last_state_modified_date, created_date) as state_since, + data ->> 'closeCloseCaseReason' as close_case_reason, + data ->> 'closeRejectionReason' as close_rejection_reason, + data ->> 'closeStrikeOutReason' as close_strike_out_reason, + data ->> 'caseIssueFinalDecisionDecisionTemplate' as final_decision_template, + data ->> 'closeAdditionalDetail' as close_additional_detail, + data ->> 'closeRejectionDetails' as close_rejection_details, + data ->> 'closeStrikeOutDetails' as close_strike_out_details + from ccd.case_data + where case_type_id = ? + order by state_since desc + limit ? + """; + + private final JdbcTemplate jdbcTemplate; + + @Override + public void configure(final ConfigBuilder configBuilder) { + PageBuilder pageBuilder = new PageBuilder(configBuilder + .event(SUPERUSER_VIEW_OPEN_CASES_STATISTICS) + .forAllStates() + .name("View case statistics") + .description("Shows a tab-separated report for copy/paste into Excel (includes closure reasons for CaseClosed)") + .aboutToStartCallback(this::aboutToStart) + .grant(CREATE_READ_UPDATE_DELETE, SUPER_USER)); + + pageBuilder.page("viewOpenCasesStatistics") + .pageLabel("View case statistics") + .label( + "viewOpenCasesStatisticsLabel", + "Copy the report below into Excel (Ctrl/Cmd+A then Ctrl/Cmd+C). Closed cases include closure columns." + ) + .optional(CaseData::getOpenCasesStatisticsTsv) + .done(); + } + + public AboutToStartOrSubmitResponse aboutToStart( + CaseDetails details + ) { + CaseData data = details.getData(); + data.setOpenCasesStatisticsTsv(generateCasesStatisticsTsv()); + + return AboutToStartOrSubmitResponse.builder() + .data(data) + .build(); + } + + private String generateCasesStatisticsTsv() { + OffsetDateTime generatedAt = OffsetDateTime.now(ZoneOffset.UTC); + List rows = fetchCases(); + + StringBuilder tsv = new StringBuilder(); + tsv.append( + "Case reference\tCreated (UTC)\tState\tState since (UTC)\tDays in state\tTime in state" + + "\tClosure reason\tClosure details\n" + ); + + for (CaseStatisticsRow row : rows) { + Duration duration = Duration.between(row.stateSince().atOffset(ZoneOffset.UTC), generatedAt); + ClosureInfo closureInfo = buildClosureInfo(row); + + tsv.append(row.reference()).append('\t') + .append(row.createdDate().format(DATE_TIME_FORMAT)).append('\t') + .append(row.state()).append('\t') + .append(row.stateSince().format(DATE_TIME_FORMAT)).append('\t') + .append(duration.toDays()).append('\t') + .append(formatDuration(duration)) + .append('\t') + .append(escapeTsv(closureInfo.reason())) + .append('\t') + .append(escapeTsv(closureInfo.details())) + .append('\n'); + } + + return tsv.toString(); + } + + private List fetchCases() { + String caseType = CcdServiceCode.ST_CIC.getCaseType().getCaseTypeName(); + return jdbcTemplate.query( + OPEN_CASES_STATISTICS_SQL, + new Object[] {caseType, SQL_LIMIT}, + new DataClassRowMapper<>(CaseStatisticsRow.class) + ); + } + + private ClosureInfo buildClosureInfo(CaseStatisticsRow row) { + if (!STATE_CLOSED.equals(row.state())) { + return new ClosureInfo("", ""); + } + + String closeReason = firstNonBlank(row.closeCaseReason(), row.finalDecisionTemplate()); + if (StringUtils.hasText(closeReason)) { + return new ClosureInfo(closeReason, buildCloseCaseDetails(closeReason, row)); + } + + return new ClosureInfo("", ""); + } + + private String buildCloseCaseDetails(String closeReason, CaseStatisticsRow row) { + StringBuilder details = new StringBuilder(64); + + if ("caseRejected".equals(closeReason)) { + appendWithSeparator(details, row.closeRejectionReason()); + appendWithSeparator(details, row.closeRejectionDetails()); + } else if ("caseStrikeOut".equals(closeReason)) { + appendWithSeparator(details, row.closeStrikeOutReason()); + appendWithSeparator(details, row.closeStrikeOutDetails()); + } + + appendWithSeparator(details, row.closeAdditionalDetail()); + + return details.toString(); + } + + private void appendWithSeparator(StringBuilder sb, String value) { + if (!StringUtils.hasText(value)) { + return; + } + if (sb.length() > 0) { + sb.append(" - "); + } + sb.append(value); + } + + private String firstNonBlank(String first, String second) { + if (StringUtils.hasText(first)) { + return first; + } + if (StringUtils.hasText(second)) { + return second; + } + return ""; + } + + private String formatDuration(Duration duration) { + long days = duration.toDays(); + Duration remaining = duration.minusDays(days); + long hours = remaining.toHours(); + long minutes = remaining.minusHours(hours).toMinutes(); + return "%dd %dh %dm".formatted(days, hours, minutes); + } + + private String escapeTsv(String value) { + if (!StringUtils.hasText(value)) { + return ""; + } + return value.replace('\t', ' ').replace('\n', ' ').replace('\r', ' '); + } + + public record CaseStatisticsRow( + long reference, + LocalDateTime createdDate, + String state, + LocalDateTime stateSince, + String closeCaseReason, + String closeRejectionReason, + String closeStrikeOutReason, + String finalDecisionTemplate, + String closeAdditionalDetail, + String closeRejectionDetails, + String closeStrikeOutDetails + ) { + } + + private record ClosureInfo(String reason, String details) { + } +} diff --git a/src/test/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEventTest.java b/src/test/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEventTest.java new file mode 100644 index 0000000000..1ee1ff57da --- /dev/null +++ b/src/test/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEventTest.java @@ -0,0 +1,89 @@ +package uk.gov.hmcts.sptribs.common.event; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import uk.gov.hmcts.ccd.sdk.ConfigBuilderImpl; +import uk.gov.hmcts.ccd.sdk.api.CaseDetails; +import uk.gov.hmcts.ccd.sdk.api.Event; +import uk.gov.hmcts.ccd.sdk.api.callback.AboutToStartOrSubmitResponse; +import uk.gov.hmcts.sptribs.ciccase.model.CaseData; +import uk.gov.hmcts.sptribs.ciccase.model.State; +import uk.gov.hmcts.sptribs.ciccase.model.UserRole; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static uk.gov.hmcts.sptribs.caseworker.util.EventConstants.SUPERUSER_VIEW_OPEN_CASES_STATISTICS; +import static uk.gov.hmcts.sptribs.testutil.ConfigTestUtil.createCaseDataConfigBuilder; +import static uk.gov.hmcts.sptribs.testutil.ConfigTestUtil.getEventsFrom; + +@ExtendWith(MockitoExtension.class) +class ViewOpenCasesStatisticsEventTest { + + @Mock + private JdbcTemplate jdbcTemplate; + + @InjectMocks + private ViewOpenCasesStatisticsEvent event; + + @Test + void shouldAddConfigurationToConfigBuilder() { + final ConfigBuilderImpl configBuilder = createCaseDataConfigBuilder(); + + event.configure(configBuilder); + + assertThat(getEventsFrom(configBuilder).values()) + .extracting(Event::getId) + .contains(SUPERUSER_VIEW_OPEN_CASES_STATISTICS); + } + + @Test + void shouldPopulateTsvOnAboutToStart() { + final CaseDetails caseDetails = new CaseDetails<>(); + final CaseData caseData = new CaseData(); + caseDetails.setData(caseData); + + LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); + List rows = List.of( + new ViewOpenCasesStatisticsEvent.CaseStatisticsRow( + 123L, + now.minusDays(10), + "CaseClosed", + now.minusDays(1), + "caseRejected", + "deadlineMissed", + null, + null, + "additional detail", + "rejection details", + null + ) + ); + + when(jdbcTemplate.query( + anyString(), + any(Object[].class), + org.mockito.ArgumentMatchers.>any() + )).thenReturn(rows); + + AboutToStartOrSubmitResponse response = event.aboutToStart(caseDetails); + + String tsv = response.getData().getOpenCasesStatisticsTsv(); + assertThat(tsv).contains("Case reference\tCreated (UTC)\tState\tState since (UTC)"); + assertThat(tsv).contains("123\t"); + assertThat(tsv).contains("\tcaseRejected\t"); + assertThat(tsv).contains("deadlineMissed"); + assertThat(tsv).contains("rejection details"); + assertThat(tsv).contains("additional detail"); + } +} From c84a7fb47ca7641e071cd755e9cd3d35ec5dce9a Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Fri, 19 Dec 2025 15:47:39 +0000 Subject: [PATCH 2/3] Remove free text from case stats export Drop free-text closure details from the stats TSV export, simplify exception handling, and increase the row limit to 50k. --- .../event/ViewOpenCasesStatisticsEvent.java | 179 +++--------------- .../ViewOpenCasesStatisticsEventTest.java | 53 +++--- 2 files changed, 55 insertions(+), 177 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java b/src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java index 92de76ae5d..68fad31941 100644 --- a/src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java +++ b/src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java @@ -1,10 +1,10 @@ package uk.gov.hmcts.sptribs.common.event; +import lombok.SneakyThrows; import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.DataClassRowMapper; +import org.postgresql.PGConnection; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import uk.gov.hmcts.ccd.sdk.api.CCDConfig; import uk.gov.hmcts.ccd.sdk.api.CaseDetails; import uk.gov.hmcts.ccd.sdk.api.ConfigBuilder; @@ -12,15 +12,10 @@ import uk.gov.hmcts.sptribs.ciccase.model.CaseData; import uk.gov.hmcts.sptribs.ciccase.model.State; import uk.gov.hmcts.sptribs.ciccase.model.UserRole; -import uk.gov.hmcts.sptribs.common.ccd.CcdServiceCode; import uk.gov.hmcts.sptribs.common.ccd.PageBuilder; -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.util.List; +import java.io.StringWriter; +import java.sql.Connection; import static uk.gov.hmcts.sptribs.caseworker.util.EventConstants.SUPERUSER_VIEW_OPEN_CASES_STATISTICS; import static uk.gov.hmcts.sptribs.ciccase.model.UserRole.SUPER_USER; @@ -30,29 +25,6 @@ @RequiredArgsConstructor public class ViewOpenCasesStatisticsEvent implements CCDConfig { - private static final String STATE_CLOSED = "CaseClosed"; - private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - private static final int SQL_LIMIT = 10_000; - - private static final String OPEN_CASES_STATISTICS_SQL = """ - select - reference, - created_date, - state, - coalesce(last_state_modified_date, created_date) as state_since, - data ->> 'closeCloseCaseReason' as close_case_reason, - data ->> 'closeRejectionReason' as close_rejection_reason, - data ->> 'closeStrikeOutReason' as close_strike_out_reason, - data ->> 'caseIssueFinalDecisionDecisionTemplate' as final_decision_template, - data ->> 'closeAdditionalDetail' as close_additional_detail, - data ->> 'closeRejectionDetails' as close_rejection_details, - data ->> 'closeStrikeOutDetails' as close_strike_out_details - from ccd.case_data - where case_type_id = ? - order by state_since desc - limit ? - """; - private final JdbcTemplate jdbcTemplate; @Override @@ -86,124 +58,33 @@ public AboutToStartOrSubmitResponse aboutToStart( .build(); } + @SneakyThrows private String generateCasesStatisticsTsv() { - OffsetDateTime generatedAt = OffsetDateTime.now(ZoneOffset.UTC); - List rows = fetchCases(); - - StringBuilder tsv = new StringBuilder(); - tsv.append( - "Case reference\tCreated (UTC)\tState\tState since (UTC)\tDays in state\tTime in state" - + "\tClosure reason\tClosure details\n" - ); - - for (CaseStatisticsRow row : rows) { - Duration duration = Duration.between(row.stateSince().atOffset(ZoneOffset.UTC), generatedAt); - ClosureInfo closureInfo = buildClosureInfo(row); - - tsv.append(row.reference()).append('\t') - .append(row.createdDate().format(DATE_TIME_FORMAT)).append('\t') - .append(row.state()).append('\t') - .append(row.stateSince().format(DATE_TIME_FORMAT)).append('\t') - .append(duration.toDays()).append('\t') - .append(formatDuration(duration)) - .append('\t') - .append(escapeTsv(closureInfo.reason())) - .append('\t') - .append(escapeTsv(closureInfo.details())) - .append('\n'); - } - - return tsv.toString(); - } - - private List fetchCases() { - String caseType = CcdServiceCode.ST_CIC.getCaseType().getCaseTypeName(); - return jdbcTemplate.query( - OPEN_CASES_STATISTICS_SQL, - new Object[] {caseType, SQL_LIMIT}, - new DataClassRowMapper<>(CaseStatisticsRow.class) - ); - } - - private ClosureInfo buildClosureInfo(CaseStatisticsRow row) { - if (!STATE_CLOSED.equals(row.state())) { - return new ClosureInfo("", ""); - } - - String closeReason = firstNonBlank(row.closeCaseReason(), row.finalDecisionTemplate()); - if (StringUtils.hasText(closeReason)) { - return new ClosureInfo(closeReason, buildCloseCaseDetails(closeReason, row)); - } - - return new ClosureInfo("", ""); - } - - private String buildCloseCaseDetails(String closeReason, CaseStatisticsRow row) { - StringBuilder details = new StringBuilder(64); - - if ("caseRejected".equals(closeReason)) { - appendWithSeparator(details, row.closeRejectionReason()); - appendWithSeparator(details, row.closeRejectionDetails()); - } else if ("caseStrikeOut".equals(closeReason)) { - appendWithSeparator(details, row.closeStrikeOutReason()); - appendWithSeparator(details, row.closeStrikeOutDetails()); - } - - appendWithSeparator(details, row.closeAdditionalDetail()); - - return details.toString(); - } - - private void appendWithSeparator(StringBuilder sb, String value) { - if (!StringUtils.hasText(value)) { - return; - } - if (sb.length() > 0) { - sb.append(" - "); + String copySql = """ + COPY ( + select + reference::text as "Case reference", + to_char(created_date, 'YYYY-MM-DD HH24:MI') as "Created (UTC)", + state as "State", + to_char(coalesce(last_state_modified_date, created_date), 'YYYY-MM-DD HH24:MI') + as "State since (UTC)", + coalesce(data ->> 'closeCloseCaseReason', '') as "Closure reason", + concat_ws(' - ', + nullif(data ->> 'closeRejectionReason', ''), + nullif(data ->> 'closeStrikeOutReason', ''), + nullif(data ->> 'caseIssueFinalDecisionDecisionTemplate', '') + ) as "Closure details" + from ccd.case_data + where case_type_id = 'CriminalInjuriesCompensation' + limit 50000 + ) TO STDOUT WITH (FORMAT csv, DELIMITER E'\\t', HEADER TRUE, NULL ''); + """; + + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + StringWriter writer = new StringWriter(256 * 1024); + PGConnection pgConnection = connection.unwrap(PGConnection.class); + pgConnection.getCopyAPI().copyOut(copySql, writer); + return writer.toString(); } - sb.append(value); - } - - private String firstNonBlank(String first, String second) { - if (StringUtils.hasText(first)) { - return first; - } - if (StringUtils.hasText(second)) { - return second; - } - return ""; - } - - private String formatDuration(Duration duration) { - long days = duration.toDays(); - Duration remaining = duration.minusDays(days); - long hours = remaining.toHours(); - long minutes = remaining.minusHours(hours).toMinutes(); - return "%dd %dh %dm".formatted(days, hours, minutes); - } - - private String escapeTsv(String value) { - if (!StringUtils.hasText(value)) { - return ""; - } - return value.replace('\t', ' ').replace('\n', ' ').replace('\r', ' '); - } - - public record CaseStatisticsRow( - long reference, - LocalDateTime createdDate, - String state, - LocalDateTime stateSince, - String closeCaseReason, - String closeRejectionReason, - String closeStrikeOutReason, - String finalDecisionTemplate, - String closeAdditionalDetail, - String closeRejectionDetails, - String closeStrikeOutDetails - ) { - } - - private record ClosureInfo(String reason, String details) { } } diff --git a/src/test/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEventTest.java b/src/test/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEventTest.java index 1ee1ff57da..366e0036cc 100644 --- a/src/test/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEventTest.java +++ b/src/test/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEventTest.java @@ -5,8 +5,9 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.postgresql.PGConnection; +import org.postgresql.copy.CopyManager; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; import uk.gov.hmcts.ccd.sdk.ConfigBuilderImpl; import uk.gov.hmcts.ccd.sdk.api.CaseDetails; import uk.gov.hmcts.ccd.sdk.api.Event; @@ -15,13 +16,14 @@ import uk.gov.hmcts.sptribs.ciccase.model.State; import uk.gov.hmcts.sptribs.ciccase.model.UserRole; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.List; +import java.io.Writer; +import java.sql.Connection; +import javax.sql.DataSource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; import static uk.gov.hmcts.sptribs.caseworker.util.EventConstants.SUPERUSER_VIEW_OPEN_CASES_STATISTICS; import static uk.gov.hmcts.sptribs.testutil.ConfigTestUtil.createCaseDataConfigBuilder; @@ -48,33 +50,30 @@ void shouldAddConfigurationToConfigBuilder() { } @Test - void shouldPopulateTsvOnAboutToStart() { + void shouldPopulateTsvOnAboutToStart() throws Exception { final CaseDetails caseDetails = new CaseDetails<>(); final CaseData caseData = new CaseData(); caseDetails.setData(caseData); - LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); - List rows = List.of( - new ViewOpenCasesStatisticsEvent.CaseStatisticsRow( - 123L, - now.minusDays(10), - "CaseClosed", - now.minusDays(1), - "caseRejected", - "deadlineMissed", - null, - null, - "additional detail", - "rejection details", - null - ) - ); + DataSource dataSource = org.mockito.Mockito.mock(DataSource.class); + Connection connection = org.mockito.Mockito.mock(Connection.class); + PGConnection pgConnection = org.mockito.Mockito.mock(PGConnection.class); + CopyManager copyManager = org.mockito.Mockito.mock(CopyManager.class); - when(jdbcTemplate.query( - anyString(), - any(Object[].class), - org.mockito.ArgumentMatchers.>any() - )).thenReturn(rows); + when(jdbcTemplate.getDataSource()).thenReturn(dataSource); + when(dataSource.getConnection()).thenReturn(connection); + when(connection.unwrap(PGConnection.class)).thenReturn(pgConnection); + when(pgConnection.getCopyAPI()).thenReturn(copyManager); + + doAnswer(invocation -> { + Writer writer = invocation.getArgument(1); + writer.write( + "Case reference\tCreated (UTC)\tState\tState since (UTC)\tClosure reason\tClosure details\n" + + "123\t2025-12-18 10:00\tCaseClosed\t2025-12-17 10:00\tcaseRejected\t" + + "deadlineMissed\n" + ); + return 1L; + }).when(copyManager).copyOut(anyString(), any(Writer.class)); AboutToStartOrSubmitResponse response = event.aboutToStart(caseDetails); @@ -83,7 +82,5 @@ void shouldPopulateTsvOnAboutToStart() { assertThat(tsv).contains("123\t"); assertThat(tsv).contains("\tcaseRejected\t"); assertThat(tsv).contains("deadlineMissed"); - assertThat(tsv).contains("rejection details"); - assertThat(tsv).contains("additional detail"); } } From e5e09fe2a765bf5ad01f090012abedea93da4f2d Mon Sep 17 00:00:00 2001 From: Alex McAusland Date: Fri, 19 Dec 2025 16:08:45 +0000 Subject: [PATCH 3/3] Fix checkstyle import order --- .../sptribs/common/event/ViewOpenCasesStatisticsEvent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java b/src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java index 68fad31941..13edadb312 100644 --- a/src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java +++ b/src/main/java/uk/gov/hmcts/sptribs/common/event/ViewOpenCasesStatisticsEvent.java @@ -1,7 +1,7 @@ package uk.gov.hmcts.sptribs.common.event; -import lombok.SneakyThrows; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import org.postgresql.PGConnection; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component;