diff --git a/README.md b/README.md index 9cbd45f79..dd190d6a9 100644 --- a/README.md +++ b/README.md @@ -119,5 +119,4 @@ X-Powered-By: Undertow/1 Server: WildFly/10 Content-Length: 0 Date: Tue, 03 Dec 2019 16:14:28 GMT -``` - +``` \ No newline at end of file diff --git a/docs/big-changes/ccsph2-change-analysis.md b/docs/big-changes/ccsph2-change-analysis.md new file mode 100644 index 000000000..b9e3f4056 --- /dev/null +++ b/docs/big-changes/ccsph2-change-analysis.md @@ -0,0 +1,454 @@ +# CCSP H2 Change Analysis: Court Schedule Integration for CROWN + +Comparison of `team/ccsph2` vs `team/hmiremoval` (baseline). +Only substantive changes are listed — trivial refactors (pom version bumps, `JsonObjects` → `javax.json.Json` static import migrations, whitespace) are excluded from analysis. + +--- + +## Legend + +- **Journey**: Which journey from `journeys.md` is affected +- **Verdict**: NEEDED / QUESTIONABLE / BUG / CLEANUP +- **Status**: PENDING APPROVAL + +--- + +## Change 1: `CourtScheduleEnrichmentService` — `needsCourtScheduleEnrichment()` now includes CROWN + +**File:** `listing-command/listing-command-api/src/main/java/.../CourtScheduleEnrichmentService.java` + +**What changed:** Previously only returned `true` for MAGISTRATES. Now also returns `true` for CROWN when hearing days are present or the hearing is a candidate for allocation. + +**Journey:** Adhoc Hearing (1), Unallocated→Allocated (2), Update Allocated (3), Adjournment Path C (4) + +**Analysis:** This is the core gate that enables court schedule enrichment for CROWN. Without this, no CROWN hearing would go through court schedule enrichment. This is **required** for the feature. + +**Potential issue:** For CROWN, the condition is `!isEmpty(hearing.getHearingDays()) || isCandidateForAllocation(hearing)`. This means even CROWN hearings that are week-commencing (which you said should be excluded) could enter enrichment if they have hearing days populated. The exclusion of week-commencing must happen inside the enrichment logic (and it does — see Change 2). This is acceptable but could be made explicit here for clarity. + +**Verdict:** NEEDED +**Status:** APPROVED + +--- + +## Change 2: `CourtScheduleEnrichmentService` — New CROWN enrichment methods + +**File:** `listing-command/listing-command-api/src/main/java/.../CourtScheduleEnrichmentService.java` + +**What changed:** Added ~350 lines of new methods: +- `isListNextHearingsV2Scenario()` / `handleListNextHearingsV2Enrichment()` — handles enrichment for list-next-hearings-v2 (Adjournment path C) +- `handleAllocationCandidateWithAssignedSessions()` — searches for assigned sessions (SHRT/LNG business types) for allocated hearings +- `handleUnallocatedWithDraftSessions()` — searches for draft sessions for unallocated hearings +- `handleDirectListingCase()` — uses supplied courtScheduleIds directly +- `handleCrownMultiDaySlotSearch()` — searches slots per hearing day for multi-day hearings +- `applyCourtScheduleRulesForChangeJudiciary()` — applies court schedule rules when judiciary changes +- `getCourtScheduleDraftStatus()` — queries draft/assigned status of court schedule IDs +- Null-safety improvements in `combineSearchAndBookResponseAndListResponse()` for hearing days without courtScheduleId + +**Journey:** All journeys (1-4) depending on path + +**Analysis:** This is the main feature implementation. The logic correctly: +- Distinguishes between ALLOCATED (assigned sessions) and UNALLOCATED (draft sessions) +- Handles the case where courtScheduleIds are supplied by the front end vs needing to search +- Excludes week-commencing hearings from court schedule integration +- Handles multi-day hearings + +**Potential issues:** +1. `handleUnallocatedWithDraftSessions()` calls `searchAndBookSlotsWithDraftStatus()` — ensure this method exists and works correctly with the court scheduler service. If the court scheduler doesn't support draft filtering, this will silently fail. +2. The `applyCourtScheduleRulesForChangeJudiciary()` method creates a new list-in-court-sessions payload and calls `hearingSlotsService.listHearingInCourtSessions()` — verify the court scheduler accepts this for CROWN jurisdiction. + +**Verdict:** NEEDED +**Status:** PENDING APPROVAL + +--- + +## Change 3: `CourtScheduleEnrichmentService` — Null-safety for `combineSearchAndBookResponseAndListResponse()` + +**File:** `listing-command/listing-command-api/src/main/java/.../CourtScheduleEnrichmentService.java` + +**What changed:** Added null-checks for `requestedHearingDay.getCourtScheduleId()` before calling `.toString()`. Hearing days without courtScheduleId are now added as-is instead of throwing NPE. + +**Journey:** All journeys — this method is used by both MAGS and CROWN enrichment flows. + +**Analysis:** This is a **bug fix**. Previously, if a hearing day didn't have a courtScheduleId, the code would NPE on `requestedHearingDay.getCourtScheduleId().toString()`. Since CROWN hearings may have some hearing days without courtScheduleIds (especially during transition), this fix is critical. + +**Verdict:** NEEDED (bug fix) +**Status:** PENDING APPROVAL + +--- + +## Change 4: `HearingEnrichmentOrchestrator` — CROWN enrichment pipeline reordered (CourtSchedule first) + +**File:** `listing-command/listing-command-api/src/main/java/.../HearingEnrichmentOrchestrator.java` + +**What changed:** +- CROWN enrichment pipeline reordered from `HearingDays → Duration → CourtSchedule` to `CourtSchedule → HearingDays → Duration` +- `enrichListCourtHearing()` CROWN path: now calls `courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst()` FIRST, then HearingDays, then Duration +- `enrichUpdateHearingForListing()` CROWN path (both overloads): same reorder — `enrichCrownCourtScheduleFirst()` first +- `enrichCrownCourtScheduleFirst()` determines 3 cases: no courtScheduleId (return unchanged), multi-day (multiDaySearchAndBook), single-day (listHearingInCourtSessions) +- Duration aggregation for multi-day decision uses `calculateAggregatedDuration()`: hearingDays → nonDefaultDays → estimatedMinutes +- MAGS pipeline order unchanged + +**Journey:** +- Adhoc Hearing (1): `enrichListCourtHearing()` path — CROWN court schedule enriched first +- Unallocated→Allocated (2): `enrichUpdateHearingForListing()` path — CROWN court schedule enriched first +- Update Allocated (3): `enrichUpdateHearingForListing()` path — CROWN court schedule enriched first +- Adjournment Path C (4): `enrichListCourtHearing()` via `listing.list-next-hearings-v2` — CROWN court schedule enriched first + +**Analysis:** This is the orchestration layer that wires court schedule enrichment into the CROWN flow. The court scheduler is the source of truth for session dates, times, rooms, and judiciary — running it first means HearingDays and Duration derive from scheduler data rather than building speculatively. See `docs/development-guidelines/crown-courtschedule-design-decisions.md` §5a for full rationale. + +**Potential issue:** In `enrichCrownWithSessionStatus()`, if `getCourtScheduleDraftStatus()` returns an empty map (e.g., court scheduler is down), the code defaults to assuming sessions are assigned (`return true`). This means a network failure could cause a hearing to be incorrectly allocated. Consider whether this should fail loudly instead. + +**Verdict:** NEEDED — but consider whether silent fallback to "assigned" on network error is safe +**Status:** PENDING APPROVAL + +--- + +## Change 5: `ListingCommandApi` — CROWN validation for `update-hearing-for-listing` + +**File:** `listing-command/listing-command-api/src/main/java/.../ListingCommandApi.java` + +**What changed:** +- New `validateCrownJurisdictionRequirements()` method: for CROWN, validates that all hearing days have a courtScheduleId and that all courtScheduleIds have the same state (all draft or all assigned) +- New `validateCrownListCourtHearingRequirements()` method: for CROWN list-court-hearing with multiple hearing days, validates consistency of courtScheduleId presence and state +- New `validateCourtScheduleIdsHaveSameState()` helper +- Validation called before enrichment in `handleUpdateHearingForListing()` and `handleListCourtHearing()` + +**Journey:** +- Unallocated→Allocated (2): `handleUpdateHearingForListing()` validates before enrichment +- Update Allocated (3): same +- Adhoc Hearing (1): `handleListCourtHearing()` validates per hearing + +**Analysis:** These validations enforce your business rules: +- All CROWN hearing days must have courtScheduleId (except single-day or no hearing days in list-court-hearing) +- All courtScheduleIds must be in the same state (all draft OR all assigned — no mix) + +**Potential issue in `validateCrownListCourtHearingRequirements()`:** The validation skips hearings with ≤1 hearing day. This is fine for adhoc hearings, but for list-court-hearing with multiple cases, each `HearingListingNeeds` is validated independently. If a single-day hearing doesn't have a courtScheduleId, it passes validation silently — is this intended for CROWN? Given that front end will always supply courtScheduleIds, this may be acceptable as a safety net. + +**Verdict:** NEEDED +**Status:** PENDING APPROVAL + +--- + +## Change 6: `ListingCommandApi` — `handleChangeJudiciaryForHearings()` now resolves hearing details + +**File:** `listing-command/listing-command-api/src/main/java/.../ListingCommandApi.java` + +**What changed:** Previously just forwarded the envelope. Now: +1. Deserializes the `ChangeJudiciaryForHearings` payload +2. For each hearing ID, fetches the hearing from viewstore via `HearingByIdProvider` +3. Converts to `HearingListingNeeds` via `ViewstoreHearingToHearingListingNeedsConverter` +4. Calls `courtScheduleEnrichmentService.applyCourtScheduleRulesForChangeJudiciary()` to update court sessions with new judiciary +5. Still sends the original envelope to the handler + +**Journey:** Not directly in our journeys.md, but `listing.command.change-judiciary-for-hearings` is a separate user action that affects CROWN hearings with court schedules. + +**Analysis:** This is needed so that when judiciary changes on a CROWN hearing, the court scheduler is notified to update the sessions with the new judiciary. Without this, the court schedule sessions would have stale judiciary information. + +**Potential issues:** +1. **Performance:** For each hearing, it queries the viewstore, converts, and calls the court scheduler. If a user changes judiciary for many hearings at once, this could be slow. +2. **Dependency on viewstore in command API:** `HearingByIdProvider` injects `HearingQueryView` (a query component) into the command API. This creates a read dependency in the command path, which is architecturally unusual in CQRS. However, this is pragmatic — the alternative would be replaying the aggregate for each hearing which is also expensive. +3. **The original envelope is still sent unchanged** — so the handler processes the original command regardless. The court scheduler call is a side effect. If the court scheduler call fails silently, the hearing aggregate still gets the judiciary update but the sessions are stale. + +**Verdict:** NEEDED — but the architectural coupling (command API querying viewstore) and silent failure mode should be noted +**Status:** PENDING APPROVAL + +--- + +## Change 7: `HearingByIdProvider` (new file) + +**File:** `listing-command/listing-command-api/src/main/java/.../HearingByIdProvider.java` + +**What changed:** New class that fetches hearing data from the viewstore by ID. Used by `handleChangeJudiciaryForHearings()`. + +**Analysis:** Supporting class for Change 6. Returns `Optional.empty()` on `NotFoundException`. + +**Verdict:** NEEDED (supports Change 6) +**Status:** PENDING APPROVAL + +--- + +## Change 8: `ViewstoreHearingToHearingListingNeedsConverter` (new file) + +**File:** `listing-command/listing-command-api/src/main/java/.../ViewstoreHearingToHearingListingNeedsConverter.java` + +**What changed:** New class that converts viewstore hearing JSON to `HearingListingNeeds` DTO. Handles hearing days, court centre, court room, court schedule IDs, and skips cancelled hearing days. + +**Analysis:** Supporting class for Change 6. Clean converter with proper null handling. + +**Verdict:** NEEDED (supports Change 6) +**Status:** PENDING APPROVAL + +--- + +## Change 9: `Hearing` aggregate — `ListingStatus` enum and `determineListingStatus()` + +**File:** `listing-domain/listing-domain-aggregate/src/main/java/.../Hearing.java` + +**What changed:** +- New `ListingStatus` enum: `ALLOCATED`, `UNALLOCATED`, `WEEK_COMMENCING`, `UNSCHEDULED` +- New `determineListingStatus()` static method: classifies a hearing based on its properties +- New `hasAssignedSession` field: tracks whether court schedule sessions are assigned (not draft) for CROWN + +**Journey:** All journeys — this affects aggregate behavior + +**Analysis:** The `ListingStatus` enum is used by `CourtScheduleEnrichmentService` to determine how to handle enrichment. The `hasAssignedSession` field is used by `canAllocateForCrown()` to determine if the hearing can be allocated. + +**Verdict:** NEEDED +**Status:** PENDING APPROVAL + +--- + +## Change 10: `Hearing` aggregate — `canAllocateForCrown()` now requires `hasAssignedSession` + +**File:** `listing-domain/listing-domain-aggregate/src/main/java/.../Hearing.java` + +**What changed:** `canAllocateForCrown()` now requires `Boolean.TRUE.equals(this.hasAssignedSession)` in addition to existing conditions (language, jurisdiction, courtRoom, dates). + +**Journey:** Unallocated→Allocated (2), Update Allocated (3), Adjournment (4) + +**Analysis:** This is a **critical change**. It means a CROWN hearing can only be allocated if the court schedule sessions are confirmed as assigned (not draft). This implements your rule that allocated hearings must have `draft=false` sessions. + +**Potential issue:** If `hasAssignedSession` is never set (e.g., for hearings created before this feature), the hearing can never be allocated. This could break existing CROWN hearings. Ensure that: +1. Existing allocated CROWN hearings have their `hasAssignedSession` field set during replay/catchup +2. The `onHearingAllocatedForListingV2()` event handler sets `hasAssignedSession = true` (it does — see below) + +**Verdict:** NEEDED — but verify backward compatibility with existing CROWN hearings +**Status:** PENDING APPROVAL + +--- + +## Change 11: `Hearing` aggregate — `assignCourtRoom()` / `removeCourtRoom()` changes + +**File:** `listing-domain/listing-domain-aggregate/src/main/java/.../Hearing.java` + +**What changed:** +- `assignCourtRoom()`: For CROWN, sets `hasAssignedSession = true`. Now also calls `updateCourtRoomId()` immediately before generating events, and calls `checkAndTriggerAllocationStatus()` after courtroom events. +- `removeCourtRoom()`: For CROWN, sets `hasAssignedSession = false`. Same immediate state update and allocation check pattern. +- New `checkAndTriggerAllocationStatus()`: checks if hearing should be allocated/unallocated and triggers appropriate events. + +**Journey:** Unallocated→Allocated (2), Update Allocated (3) + +**Analysis:** This ensures that when a courtroom is assigned to a CROWN hearing (meaning sessions are assigned/not draft), the allocation status is re-evaluated. And when a courtroom is removed (meaning sessions became draft), the hearing can become unallocated. + +**Potential issue — STATE MUTATION BEFORE EVENTS:** The code calls `updateCourtRoomId(courtRoomId)` before generating the `CourtRoomAssignedToHearing` event. This changes the aggregate's internal state before the event is applied. In event-sourced aggregates, state should typically change only in response to events. However, this appears to be necessary so that `checkAndTriggerAllocationStatus()` can see the updated courtRoomId. This is a pragmatic trade-off but **could cause issues during replay** if the event handler also updates courtRoomId (double update). Verify that the event handler for `CourtRoomAssignedToHearing` is idempotent with respect to courtRoomId. + +**Verdict:** NEEDED — but verify event replay safety +**Status:** PENDING APPROVAL + +--- + +## Change 12: `Hearing` aggregate — CROWN now triggers `availableSlotsForHearingFreed` on vacate and eject + +**File:** `listing-domain/listing-domain-aggregate/src/main/java/.../Hearing.java` + +**What changed:** +- `vacateTrial()`: Previously only MAGISTRATES fired `availableSlotsForHearingFreed`. Now CROWN also fires it when a vacating reason is provided. +- `deleteAllocatedHearing()`: Previously only MAGISTRATES fired `availableSlotsForHearingFreed`. Now CROWN also fires it. +- `ejectCase()` (via `listCasesForEviction`): Previously only MAGISTRATES fired `availableSlotsForHearingFreed`. Now CROWN also fires it. + +**Journey:** Update Allocated (3) for vacate/delete, plus eject-case flows (not yet in journeys.md) + +**Analysis:** This ensures that when a CROWN hearing is vacated, deleted, or a case is ejected, the court scheduler is notified to free the slots. Without this, court schedule sessions would remain booked even though the hearing is gone. This is **required** for proper court schedule lifecycle management. + +**Verdict:** NEEDED +**Status:** PENDING APPROVAL + +--- + +## Change 13: `Hearing` aggregate — `onHearingAllocatedForListingV2()` sets `hasAssignedSession` + +**File:** `listing-domain/listing-domain-aggregate/src/main/java/.../Hearing.java` + +**What changed:** When replaying a `HearingAllocatedForListingV2` event, if jurisdiction is CROWN and courtRoomId is present, sets `hasAssignedSession = true`. Also updates courtRoomId from the event. + +**Journey:** All — this is during aggregate replay + +**Analysis:** This is needed for backward compatibility. When replaying events for existing CROWN hearings that were allocated, this ensures `hasAssignedSession` is set correctly so that `canAllocateForCrown()` works. + +**Verdict:** NEEDED +**Status:** PENDING APPROVAL + +--- + +## Change 14: `ListingCommandHandler` — `changeJudiciaryForHearings()` CROWN-specific logic + +**File:** `listing-command/listing-command-handler/src/main/java/.../ListingCommandHandler.java` + +**What changed:** +- New `CrownHearingType` enum: `UNSCHEDULED`, `WEEK_COMMENCING`, `ALLOCATED`, `UNALLOCATED` +- New `classifyCrownHearingType()` method +- `changeJudiciaryForHearings()`: For CROWN UNSCHEDULED and WEEK_COMMENCING hearings, only applies judicial events (skips allocation rules). For ALLOCATED and UNALLOCATED, applies both judicial events and allocation rules as before. + +**Journey:** Not in journeys.md, but `listing.command.change-judiciary-for-hearings` is a user action + +**Analysis:** This prevents unnecessary allocation rule processing for CROWN hearings that can't be allocated (unscheduled, week-commencing). For allocated/unallocated CROWN hearings, allocation rules still run which may trigger court schedule updates. + +**Verdict:** NEEDED +**Status:** PENDING APPROVAL + +--- + +## Change 15: `HearingDaysEnrichmentService` — Null-safety for `enrichCandidate()` + +**File:** `listing-command/listing-command-api/src/main/java/.../HearingDaysEnrichmentService.java` + +**What changed:** Added null check for `startTime` in `enrichCandidate()`. If both `listedStartDateTime` and `earliestStartDateTime` are null, logs a warning and returns early instead of NPE. + +**Journey:** Adhoc Hearing (1), Adjournment Path C (4) + +**Analysis:** This is a **bug fix**. CROWN hearings may not have either start time set in some scenarios, and this would cause an NPE during hearing days enrichment. + +**Verdict:** NEEDED (bug fix) +**Status:** PENDING APPROVAL + +--- + +## Change 16: `HearingDaysEnrichmentService` — Removed dead code `enrichNonSittingDaysForCrown(HearingListingNeeds)` + +**File:** `listing-command/listing-command-api/src/main/java/.../HearingDaysEnrichmentService.java` + +**What changed:** Removed unused overload `enrichNonSittingDaysForCrown(HearingListingNeeds)` that always returned null. + +**Verdict:** CLEANUP — harmless +**Status:** PENDING APPROVAL + +--- + +## Change 17: `DefaultQueryApiHearingSlotsResource` — New query parameters + +**File:** `listing-query/listing-query-api/src/main/java/.../DefaultQueryApiHearingSlotsResource.java` + +**What changed:** +- Added `status`, `consecutiveDays`, `isWeekCommencing` query parameters +- If `isWeekCommencing=true`, returns empty result immediately (week-commencing excluded from slot search) +- Parameters forwarded to court scheduler service + +**Journey:** Not directly in journeys — this is the hearing slots search query used by the listing UI + +**Analysis:** These new parameters support CROWN-specific slot searching: +- `status`: filter by draft/assigned session status +- `consecutiveDays`: for multi-day trial slot searching +- `isWeekCommencing`: immediately returns empty if true (week-commencing hearings excluded from court schedule) + +**Verdict:** NEEDED +**Status:** PENDING APPROVAL + +--- + +## Change 18: `HearingQueryView` — `searchHearingsWithAnyAllocationState()` simplified + +**File:** `listing-query/listing-query-view/src/main/java/.../HearingQueryView.java` + +**What changed:** Removed `startDate` parameter. Now searches by caseUrn only without date filtering. + +**Journey:** Not directly in journeys — this is a query used by the split/merge hearing UI + +**Analysis:** This broadens the search to find all hearings regardless of start date. This could return more results than before. The corresponding `HearingRepository` query was also changed. + +**Potential issue:** Removing the start date filter could return very old hearings. Verify this is intentional. + +**Verdict:** QUESTIONABLE — may be unrelated to court schedule integration. Confirm if this is needed for the CCSP H2 feature. +**Status:** PENDING APPROVAL + +--- + +## Change 19: `HearingRepository` — `findHearingsByCaseUrnAndAnyAllocationState()` query change + +**File:** `listing-viewstore/listing-viewstore-persistence/src/main/java/.../HearingRepository.java` + +**What changed:** +- Removed `startDate` parameter and its filter conditions +- Changed from subquery EXISTS pattern to LEFT JOIN pattern +- Added `distinct` to prevent duplicates from joins +- Changed case reference matching to use `UPPER(cast(...))` pattern + +**Analysis:** Same as Change 18 — broadens the search. The LEFT JOIN + WHERE pattern is functionally equivalent but may have different performance characteristics. + +**Potential issue:** Using `UPPER(cast(lc.case_reference as varchar))` is different from the previous exact match. The previous code used `toUpperCase()` in Java then matched exactly in SQL. The new code does UPPER in SQL. Functionally equivalent but verify performance. + +**Verdict:** QUESTIONABLE — same concern as Change 18 +**Status:** PENDING APPROVAL + +--- + +## Change 20: `HearingSearchSyncService` — Removed `.toUpperCase()` on case/application references + +**File:** `listing-event/listing-event-listener/src/main/java/.../HearingSearchSyncService.java` + +**What changed:** No longer uppercases `caseReference` and `applicationReference` when indexing into the search/viewstore. + +**Analysis:** This changes the data stored in the viewstore. Previously references were uppercased before storage. Now they're stored as-is. This aligns with Change 19 which does `UPPER()` in SQL at query time. + +**Potential issue:** If there are existing records with uppercased references and new records without, queries might need to handle both. The SQL `UPPER()` in Change 19 handles this, but other queries that don't use `UPPER()` might break. + +**Verdict:** QUESTIONABLE — appears to be a separate data quality change, not directly related to court schedule integration +**Status:** PENDING APPROVAL + +--- + +## Change 21: `HearingDaysUpdateEventListener` — Simplified `correctNonDefaultDaysWithoutCourtCentre()` + +**File:** `listing-event/listing-event-listener/src/main/java/.../HearingDaysUpdateEventListener.java` + +**What changed:** Replaced `determineRoomId()` and `determineCourtCentreId()` helper methods with inline null checks. Removed null-safe handling that the helpers provided — the new code will NPE if `courtRoomId` is null (calls `courtRoomId.toString()` without null check). + +**Journey:** Unallocated→Allocated (2), Update Allocated (3) — viewstore update path + +**Analysis:** This is a **potential regression/bug**. The previous `determineRoomId()` method returned `nonNull(courtRoomId) ? courtRoomId.toString() : null` when the nonDefaultDay's roomId was null. The new code does `null == nonDefaultDay.getRoomId() ? courtRoomId.toString() : nonDefaultDay.getRoomId()` — if `courtRoomId` is null AND `nonDefaultDay.getRoomId()` is null, this will NPE on `courtRoomId.toString()`. For CROWN unallocated hearings where courtRoomId may be null, this could be a problem. + +**Verdict:** BUG — potential NPE when courtRoomId is null for non-default days on unallocated CROWN hearings +**Status:** PENDING APPROVAL — recommend reverting to the null-safe version from hmiremoval + +--- + +## Change 22: Liquibase — Deleted `027-add-hearing-ref-indexes.xml` + +**File:** `listing-viewstore/listing-viewstore-liquibase/src/main/resources/liquibase/listing-view-store-db-changesets/027-add-hearing-ref-indexes.xml` + +**What changed:** Removed indexes on `listed_cases.case_reference` and `court_applications.application_reference`. + +**Analysis:** These indexes were added in hmiremoval. Removing them means: +- `findHearingsByCaseUrnAndAnyAllocationState()` (Change 19) may be slower without these indexes +- The UPPER() SQL function in Change 19 wouldn't use these indexes anyway (would need functional indexes) + +**Verdict:** QUESTIONABLE — the indexes were useful for the original query. If Change 19 is kept, these indexes are less useful (UPPER() won't use them). But if Change 19 is reverted, they should be kept. +**Status:** PENDING APPROVAL + +--- + +## Summary of Verdicts + +| # | File/Area | Verdict | Notes | +|---|-----------|---------|-------| +| 1 | `needsCourtScheduleEnrichment()` CROWN gate | NEEDED | Core enabler | +| 2 | New CROWN enrichment methods | NEEDED | Main feature | +| 3 | Null-safety in `combineSearchAndBookResponseAndListResponse` | NEEDED | Bug fix | +| 4 | `HearingEnrichmentOrchestrator` CROWN flow | NEEDED | Orchestration | +| 5 | `ListingCommandApi` CROWN validation | NEEDED | Business rules | +| 6 | `handleChangeJudiciaryForHearings()` enrichment | NEEDED | Court scheduler sync | +| 7 | `HearingByIdProvider` (new) | NEEDED | Supports #6 | +| 8 | `ViewstoreHearingToHearingListingNeedsConverter` (new) | NEEDED | Supports #6 | +| 9 | `Hearing` aggregate — `ListingStatus` enum | NEEDED | Classification | +| 10 | `canAllocateForCrown()` requires `hasAssignedSession` | NEEDED | Core rule | +| 11 | `assignCourtRoom()`/`removeCourtRoom()` changes | NEEDED | Allocation lifecycle | +| 12 | CROWN `availableSlotsForHearingFreed` on vacate/eject | NEEDED | Slot lifecycle | +| 13 | `onHearingAllocatedForListingV2` replay fix | NEEDED | Backward compat | +| 14 | `changeJudiciaryForHearings` handler CROWN logic | NEEDED | Skip allocation for WC/unscheduled | +| 15 | Null-safety in `enrichCandidate()` | NEEDED | Bug fix | +| 16 | Dead code removal | CLEANUP | Harmless | +| 17 | Hearing slots query — new params | NEEDED | UI support | +| 18 | `searchHearingsWithAnyAllocationState` no startDate | QUESTIONABLE | May be unrelated | +| 19 | `HearingRepository` query rewrite | QUESTIONABLE | May be unrelated | +| 20 | Remove `.toUpperCase()` on references | QUESTIONABLE | Data quality change | +| 21 | `correctNonDefaultDaysWithoutCourtCentre` simplification | **BUG** | Potential NPE | +| 22 | Liquibase index removal | QUESTIONABLE | Tied to #19 | + +## Recommendations + +1. **Changes 18-20, 22** appear unrelated to CROWN court schedule integration. Consider whether they should be in this branch or handled separately. If they're fixing issues found during development, that's fine, but they should be tested independently. + +2. **Change 21 is a bug** — the null-safe helpers from hmiremoval should be preserved. The simplified version will NPE for unallocated CROWN hearings where courtRoomId is null. + +3. **Change 10-11** — verify that existing CROWN hearings in production have their `hasAssignedSession` field correctly populated during replay. If there are CROWN hearings that were allocated before this feature, they need Change 13 to set `hasAssignedSession = true` on replay. + +4. **Change 4** — consider whether silent fallback to "assigned" when the court scheduler is unreachable is the right behavior. A loud failure might be safer. diff --git a/docs/frameworkITupgrade.md b/docs/big-changes/frameworkITupgrade.md similarity index 100% rename from docs/frameworkITupgrade.md rename to docs/big-changes/frameworkITupgrade.md diff --git a/docs/development-guidelines/crown-court-schedule-ui-changes.md b/docs/development-guidelines/crown-court-schedule-ui-changes.md new file mode 100644 index 000000000..1bd712670 --- /dev/null +++ b/docs/development-guidelines/crown-court-schedule-ui-changes.md @@ -0,0 +1,226 @@ +# UI Changes Required for CROWN Court Schedule Enrichment + +This document describes the UI changes needed to enable court schedule enrichment for CROWN fixed-date hearings. + +--- + +## Overview + +CROWN court schedule enrichment requires **`courtScheduleId`** to be included in `nonDefaultDays` when calling the listing backend. The schema already supports this field — the UI just needs to start passing it. + +The `courtScheduleId` is the UUID of the court schedule session selected by the user in the scheduling calendar. It links the hearing day to a specific session in the listing-court-scheduler viewstore. + +--- + +## Endpoint 1: `update-hearing-for-listing` + +**Used when:** A listing officer allocates, re-allocates, or updates a CROWN hearing from the listing UI. + +**Endpoint:** `POST /hearings/{hearingId}` +**Content-Type:** `application/vnd.listing.command.update-hearing-for-listing+json` + +### What to change + +Add `courtScheduleId` (string, UUID format) to each `nonDefaultDays` entry. + +### Current payload (without courtScheduleId) + +```json +{ + "courtCentreId": "89592405-c29b-3706-b1d3-b1dd3a08b227", + "courtRoomId": "d0624ee3-9198-3c8b-94d6-42fb197ebe5e", + "type": { + "id": "06b0c2bf-3f98-46ed-ab7e-56efaf9ecced", + "description": "Plea and Trial Preparation" + }, + "startDate": "2026-03-16", + "endDate": "2026-03-16", + "nonSittingDays": [], + "nonDefaultDays": [ + { + "startTime": "2026-03-16T10:00:00.000Z", + "courtCentreId": "89592405-c29b-3706-b1d3-b1dd3a08b227", + "roomId": "d0624ee3-9198-3c8b-94d6-42fb197ebe5e", + "duration": 180 + } + ], + "judiciary": [], + "jurisdictionType": "CROWN", + "hearingLanguage": "ENGLISH", + "sendNotificationToParties": false, + "hearingId": "16e9796e-8912-4081-a573-80f1d1f81c7e" +} +``` + +### Required payload (with courtScheduleId) + +```json +{ + "courtCentreId": "89592405-c29b-3706-b1d3-b1dd3a08b227", + "courtRoomId": "d0624ee3-9198-3c8b-94d6-42fb197ebe5e", + "type": { + "id": "06b0c2bf-3f98-46ed-ab7e-56efaf9ecced", + "description": "Plea and Trial Preparation" + }, + "startDate": "2026-03-16", + "endDate": "2026-03-16", + "nonSittingDays": [], + "nonDefaultDays": [ + { + "startTime": "2026-03-16T10:00:00.000Z", + "courtCentreId": "89592405-c29b-3706-b1d3-b1dd3a08b227", + "roomId": "d0624ee3-9198-3c8b-94d6-42fb197ebe5e", + "duration": 180, + "courtScheduleId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + } + ], + "judiciary": [], + "jurisdictionType": "CROWN", + "hearingLanguage": "ENGLISH", + "sendNotificationToParties": false, + "hearingId": "16e9796e-8912-4081-a573-80f1d1f81c7e" +} +``` + +### Multi-day example + +For a multi-day CROWN hearing (e.g. 3-day trial, duration 1080 mins), only one `nonDefaultDay` entry is needed with the **first day's** `courtScheduleId`. The backend will use the `multidaysearchandbook` endpoint to find and book consecutive sessions automatically. + +```json +{ + "courtCentreId": "1e6f8561-2dff-3161-b7b3-6dcd679e7c65", + "courtRoomId": "a0f7a73e-e99d-3e93-b0f1-f86fcfdab315", + "startDate": "2026-03-16", + "endDate": "2026-03-18", + "nonSittingDays": [], + "nonDefaultDays": [ + { + "startTime": "2026-03-16T10:00:00.000Z", + "courtCentreId": "1e6f8561-2dff-3161-b7b3-6dcd679e7c65", + "roomId": "a0f7a73e-e99d-3e93-b0f1-f86fcfdab315", + "duration": 1080, + "courtScheduleId": "f1e2d3c4-b5a6-7890-1234-567890abcdef" + } + ], + "judiciary": [], + "jurisdictionType": "CROWN", + "hearingLanguage": "ENGLISH", + "sendNotificationToParties": false, + "hearingId": "2cdfa174-a970-401e-80d1-a72fbfae4c35" +} +``` + +--- + +## Endpoint 2: `list-court-hearing` + +**Used when:** Progression context sends a hearing to listing (adhoc hearing creation, adjournment path C). + +**Endpoint:** `POST /hearings` +**Content-Type:** `application/vnd.listing.command.list-court-hearing+json` + +### What to change + +The `nonDefaultDays` array inside each hearing object should include `courtScheduleId`. This field is set by progression when it has court schedule information (e.g. from the scheduling calendar). + +### Current payload (without courtScheduleId) + +```json +{ + "hearings": [ + { + "courtCentre": { + "id": "89592405-c29b-3706-b1d3-b1dd3a08b227", + "name": "Blackfriars Crown Court", + "roomId": "d0624ee3-9198-3c8b-94d6-42fb197ebe5e", + "roomName": "Courtroom 01" + }, + "estimatedMinutes": 180, + "id": "16e9796e-8912-4081-a573-80f1d1f81c7e", + "jurisdictionType": "CROWN", + "nonDefaultDays": [ + { + "startTime": "2026-03-16T10:00:00.000Z", + "courtCentreId": "89592405-c29b-3706-b1d3-b1dd3a08b227", + "roomId": "d0624ee3-9198-3c8b-94d6-42fb197ebe5e", + "duration": 180 + } + ], + "prosecutionCases": [ ... ] + } + ] +} +``` + +### Required payload (with courtScheduleId) + +```json +{ + "hearings": [ + { + "courtCentre": { + "id": "89592405-c29b-3706-b1d3-b1dd3a08b227", + "name": "Blackfriars Crown Court", + "roomId": "d0624ee3-9198-3c8b-94d6-42fb197ebe5e", + "roomName": "Courtroom 01" + }, + "estimatedMinutes": 180, + "id": "16e9796e-8912-4081-a573-80f1d1f81c7e", + "jurisdictionType": "CROWN", + "nonDefaultDays": [ + { + "startTime": "2026-03-16T10:00:00.000Z", + "courtCentreId": "89592405-c29b-3706-b1d3-b1dd3a08b227", + "roomId": "d0624ee3-9198-3c8b-94d6-42fb197ebe5e", + "duration": 180, + "courtScheduleId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + } + ], + "prosecutionCases": [ ... ] + } + ] +} +``` + +--- + +## Where to get the `courtScheduleId` + +The `courtScheduleId` is obtained from the court scheduler when the user selects a session in the scheduling calendar. The existing `courtscheduler.get.court_schedule` endpoint returns sessions that include their `courtScheduleId` (the `id` field in the court_schedule table). + +When the user picks a session from the calendar: +1. The session's `id` (UUID) becomes the `courtScheduleId` +2. Pass it in the `nonDefaultDays` entry for that hearing day + +--- + +## Scope — What does NOT need courtScheduleId + +| Scenario | courtScheduleId needed? | Reason | +|---|---|---| +| CROWN fixed-date single-day | Yes | Enrichment resolves session, checks isDraft, books slot | +| CROWN fixed-date multi-day | Yes (first day only) | Backend expands to consecutive sessions via `multidaysearchandbook` | +| CROWN week-commencing | No | Out of scope — `weekCommencingDate != null` skips enrichment | +| CROWN unscheduled | No | Out of scope — no dates, "date and time to be fixed" | +| MAGISTRATES (all types) | No change | Existing MAGS flow already handles courtScheduleId | + +--- + +## Backend behaviour when courtScheduleId is missing + +If `courtScheduleId` is not included in `nonDefaultDays` for a CROWN fixed-date hearing: +- The hearing will still be processed normally (hearing days, duration enrichment) +- Court schedule enrichment will be skipped (no isDraft check, no slot booking) +- The hearing will remain **unallocated** (no courtScheduleIds on hearingDays → `canAllocateForCrown` returns false) + +This is backward-compatible — existing payloads without `courtScheduleId` will continue to work as before. + +--- + +## Journey reference + +See `docs/development-guidelines/journeys.md` for the full end-to-end flows: +- **Journey 1** (Adhoc Hearing) → Step 4: `list-court-hearing` enrichment +- **Journey 2** (Unallocated → Allocated) → Step 1: `update-hearing-for-listing` enrichment +- **Journey 3** (Update Allocated) → Step 1: same endpoint as Journey 2 +- **Journey 4** (Adjournment Path C) → Step C2: same as Journey 1 Step 4 diff --git a/docs/development-guidelines/crown-courtschedule-design-decisions.md b/docs/development-guidelines/crown-courtschedule-design-decisions.md new file mode 100644 index 000000000..a4486266d --- /dev/null +++ b/docs/development-guidelines/crown-courtschedule-design-decisions.md @@ -0,0 +1,177 @@ +# CROWN Court Schedule Enrichment — Design Decisions + +Date: 2026-03-02 +Status: Draft — Pending implementation + +--- + +## 1. Why extend CourtScheduleEnrichmentService vs creating a new service? + +**Decision:** Extend the existing `CourtScheduleEnrichmentService` with CROWN-specific methods. + +**Rationale:** +- CROWN single-day enrichment closely parallels MAGS "direct listing" (Case 1: all hearingDays have courtScheduleId) +- Reuses existing infrastructure: `listHearingSessionsAndExtractData()`, `combineSearchAndBookResponseAndListResponse()`, `populateJudiciaryInfoFromSlots()`, `SlotsToJsonStringConverter` +- Keeps the enrichment pipeline pattern consistent (MAGS: HearingDays → Duration → CourtSchedule; CROWN: CourtSchedule → HearingDays → Duration — see §5a) +- A separate service would duplicate HTTP client wiring, JSON parsing, and `EnrichmentResult` handling + +**Trade-off:** The class grows larger. Mitigated by clean method separation — CROWN methods are self-contained and don't share mutable state with MAGS methods. + +--- + +## 2. How is isDraft used for allocation without adding it to the aggregate domain? + +**Decision:** Use the existing indirect mechanism — when sessions are draft, skip `listHearingInCourtSessions`, so hearingDays lack courtScheduleIds → `canAllocateForCrown()` returns false. + +**Rationale:** +- `isDraft` is an attribute of the court schedule session, not the hearing domain +- Adding `isDraft` to `HearingDay` in the aggregate would require event schema changes, Liquibase migrations, and would couple listing's domain to court-scheduler internals +- The MAGS pattern already works this way: if `searchAndBook` fails, hearingDays have no courtScheduleId → hearing stays unallocated +- For CROWN, the same pattern: if any session isDraft=true, we don't call `listHearingInCourtSessions` → no confirmed courtScheduleIds → `hasCourtScheduleIds` returns false → unallocated + +**Exception case:** All sessions under one hearing MUST have the same isDraft value. If mixed (some draft, some not), the hearing is treated as unallocated. This is enforced in the enrichment service, not the aggregate. + +--- + +## 3. Why a new `multidaysearchandbook` endpoint vs extending existing endpoints? + +**Decision:** Create a new dedicated endpoint rather than extending `search.book.hearing.slots` or `get.hearing.slots`. + +**Rationale:** +- `search.book.hearing.slots` handles single-session search-and-book with police/non-police business type matching. Multi-day CROWN requires finding consecutive sessions across multiple dates — fundamentally different logic +- `get.hearing.slots` is read-only (no booking). Multi-day CROWN needs atomic search + book +- A dedicated endpoint keeps the responsibility clear: "given a starting courtScheduleId and total duration, find and book N consecutive weekday sessions" +- The consecutive-weekday validation and multi-session atomic booking are complex enough to warrant isolation + +**Alternative considered:** Calling `get.hearing.slots` N times with `consecutiveDays` filter. Rejected because: (1) it doesn't atomically book, (2) race conditions between search and book, (3) the caller would need to orchestrate N individual bookings. + +--- + +## 4. How does courtScheduleId arrive in CROWN payloads? + +**Decision:** Via `nonDefaultDays` with `courtScheduleId` field, using the existing payload structure. + +**Rationale:** +- The `HearingListingNeeds` (list-court-hearing) already supports `nonDefaultDays` and the enrichment code (`enrichHearingDaysForCrown`) already calls `enrichByNonDefaultDaysIfPresent()` → `convertCoreNonDefaultDaysToHearingDays()` which preserves courtScheduleId +- The `UpdateHearingForListing` already has `nonDefaultDays` in CROWN payloads and `convertNonDefaultDaysToHearingDays()` already preserves courtScheduleId +- No schema changes needed — `courtScheduleId` is already an optional field on `nonDefaultDay` in the JSON schema +- For multi-day: a single nonDefaultDay with courtScheduleId + total duration (e.g. 1080 for 3 days). The `multidaysearchandbook` endpoint expands this into N sessions + +**Dependency:** The calling context (progression) and UI must start including courtScheduleId on nonDefaultDays. This is an external dependency — no listing-side schema changes required. + +--- + +## 5. Single-day vs multi-day determination + +**Decision:** Use aggregated duration > 360 as the threshold (360 = `HearingDurationEnrichmentService.MINUTES_IN_DAY`). + +**Duration aggregation priority** (implemented in `CourtScheduleEnrichmentService.calculateAggregatedDuration`): +1. If `hearingDays` exist → sum all `durationMinutes` +2. Else if `nonDefaultDays` exist → sum all `duration` +3. Else → fall back to `estimatedMinutes` (for `HearingListingNeeds`) or 0 (for `UpdateHearingForListing`) + +**Rationale:** +- Aligns with the existing constant already used for MAGS duration calculations +- For list-court-hearing: the payload carries hearing days or a single nonDefaultDay. If the aggregated duration > 360, it's multi-day +- For update-hearing-for-listing: the enriched hearing has hearing days with durations. If the total > 360, it's multi-day +- `estimatedMinutes` is a fallback when no structured day data is available + +--- + +## 5a. CROWN enrichment pipeline order + +**Decision:** For CROWN, run CourtSchedule enrichment FIRST, then HearingDays, then Duration. MAGS retains the original order (HearingDays → Duration → CourtSchedule). + +**Pipeline order:** +``` +MAGS: HearingDays → Duration → CourtSchedule +CROWN: CourtSchedule → HearingDays → Duration +``` + +**Entry point:** `CourtScheduleEnrichmentService.enrichCrownCourtScheduleFirst()` (overloaded for `HearingListingNeeds` and `UpdateHearingForListing`). + +**Three cases inside `enrichCrownCourtScheduleFirst`:** + +| Case | Condition | Action | +|------|-----------|--------| +| 1 | No courtScheduleId in hearingDays/bookedSlots | Return unchanged — no court schedule enrichment | +| 2 | Has courtScheduleId + aggregated duration > 360 | Multi-day: `multiDaySearchAndBook` → get all sessions | +| 3 | Has courtScheduleId + aggregated duration ≤ 360 | Single-day: `listHearingInCourtSessions` → get session details | + +**Rationale:** +- The court scheduler is the source of truth for CROWN session dates, times, rooms, and judiciary +- Running CourtSchedule first means HearingDays and Duration enrichment derive truth from the scheduler response rather than building hearing days speculatively and then correcting +- For MAGS, the UI provides most hearing day details (slots, times, rooms) — the court scheduler just confirms and books, so the original order is correct + +**Affected entry points (all in `HearingEnrichmentOrchestrator`):** +- `enrichListCourtHearing()` — used by `listing.command.list-court-hearing` and `listing.list-next-hearings-v2` +- `enrichUpdateHearingForListing(hearing, envelope)` — used by `listing.command.update-related-hearing` +- `enrichUpdateHearingForListing(hearing, envelope, courtCentreDetails)` — used by `listing.command.update-hearing-for-listing` and `listing.command.update-hearings-for-listing` + +--- + +## 6. Consecutive weekday enforcement in multidaysearchandbook + +**Decision:** Strictly consecutive weekdays (Mon-Fri), no gaps. If any day cannot be found, return empty array. + +**Rationale:** +- Crown Court trials span consecutive working days — a 3-day trial starting Monday must be Mon/Tue/Wed +- Weekends (Sat/Sun) are excluded from "consecutive" — a Friday trial continues on Monday +- If any weekday between the first and last date has no available session (or the session doesn't match criteria), the entire allocation fails +- An empty response tells listing "this hearing cannot be allocated as multi-day at this time" → hearing stays unallocated +- This prevents partial allocations where day 1 and day 3 are booked but day 2 is missing + +**Search criteria for consecutive sessions:** +- Same `courtRoomId` as anchor session +- Same `rota_business_type` as anchor session +- `court_session = 'AD'` (all-day, as specified) +- `active = true` +- Session dates are consecutive weekdays starting from anchor's `session_start` + +--- + +## 7. Availability check in multidaysearchandbook + +**Decision:** Reuse the same dynamic calculation logic from `courtscheduler.get.hearing.slots`. + +**Rationale:** +- The `get.hearing.slots` endpoint already computes availability dynamically via SQL: `max_duration_mins - SUM(al.duration)` with AD-split support +- The `overbookingFilter` in `SlotsSearchService` provides the Java-side validation +- Reusing this logic ensures consistency — a session that shows as "available" in the search UI will also be bookable via multidaysearchandbook +- The duration per day for availability check = `totalDuration / daysNeeded` (integer division) + +--- + +## 8. Slot payback — extending both vacate-trial paths + +**Decision:** Both vacate-trial paths extended for CROWN. + +- **Path A (ListingCommandApi.handleVacateTrial):** Already calls `hearingSlotsService.delete(hearingId)` unconditionally — no change needed +- **Path B (Hearing.hearingVacateTrial):** Extended to emit `availableSlotsForHearingFreed` for CROWN when `hasCourtScheduleIds` is true + +**Rationale:** Both paths exist and are used. Path A handles the synchronous API-level vacate, Path B handles the aggregate-level vacate via event. CROWN hearings may use either path depending on the calling context. The `hasCourtScheduleIds` guard ensures we only attempt to free slots when the hearing actually has booked sessions. + +--- + +## 9. Hearing move detection + +**Decision:** No explicit change detection needed in listing. The existing `listHearingInCourtSessions` mechanism handles it. + +**Rationale:** +- When `listHearingInCourtSessions` is called with new courtScheduleIds, the court-scheduler's `updateListHearingSlots()` already calls `releaseOldListingsFromAllocatedListings(hearingId)` before creating new bookings +- For multi-day CROWN: first call `multidaysearchandbook` to get new courtScheduleIds, then `listHearingInCourtSessions` with those IDs (releases old sessions automatically) +- For single-day CROWN: `listHearingInCourtSessions` directly (releases old sessions) +- This mirrors the MAGS pattern — listing doesn't detect changes, it always re-books, and the court-scheduler handles the release + +--- + +## 10. Sanity checks — logging vs failing + +**Decision:** Use court-scheduler values when conflicts exist, log as errors. + +**Rationale:** +- The court-scheduler is the source of truth for session dates, times, and courtroom assignments +- If the calling context sends startDate=2026-03-02 but the court scheduler says the session is on 2026-03-03, we use 2026-03-03 +- Descriptive error logs help debugging without failing the entire request +- This is a defensive approach — the calling context may have stale data, but the hearing can still be processed +- Log format: `"CROWN sanity: received startDate {} but sessionDate {}. Using court scheduler value."` diff --git a/docs/development-guidelines/integration-map.mmd b/docs/development-guidelines/integration-map.mmd new file mode 100644 index 000000000..cdc068924 --- /dev/null +++ b/docs/development-guidelines/integration-map.mmd @@ -0,0 +1,156 @@ +%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2d3748', 'primaryTextColor': '#fff', 'primaryBorderColor': '#4a5568', 'lineColor': '#718096', 'secondaryColor': '#edf2f7', 'tertiaryColor': '#e2e8f0', 'fontSize': '14px'}}}%% + +graph TB + subgraph LISTING["cpp-context-listing"] + direction TB + + subgraph CMD_API["Command API (28 endpoints)"] + CMD_CASES["POST /cases
list-court-hearing
list-unscheduled-court-hearing"] + CMD_HEARING["POST /hearings/{hearingId}
update-hearing-for-listing
update-defendants-for-hearing
delete-offences-for-hearing
update-offences-for-hearing
add-offences-for-hearing
extend-hearing-for-hearing
vacate-trial
mark-unallocated-hearing-as-duplicate
list-next-hearings-v2
list-unscheduled-next-hearings
update-related-hearing
delete-next-hearings"] + CMD_HEARINGS["POST /hearings
change-judiciary-for-hearings
restrict-court-list
delete-prev-hearings-and-create-next"] + CMD_HEARINGS_OTHER["POST /hearings/sequence
POST /hearings/update-hearings"] + CMD_NOTE["POST /listing-note
POST /listing-notes/{noteId}"] + CMD_COURT_LIST["POST /publishCourtList/{courtCentreId}
POST /publishCourtListsForCrownCourts
POST /courtlist/{courtCentreId}/{type}/{startDate}"] + CMD_SCHEDULE["POST /correct-hearing-days-without-court-centre
POST /update-hearing-day-court-schedule
POST /delete-hearing/{hearingId}"] + end + + subgraph QRY_API["Query API (19 endpoints)"] + QRY_SEARCH["GET /hearings — search
GET /hearings/{id}
GET /hearings/range-search
GET /hearings/available-search
GET /hearings/allocated-and-unallocated
GET /hearings/any-allocation
GET /hearings/cotr-search
GET /hearings/unscheduled"] + QRY_COURTLIST["GET /courtlist
GET /courtlistpayload
GET /courtListPublishStatus/{id}"] + QRY_CASES["GET /cases/by-person-defendant-and-hearingDate
GET /cases/by-organisation-defendant-and-hearingDate"] + QRY_SLOTS["GET /hearingSlots
POST /sessionAvailabilityValidation"] + QRY_CACHE["GET /cache-refdata-courtrooms/refresh
POST /cache-refdata-courtrooms/add
POST /cache-refdata-courtrooms/close"] + QRY_CSV["GET /hearings/download-hearing-csv-report"] + end + + AGGREGATE["Hearing Aggregate
(Domain)"] + VIEWSTORE[("PostgreSQL
Viewstore")] + EVENT_PROC["Event Processors"] + EVENT_LIST["Event Listeners
(Viewstore Updates)"] + end + + %% ============================================= + %% UPSTREAM CONTEXTS (events INTO listing) + %% ============================================= + subgraph HEARING_CTX["cpp-context-hearing"] + H_PUB["Public Events Published"] + end + + subgraph PROGRESSION_CTX["cpp-context-progression"] + P_PUB["Public Events Published"] + P_CMD["Cross-Context Commands"] + end + + subgraph REFDATA_CTX["cpp-context-reference-data"] + R_PUB["Public Events Published"] + end + + subgraph SJP_CTX["cpp-context-sjp"] + SJP_PUB["Public Events Published"] + end + + %% ============================================= + %% DOWNSTREAM CONTEXTS (events FROM listing) + %% ============================================= + subgraph NOTIFICATION_CTX["cpp-context-notification"] + N_SUB["Subscribes to Listing Events"] + end + + subgraph BUSINESSPROC_CTX["cpp-context-businessprocesses"] + BP_SUB["Subscribes to Listing Events"] + end + + subgraph MI_CTX["cpp-context-mi-reportdata"] + MI_SUB["Event Listeners"] + end + + %% ============================================= + %% OUTBOUND REST SERVICES (listing calls these) + %% ============================================= + subgraph COURTSCHEDULER["cpp-context-listing-courtscheduler"] + CS_API["REST API
/hearingslots
/courtschedule/search.court-schedules-by-id
/validate-session-availability
/list/hearingslots
/searchlist/hearingslots
/multidaysearchandbook/hearingslots"] + end + + subgraph DOC_GEN["Document Generator Service"] + DG_API["PDF Generation API"] + end + + subgraph XHIBIT["Xhibit Gateway"] + XH_API["WebDAV Endpoint
/xhibit-gateway/send-to-xhibit/"] + end + + %% ============================================= + %% INBOUND PUBLIC EVENTS (other contexts → listing) + %% ============================================= + H_PUB -->|"public.events.hearing.hearing-resulted
public.events.hearing.marked-as-duplicate
public.hearing.trial-vacated
public.hearing.hearing-days-cancelled
public.hearing.defence-counsel-added/updated/removed
public.hearing.prosecution-counsel-added/updated/removed
public.hearing.selected-offences-removed-from-existing-hearing"| EVENT_PROC + + P_PUB -->|"public.progression.defendant-offences-changed
public.progression.case-defendant-changed
public.progression.court-application-changed
public.progression.defendants-added-to-court-proceedings
public.progression.case-markers-updated
public.progression.hearing-resulted-case-updated
public.progression.defendant-legalaid-status-updated
public.progression.case-linked
public.progression.case-removed-from-group-cases
public.progression.application-offences-updated
public.progression.application-laa-reference-updated-for-application
public.progression.events.case-or-application-ejected
public.progression.events.cps-prosecutor-updated
public.progression.events.hearing-extended
public.progression.events.court-application-deleted
public.progression.related-hearing-updated-for-adhoc-hearing"| EVENT_PROC + + R_PUB -->|"public.referencedata.event.courtroom-added
public.referencedata.event.courtroom-closed"| EVENT_PROC + + SJP_PUB -->|"public.events.hearing.hearing-resulted
(co-publishes with hearing)"| EVENT_PROC + + P_CMD -->|"listing.command.list-court-hearing
(cross-context command)"| CMD_API + + %% ============================================= + %% OUTBOUND PUBLIC EVENTS (listing → other contexts) + %% ============================================= + EVENT_PROC -->|"public.listing.hearing-listed
public.listing.hearing-confirmed"| BP_SUB + EVENT_PROC -->|"public.listing.hearing-listed
public.listing.court-application-added-for-hearing
public.listing.hearing-changes-saved
public.listing.hearing-confirmed
public.listing.hearing-updated
public.listing.hearing-partially-updated
public.listing.hearings-update-completed
public.listing.hearing-added-to-case
public.listing.hearing-days-changed-for-hearing
public.listing.hearing-days-sequenced
public.listing.court-list-restricted
public.listing.created-listing-note
public.listing.deleted-listing-note
public.listing.note-edited
public.listing.judiciary-changed-for-hearings-status"| N_SUB + + EVENT_PROC -->|"public.listing.hearing-confirmed
public.listing.hearing-updated
public.listing.hearing-partially-updated
public.listing.hearing-requested-for-listing
public.listing.create-next-hearing-requested
public.listing.cases-added-to-hearing
public.listing.new-defendant-added-for-court-proceedings
public.listing.offences-moved-to-next-hearing
public.listing.vacated-trial-updated
public.listing.hearing-unallocated-courtroom-removed
public.listing.court-list-published
public.events.listing.allocated-hearing-deleted
public.events.listing.unallocated-hearing-deleted
public.events.listing.hearing-deleted
public.events.listing.hearing-days-without-court-centre-corrected
public.events.listing.cases-added-for-updated-related-hearing
public.events.listing.offences-removed-from-allocated-hearing
public.events.listing.offences-removed-from-existing-allocated-hearing
public.events.listing.offences-removed-from-existing-unallocated-hearing
public.events.listing.offences-removed-from-unallocated-hearing
public.events.listing.next-hearing-day-changed"| P_PUB + + EVENT_PROC -->|"public.events.listing.allocated-hearing-deleted
public.events.listing.offences-removed-from-existing-allocated-hearing
public.events.listing.next-hearing-day-changed
public.listing.court-list-restricted
public.listing.hearing-unallocated-courtroom-removed
public.listing.vacated-trial-updated
public.events.listing.hearing-unallocated"| H_PUB + + EVENT_LIST -->|"hearing-listed
hearing-allocated
cases-added
(viewstore updates)"| VIEWSTORE + + EVENT_PROC -->|"(External Listeners)"| MI_SUB + + %% ============================================= + %% OUTBOUND REST CALLS (listing → external services) + %% ============================================= + CMD_API -->|"GET /courtschedule/search.court-schedules-by-id
GET /hearingslots
GET /searchlist/hearingslots
GET /multidaysearchandbook/hearingslots
PUT /list/hearingslots"| CS_API + QRY_SLOTS -->|"GET /hearingslots
POST /validate-session-availability"| CS_API + + QRY_COURTLIST -->|"POST generateDocument()"| DG_API + CMD_COURT_LIST -->|"WebDAV PUT (court list XML)"| XH_API + + %% ============================================= + %% INTER-SERVICE QUERY CALLS (via Requester) + %% ============================================= + QRY_CASES -->|"defence.query.get-case-by-person-defendant
defence.query.get-case-by-organisation-defendant"| DEFENCE_CTX + QRY_SEARCH -->|"progression.query.prosecutioncase
progression.query.case-notes
progression.query.application-notes"| PROGRESSION_CTX + QRY_SEARCH -->|"referencedata.query.courtroom
referencedata.query.judiciaries
referencedata.query.hearing-types
referencedata.query.courtrooms"| REFDATA_CTX + QRY_SEARCH -->|"usersgroups.is-logged-in-user-has-permission-for-action"| UG_CTX + + subgraph DEFENCE_CTX["cpp-context-defence"] + DEF_QRY["Query API"] + end + + subgraph UG_CTX["cpp-context-users-groups"] + UG_QRY["Query API"] + end + + %% ============================================= + %% INTERNAL FLOW + %% ============================================= + CMD_API --> AGGREGATE + AGGREGATE --> EVENT_PROC + AGGREGATE --> EVENT_LIST + QRY_API --> VIEWSTORE + + %% ============================================= + %% STYLING + %% ============================================= + classDef listing fill:#2d3748,stroke:#4a5568,color:#fff + classDef upstream fill:#2b6cb0,stroke:#2c5282,color:#fff + classDef downstream fill:#276749,stroke:#22543d,color:#fff + classDef external fill:#9b2c2c,stroke:#742a2a,color:#fff + classDef query fill:#553c9a,stroke:#44337a,color:#fff + classDef internal fill:#4a5568,stroke:#2d3748,color:#fff + + class LISTING listing + class HEARING_CTX,PROGRESSION_CTX,REFDATA_CTX,SJP_CTX upstream + class NOTIFICATION_CTX,BUSINESSPROC_CTX,MI_CTX downstream + class COURTSCHEDULER,DOC_GEN,XHIBIT external + class DEFENCE_CTX,UG_CTX query diff --git a/docs/development-guidelines/integration-test-guide.md b/docs/development-guidelines/integration-test-guide.md index 3570944b1..fb1a095da 100644 --- a/docs/development-guidelines/integration-test-guide.md +++ b/docs/development-guidelines/integration-test-guide.md @@ -449,91 +449,9 @@ mvn verify -pl listing-integration-test -P listing-integration-test --- -## 13. Deterministic Time: `ItClock` and the Wall-Clock Ban - -The suite used to fail in a one-hour band at **00:00–01:00 BST**. British Summer Time is UTC+1, so in -that hour a "today" computed in `Europe/London` and a "today" computed in UTC land on **different calendar -days**. With many files independently calling `LocalDate.now()` / `ZonedDateTime.now()`, a test could build -a hearing date in one zone and assert against another — flaky only during that hour, and "green local / -red CI" because a UK laptop runs `Europe/London` while CI runs UTC. - -### 13.1 The rule: one anchor, one zone, derive everything - -All test time goes through **`ItClock`** (`listing-integration-test/.../it/util/ItClock.java`): - -| Instead of | Use | Returns | -|---|---|---| -| `LocalDate.now()` | `ItClock.today()` | `LocalDate` (anchored once per run, UTC) | -| `ZonedDateTime.now()`, `ZonedDateTime.now(ZoneOffset.UTC)` | `ItClock.nowUtc()` | `ZonedDateTime` (UTC) | -| `ZonedDateTime.now(ZoneId.of("Europe/London"))` | `ItClock.nowLondon()` | `ZonedDateTime` (London) | -| `LocalDateTime.now()` | `ItClock.nowLocalDateTime()` | `LocalDateTime` | -| `Instant.now()` (as a data date) | `ItClock.nowInstant()` | `Instant` | -| bespoke London→UTC conversion | `ItClock.utc(date, londonTime)` | UTC instant `String` | - -`today()` is captured **once per JVM** and is **UTC-internal**, so (a) a test can never straddle midnight -between building a date and asserting on it, and (b) a UK-laptop run and a UTC CI run agree on "today" -without needing a JVM timezone pin — `ItClock` *is* the canonical UTC source. Chained arithmetic is -unchanged: `ItClock.today().plusDays(7)`, `ItClock.nowUtc().truncatedTo(ChronoUnit.HOURS)`, etc. - -Elapsed-time measurement is **not** a date and stays as-is: `System.currentTimeMillis()` in `QueueUtil` -timing loops and `Instant.now()` inside duration/log-marker infra. - -### 13.2 The guard rail - -A `forbidden-apis` check (in the IT module's default `build` profile, so it runs on `mvn install`) fails -the build if any IT class calls a no-arg wall-clock `now()` or `new Date()` outside `ItClock`. Only the -no-arg overloads are banned — `ItClock` itself uses the `Clock`-argument overloads (`LocalDate.now(CLOCK)`), -so it is not flagged. The command-line `-P listing-integration-test` profile deactivates the `build` -profile, so the guard adds zero overhead to the IT run itself. - -### 13.3 Midnight simulation (no waiting for real midnight) - -`ItClock` reads an optional `-Dit.clock` anchor (forwarded into the forked IT JVM by the failsafe -`systemPropertyVariables`). `run-it-midnight.sh` at the repo root freezes the clock across the -00:00–01:00 BST band and weekend boundaries and runs the date-sensitive subset: - -```bash -./run-it-midnight.sh # green at every anchor == midnight-safe -``` - -### 13.4 Day-of-week safety: working-day anchors for multi-day spans - -`ItClock.today()` removes *time-of-day* non-determinism (the midnight band) but it is still the **live -calendar date**, so it can be any day of the week. **Courts do not sit at weekends.** A test that builds a -**split / multi-day / extend** hearing span with raw `plusDays(...)` and then asserts per-day listing events -(e.g. `verifyHearingRequestedForListingEvent`) silently straddles a Sat/Sun on some days of the week — the -weekend day emits no listing request, the awaited JMS message never arrives, and the test times out. This is -non-determinism by **day-of-week** rather than time-of-day, and it only surfaces on the days the run lands on. - -Anchor such spans on working days: - -| Use | Returns | -|---|---| -| `ItClock.nextWorkingDay()` | `today()`, or the next Mon-Fri if today is a weekend | -| `ItClock.nextWorkingDay(date)` | `date`, or the next Mon-Fri if `date` is a weekend | -| `ItClock.plusWorkingDays(date, n)` | `date` advanced by `n` working days, skipping weekends; result is always Mon-Fri | - -`plusWorkingDays` is identical to `plusDays` on a run with no weekend in range, so it does not perturb the -passing weekday case. Example (`HearingDaysIT.testHearingDaysWithCourtCentreForSplit`): - -```java -// before — span [start+1, start+3] hits a weekend whenever the suite runs Wed-Sat: -hearingData.getHearingStartDate().plusDays(1), hearingData.getHearingEndDate().plusDays(2) -// after — span endpoints are always working days: -final LocalDate splitStartDate = ItClock.plusWorkingDays(hearingData.getHearingStartDate(), 1); -final LocalDate splitEndDate = ItClock.plusWorkingDays(splitStartDate, 2); -``` - -Single-day-on-`today()` tests are weekend-safe and need no change; only multi-day spans that assert -sitting-day events do. - ---- - ## Summary Checklist When writing or reviewing integration tests, verify: -- [ ] No raw wall-clock reads (`LocalDate.now()`, `ZonedDateTime.now()`, `new Date()`, …) — use `ItClock` (§13; the `forbidden-apis` guard enforces this on `mvn install`) -- [ ] Multi-day / split / extend spans that assert sitting-day events use `ItClock.plusWorkingDays`/`nextWorkingDay`, never raw `plusDays` — so they never straddle a weekend (§13.4) - [ ] No usage of `javax.json.Json` — use `uk.gov.justice.services.messaging.JsonObjects` instead - [ ] No direct `RestPoller.poll()` calls — use `RestPollerHelper.pollWithDefaults()` or `pollWithDelayForJms()` diff --git a/docs/development-guidelines/journeys.md b/docs/development-guidelines/journeys.md new file mode 100644 index 000000000..18eb6fe15 --- /dev/null +++ b/docs/development-guidelines/journeys.md @@ -0,0 +1,457 @@ +# Journeys & Touch Points + +Reference document for end-to-end flows that touch the listing context. + +--- + +## 1. Adhoc Hearing + +Triggered when a user requests a new adhoc hearing from the prosecution casefile UI. + +**UI origin:** `cpp-ui-prosecution-casefile` → `progression.service.ts` (commandSync, expects `public.listing.hearing-confirmed`) + +### Flow + +#### Step 1 — Progression: Command API +- **Endpoint:** `progression.list-new-hearing` (POST `/defendant/{defendantId}/courtdocument/{materialId}`) +- **Handler:** `ListNewHearingApi.handle()` +- **Sends command:** `progression.command.list-new-hearing` + +#### Step 2 — Progression: Command Handler +- **Handler:** `ListNewHearingHandler.handle()` +- **Raises events:** + - `progression.event.list-hearing-requested` → continues to **Step 3** + - `progression.event.related-case-requested-for-adhoc-hearing` → see [Side Branch A](#side-branch-a--related-cases) + +#### Step 3 — Progression: Event Processor +- **Handler:** `ListHearingRequestedProcessor.handle()` +- **Sends command (cross-context):** `listing.command.list-court-hearing` → enters **listing context** + +#### Step 4 — Listing: Command API +- **Handler:** `ListingCommandApi.handleListCourtHearing()` +- **Sends command:** `listing.command.list-court-hearing-enriched` + +#### Step 5 — Listing: Command Handler +- **Handler:** `ListingCommandHandler.listCourtHearing()` +- **Raises events:** + - `listing.event.court-centre-details` (aggregate event, no downstream processor) + - `listing.events.hearing-listed` → continues to **Step 6** + +#### Step 6 — Listing: Event Processor + Listener (hearing-listed) + +**Event Processor** — `ListingEventProcessor.handleHearingListedMessage()`: +- Sends commands: + - `listing.command.add-hearing-to-case` → **Step 7** + - `listing.command.add-court-application-to-hearing` → **Step 8** +- Publishes public events: + - `public.listing.hearing-listed` → consumed by `cpp-context-businessprocesses` (`ListingHearingListedEventProcessor`) + - `public.listing.court-application-added-for-hearing` + +**Event Listener** — `HearingEventListener.hearingListed()`: +- Same commands as above (used for viewstore updates) + +**External Listener** — `cpp-context-mi-reportdata` (`HearingListedEventListener.handleHearingListed()`) + +#### Step 7 — Listing: Add Hearing to Case +- **Handler:** `ListingCommandHandler.addHearingToCase()` +- **Raises events:** + - `listing.events.defendants-to-be-updated` → **Step 7a** + - `listing.events.hearing-added-to-case` → **Step 7b** + +##### Step 7a — Update Defendants +- **Event Processor:** `ListingEventProcessor.handleDefendantsToBeUpdatedMessage()` +- **Sends command:** `listing.command.update-defendants-for-hearing` +- **Handler:** `ListingCommandHandler.updateDefendantsForHearing()` (terminal) + +##### Step 7b — Hearing Added to Case +- **Event Processor:** `ListingEventProcessor.handleHearingAddedToCase()` +- **Publishes:** `public.listing.hearing-added-to-case` + +#### Step 8 — Listing: Add Court Application to Hearing +- **Handler:** `ListingCommandHandler.addCourtApplicationToHearing()` +- **Raises event:** `listing.event.court-application-added-to-hearing` (terminal) + +### Side Branch A — Related Cases + +Internal to progression. Handles related cases that need to be listed alongside the adhoc hearing. + +1. `progression.event.related-case-requested-for-adhoc-hearing` + → `RelatedHearingEventProcessor.processRelatedCaseRequestedForAdhocHearing()` + → sends `progression.command.request-related-hearing-for-adhoc-hearing` +2. `ListNewHearingHandler.handleRequestRelatedHearingForAdhocHearing()` + → raises `progression.event.related-hearing-requested-for-adhoc-hearing` +3. `RelatedHearingEventProcessor.processRelatedHearingRequestedForAdhocHearing()` + → sends `progression.command.update-related-hearing-for-adhoc-hearing` +4. `RelatedHearingCommandHandler.handleUpdateRelatedHearingCommandForAdhocHearing()` (terminal) + +### Side Branch B — Court Document + +Also triggered by `ListNewHearingHandler.handle()` in progression (not part of the listing flow). + +1. Sends `progression.command.add-court-document` + → `AddCourtDocumentHandler.handle()` raises multiple document-related events + → publishes `public.progression.event.court-document-shared`, `public.progression.events.court-document-updated` + +### Summary of Public Events Emitted by Listing + +| Event | Consumed By | +|---|---| +| `public.listing.hearing-listed` | `cpp-context-businessprocesses`, `cpp-context-notification` | +| `public.listing.hearing-added-to-case` | `cpp-context-notification` | +| `public.listing.court-application-added-for-hearing` | `cpp-context-notification` | + +--- + +## 2. Unallocated to Allocated Hearing + +Triggered when a listing officer allocates an unallocated hearing to a courtroom/date. Uses the same endpoint as [Update Allocated Hearing](#3-update-allocated-hearing). The aggregate determines which events to raise based on the hearing's current state. + +### Flow + +#### Step 1 — Listing: Command API +- **Endpoint:** `listing.command.update-hearing-for-listing` (POST `/hearings/{hearingId}`) +- **Handler:** `ListingCommandApi.handleUpdateHearingForListing()` +- **Sends command:** `listing.command.update-hearing-for-listing-enriched` + +#### Step 2 — Listing: Command Handler (Hearing aggregate) +- **Handler:** `ListingCommandHandler.updateHearingForListing()` +- **Raises aggregate events (conditional based on state):** + - `listing.events.hearing-listed` → **Step 3** (the hearing is now allocated/listed) + - `listing.event.cases-added-to-hearing` → **Step 4** + - `listing.event.added-cases-for-hearing` → listener: `ExtendHearingForHearingListener.hearingAddedCasesForHearing()` (viewstore) + - `listing.event.hearing-updated-to-case` (aggregate on Case, no downstream processor) + - `listing.event.court-application-added-to-hearing` (aggregate-only) + - `listing.event.master-case-updated-for-group` (aggregate-only) + - `listing.event.defendants-to-be-updated-later` (aggregate-only) + - `listing.event.case-resulted-defendant-proceedings-concluded` (aggregate-only) + - `listing.event.sequences-reset-on-hearing-days` (aggregate-only) + +#### Step 3 — Listing: Event Processor + Listener (hearing-listed) + +Same as [Adhoc Hearing Step 6](#step-6--listing-event-processor--listener-hearing-listed): + +**Event Processor** — `ListingEventProcessor.handleHearingListedMessage()`: +- Sends commands: + - `listing.command.add-hearing-to-case` → see [Adhoc Hearing Step 7](#step-7--listing-add-hearing-to-case) + - `listing.command.add-court-application-to-hearing` → see [Adhoc Hearing Step 8](#step-8--listing-add-court-application-to-hearing) +- Publishes public events: + - `public.listing.hearing-listed` → consumed by `cpp-context-businessprocesses` + - `public.listing.court-application-added-for-hearing` + +**Event Listener** — `HearingEventListener.hearingListed()` (viewstore) + +**External Listener** — `cpp-context-mi-reportdata` + +#### Step 4 — Listing: Event Processor (cases-added-to-hearing) +- **Handler:** `UpdateExistingHearingEventProcessor.handleCasesAddedToHearingEvent()` +- **Sends command:** `listing.command.add-hearing-to-case` → see [Adhoc Hearing Step 7](#step-7--listing-add-hearing-to-case) +- **Publishes public events:** + - `public.listing.cases-added-to-hearing` → consumed by `cpp-context-progression` (`ExtendedHearingProcessor.addCasesToUnAllocatedHearing()`) + - `public.events.listing.cases-added-for-updated-related-hearing` → consumed by `cpp-context-progression` (`RelatedHearingEventProcessor`) + +**Event Listener** — `ExtendHearingForHearingListener.handleCasesAddedToHearingEvent()` (viewstore) + +**External Listener** — `cpp-context-mi-reportdata` (`CasesAddedToHearingEventListener`) + +### Summary of Public Events Emitted by Listing + +| Event | Consumed By | +|---|---| +| `public.listing.hearing-listed` | `cpp-context-businessprocesses`, `cpp-context-notification` | +| `public.listing.court-application-added-for-hearing` | `cpp-context-notification` | +| `public.listing.hearing-added-to-case` | `cpp-context-notification` | +| `public.listing.cases-added-to-hearing` | `cpp-context-notification`, `cpp-context-progression` | +| `public.events.listing.cases-added-for-updated-related-hearing` | `cpp-context-progression` | + +--- + +## 3. Update Allocated Hearing + +Triggered when a listing officer updates an already-allocated hearing (e.g. changing courtroom, date, judiciary, or hearing details). Uses the same endpoint as [Unallocated to Allocated](#2-unallocated-to-allocated-hearing). + +### Flow + +#### Step 1 — Listing: Command API +- **Endpoint:** `listing.command.update-hearing-for-listing` (POST `/hearings/{hearingId}`) +- **Handler:** `ListingCommandApi.handleUpdateHearingForListing()` +- **Sends command:** `listing.command.update-hearing-for-listing-enriched` + +#### Step 2 — Listing: Command Handler (Hearing aggregate) +- **Handler:** `ListingCommandHandler.updateHearingForListing()` +- **Raises aggregate events (conditional based on state):** + - `listing.events.allocated-hearing-deleted` → **Step 3** (the old allocated hearing is replaced) + - `listing.events.hearing-listed` → **Step 4** (the updated hearing is re-listed) + - `listing.event.cases-added-to-hearing` → **Step 5** + - `listing.event.added-cases-for-hearing` → listener: `ExtendHearingForHearingListener` (viewstore) + - `listing.event.hearing-updated-to-case` (aggregate on Case, no downstream processor) + - `listing.event.hearing-listed-case-updated` (aggregate-only) + - `listing.event.hearing-marked-as-duplicate-for-case` (aggregate-only) + - `listing.event.court-application-added-to-hearing` (aggregate-only) + - `listing.event.master-case-updated-for-group` (aggregate-only) + - `listing.event.defendants-to-be-updated-later` (aggregate-only) + - `listing.event.case-resulted-defendant-proceedings-concluded` (aggregate-only) + - `listing.event.sequences-reset-on-hearing-days` (aggregate-only) + - `listing.event.court-list-export-requested` → processor: `CourtListEventProcessor` (Xhibit court list side effect) + +#### Step 3 — Listing: Event Processor (allocated-hearing-deleted) +- **Handler:** `NextHearingProcessor.handleAllocatedHearingDeleted()` +- **Sends command:** `listing.command.mark-hearing-as-duplicate-for-case` +- **Publishes:** `public.events.listing.allocated-hearing-deleted` → consumed by `cpp-context-hearing` (`HearingDeletedEventProcessor.handleHearingDeletedPublicEvent()` → `hearing.command.delete-hearing`) + +**Event Listener** — `HearingMarkedAsDuplicateEventListener.handleAllocatedHearingDeleted()` (viewstore) + +**External Listener** — `cpp-context-mi-reportdata` (`HearingDeletedEventListener`) + +#### Step 4 — Listing: Event Processor + Listener (hearing-listed) + +Same as [Adhoc Hearing Step 6](#step-6--listing-event-processor--listener-hearing-listed): + +**Event Processor** — `ListingEventProcessor.handleHearingListedMessage()`: +- Sends commands: + - `listing.command.add-hearing-to-case` → see [Adhoc Hearing Step 7](#step-7--listing-add-hearing-to-case) + - `listing.command.add-court-application-to-hearing` → see [Adhoc Hearing Step 8](#step-8--listing-add-court-application-to-hearing) +- Publishes public events: + - `public.listing.hearing-listed` → consumed by `cpp-context-businessprocesses` + - `public.listing.court-application-added-for-hearing` + +**Event Listener** — `HearingEventListener.hearingListed()` (viewstore) + +#### Step 5 — Listing: Event Processor (cases-added-to-hearing) + +Same as [Unallocated to Allocated Step 4](#step-4--listing-event-processor-cases-added-to-hearing): + +- **Handler:** `UpdateExistingHearingEventProcessor.handleCasesAddedToHearingEvent()` +- **Sends command:** `listing.command.add-hearing-to-case` +- **Publishes:** + - `public.listing.cases-added-to-hearing` → consumed by `cpp-context-progression` + - `public.events.listing.cases-added-for-updated-related-hearing` → consumed by `cpp-context-progression` + +### Summary of Public Events Emitted by Listing + +| Event | Consumed By | +|---|---| +| `public.events.listing.allocated-hearing-deleted` | `cpp-context-hearing`, `cpp-context-progression` | +| `public.listing.hearing-listed` | `cpp-context-businessprocesses`, `cpp-context-notification` | +| `public.listing.court-application-added-for-hearing` | `cpp-context-notification` | +| `public.listing.hearing-added-to-case` | `cpp-context-notification` | +| `public.listing.cases-added-to-hearing` | `cpp-context-notification`, `cpp-context-progression` | +| `public.events.listing.cases-added-for-updated-related-hearing` | `cpp-context-progression` | + +--- + +## 4. Adjournment + +Triggered when a judge adjourns a hearing and the results are shared. This is a complex cross-context flow; only the listing integration points are documented here. + +**UI origin:** `cpp-ui-hearing` → `results.service.ts` (commandSync, expects `public.events.hearing.hearing-resulted-success`) + +### Overview + +The flow starts in hearing context with `hearing.share-results-v2`. Hearing processes results and publishes public events. These reach listing through **three separate paths**: + +- **Path A** — `public.events.hearing.hearing-resulted` → listing directly (set resulted status) +- **Path B** — `public.events.hearing.hearing-resulted` → progression → `public.progression.hearing-resulted-case-updated` → listing (update defendant proceedings concluded) +- **Path C** — `public.hearing.adjourned` → progression → `listing.command.list-court-hearing` (list the adjourned/next hearing) + +### Hearing Context (origin) + +#### Step 1 — Hearing: Command API +- **Endpoint:** `hearing.share-results-v2` (POST `/hearings/{hearingId}/share-results`) +- **Handler:** `HearingCommandApi.shareResultsV2()` +- **Sends command:** `hearing.command.share-results-v2` + +#### Step 2 — Hearing: Command Handler +- **Handler:** `ShareResultsCommandHandler.shareResultV2()` +- **Raises event:** `hearing.events.results-shared-v2` + +#### Step 3 — Hearing: Event Processor +- **Handler:** `PublishResultsV2EventProcessor.resultsShared()` +- **Publishes:** `public.events.hearing.hearing-resulted-success` (UI acknowledgement) +- **Sends command:** `hearing.command.handler.update-offence-results` +- Internally also raises `hearing.event.hearing-adjourned` (for adjournment results) → publishes `public.hearing.adjourned` +- Eventually publishes `public.events.hearing.hearing-resulted` (full result payload) + +### Path A — Listing: Set Hearing Resulted Status + +Listing directly subscribes to the hearing resulted public event. + +#### Step A1 — Listing: Event Processor +- **Subscribes to:** `public.events.hearing.hearing-resulted` (from `cpp-context-hearing`) +- **Handler:** `HearingResultedEventProcessor.handlePublicHearingResulted()` +- **Logic:** Skips SJP hearings. Sends `listing.command.set-hearing-resulted-status` with the hearingId. + +#### Step A2 — Listing: Command Handler +- **Handler:** `HearingResultedCommandHandler.handleSetHearingResultStatus()` +- **Raises aggregate events on Hearing:** + - `listing.event.cases-added-to-hearing` → processor: `UpdateExistingHearingEventProcessor` → publishes `public.listing.cases-added-to-hearing`, `public.events.listing.cases-added-for-updated-related-hearing` + - `listing.event.added-cases-for-hearing` → listener: `ExtendHearingForHearingListener` (viewstore) + - `listing.event.case-resulted-defendant-proceedings-concluded` (aggregate-only) + +### Path B — Listing: Update Case Resulted Defendant Proceedings Concluded + +This path goes hearing → progression → listing. + +#### Step B1 — Progression: Event Processor +- **Subscribes to:** `public.events.hearing.hearing-resulted` (from `cpp-context-hearing`) +- **Handler:** `HearingResultedEventProcessor.handlePublicHearingResulted()` +- **Sends command:** `progression.command.process-hearing-results` + +#### Step B2 — Progression: Internal Processing +- `HearingResultsCommandHandler.processHearingResults()` raises `progression.event.hearing-resulted` +- `HearingResultedEventProcessor.processEvent()` publishes `public.progression.hearing-resulted` +- Eventually `progression.command.hearing-resulted-update-case` → `UpdateCaseHandler` → raises `progression.event.hearing-resulted-case-updated` +- Published as `public.progression.hearing-resulted-case-updated` (via publications descriptor) + +#### Step B3 — Listing: Event Processor +- **Subscribes to:** `public.progression.hearing-resulted-case-updated` (from `cpp-context-progression`) +- **Handler:** `ListingEventProcessor.handleHearingResultedAndCaseUpdated()` +- **Sends command:** `listing.command.update-case-resulted-defendant-proceedings-concluded` + +#### Step B4 — Listing: Command Handler +- **Handler:** `ListingCommandHandler.updateDefendantHearingResultedAndCaseResulted()` +- **Raises aggregate events on Hearing:** + - `listing.event.cases-added-to-hearing` → processor: `UpdateExistingHearingEventProcessor` → publishes `public.listing.cases-added-to-hearing`, `public.events.listing.cases-added-for-updated-related-hearing` + - `listing.event.added-cases-for-hearing` → listener: `ExtendHearingForHearingListener` (viewstore) + - `listing.event.case-resulted-defendant-proceedings-concluded` (aggregate-only) + +### Path C — Listing: List the Adjourned/Next Hearing + +When the result is an adjournment, progression creates the next hearing in listing. + +#### Step C1 — Progression: Event Processor +- **Subscribes to:** `public.hearing.adjourned` (from `cpp-context-hearing`) +- **Handler:** `AdjournHearingEventProcessor.handleHearingAdjournedPublicEvent()` +- **Sends command (cross-context):** `listing.command.list-court-hearing` + +#### Step C2 — Listing: Same as Adhoc Hearing Steps 4-8 + +The flow from `listing.command.list-court-hearing` is identical to the [Adhoc Hearing](#1-adhoc-hearing) flow starting at Step 4: +- `ListingCommandApi.handleListCourtHearing()` → enriched → `ListingCommandHandler.listCourtHearing()` +- Raises `listing.events.hearing-listed` → processor publishes `public.listing.hearing-listed`, adds hearing to case, etc. + +### Summary of Listing Integration Points + +| Direction | Event | Handler in Listing | +|---|---|---| +| **Inbound** | `public.events.hearing.hearing-resulted` | `HearingResultedEventProcessor.handlePublicHearingResulted()` | +| **Inbound** | `public.progression.hearing-resulted-case-updated` | `ListingEventProcessor.handleHearingResultedAndCaseUpdated()` | +| **Inbound** | `listing.command.list-court-hearing` (from progression) | `ListingCommandApi.handleListCourtHearing()` | +| **Outbound** | `public.listing.cases-added-to-hearing` | → `cpp-context-progression` | +| **Outbound** | `public.events.listing.cases-added-for-updated-related-hearing` | → `cpp-context-progression` | +| **Outbound** | `public.listing.hearing-listed` | → `cpp-context-businessprocesses` | +| **Outbound** | `public.listing.hearing-added-to-case` | → `cpp-context-notification` | +| **Outbound** | `public.listing.court-application-added-for-hearing` | → `cpp-context-notification` | + +--- + +## Complete Integration Reference + +Comprehensive view of all cross-context integration points for cpp-context-listing. Data sourced from microservice-analyzer and subscriptions-descriptor.yaml. + +For a visual diagram of all integration points, see [integration-map.mmd](./integration-map.mmd). + +### All Public Events Listing Publishes + +| Public Event | Triggered By (internal event) | Subscribers | +|---|---|---| +| `public.listing.hearing-listed` | `listing.events.hearing-listed` | businessprocesses, notification | +| `public.listing.hearing-confirmed` | `listing.events.hearing-allocated-for-listing`, `listing.events.hearing-allocated-for-listing-v2`, `listing.events.allocated-hearing-extended-for-listing(-v2)` | notification, progression | +| `public.listing.hearing-updated` | `listing.events.allocated-hearing-updated-for-listing(-v2)` | notification, progression | +| `public.listing.hearing-changes-saved` | `listing.events.hearing-allocated-for-listing(-v2)`, `listing.events.hearing-partially-updated`, `listing.events.hearing-changes-saved`, `listing.events.hearing-days-changed-for-hearing` | notification | +| `public.listing.hearing-partially-updated` | `listing.events.hearing-partially-updated` | notification, progression | +| `public.listing.hearing-requested-for-listing` | `listing.events.hearing-requested-for-listing` | progression | +| `public.listing.hearing-added-to-case` | `listing.events.hearing-added-to-case` | notification | +| `public.listing.hearing-days-changed-for-hearing` | `listing.events.hearing-days-changed-for-hearing` | notification | +| `public.listing.hearing-days-sequenced` | `listing.events.hearing-days-sequenced` | notification | +| `public.listing.hearings-update-completed` | `listing.events.hearings-update-completed` | notification | +| `public.listing.court-application-added-for-hearing` | `listing.events.hearing-listed`, `listing.events.court-application-added-for-hearing` | notification | +| `public.listing.court-list-restricted` | `listing.events.court-list-restricted` | hearing, notification | +| `public.listing.court-list-published` | (court list publish flow) | progression | +| `public.listing.court-daily-list` | (court list export flow) | _(none)_ | +| `public.listing.cases-added-to-hearing` | `listing.event.cases-added-to-hearing` | notification, progression | +| `public.listing.new-defendant-added-for-court-proceedings` | `listing.events.new-defendant-added-for-court-proceedings` | progression | +| `public.listing.create-next-hearing-requested` | `listing.events.create-next-hearing-requested` | progression | +| `public.listing.judiciary-changed-for-hearings-status` | `listing.events.judiciary-changed-for-hearings-status` | notification | +| `public.listing.created-listing-note` | `listing.events.created-listing-note` | notification | +| `public.listing.note-edited` | `listing.events.listing-note-edited` | notification | +| `public.listing.deleted-listing-note` | `listing.events.deleted-listing-note` | notification | +| `public.listing.vacated-trial-updated` | `listing.events.trial-vacated`, `listing.events.hearing-rescheduled` | hearing, progression | +| `public.listing.offences-moved-to-next-hearing` | `listing.events.next-hearing-replaced` | progression | +| `public.listing.hearing-unallocated-courtroom-removed` | `listing.events.hearing-unallocated-courtroom-removed` | hearing, progression | +| `public.events.listing.allocated-hearing-deleted` | `listing.events.allocated-hearing-deleted` | hearing, progression | +| `public.events.listing.unallocated-hearing-deleted` | `listing.events.unallocated-hearing-deleted` | progression | +| `public.events.listing.hearing-deleted` | `listing.events.hearing-deleted` | progression | +| `public.events.listing.hearing-days-without-court-centre-corrected` | `listing.events.hearing-days-without-court-centre-corrected` | progression | +| `public.events.listing.cases-added-for-updated-related-hearing` | `listing.event.cases-added-to-hearing` | progression | +| `public.events.listing.next-hearing-day-changed` | `listing.events.next-hearing-day-changed` | hearing | +| `public.events.listing.hearing-unallocated` | `listing.events.hearing-unallocated-for-listing` | hearing, progression | +| `public.events.listing.offences-removed-from-allocated-hearing` | `listing.events.offences-removed-from-existing-allocated-hearing` | progression | +| `public.events.listing.offences-removed-from-existing-allocated-hearing` | `listing.events.offences-removed-from-hearing`, `listing.events.offences-removed-from-existing-allocated-hearing` | hearing, progression | +| `public.events.listing.offences-removed-from-existing-unallocated-hearing` | `listing.events.offences-removed-from-existing-unallocated-hearing` | progression | +| `public.events.listing.offences-removed-from-unallocated-hearing` | `listing.events.offences-removed-from-hearing` | progression | + +### All Public Events Listing Subscribes To + +| Public Event | Published By | Handler in Listing | +|---|---|---| +| `public.events.hearing.hearing-resulted` | hearing, sjp | `HearingResultedEventProcessor` | +| `public.events.hearing.marked-as-duplicate` | hearing | `MarkHearingAsDuplicateEventProcessor` | +| `public.hearing.trial-vacated` | hearing | `ListingEventProcessor` | +| `public.hearing.hearing-days-cancelled` | hearing | `CancelHearingDaysEventProcessor` | +| `public.hearing.defence-counsel-added` | hearing | `CounselModifiedEventProcessor` | +| `public.hearing.defence-counsel-updated` | hearing | `CounselModifiedEventProcessor` | +| `public.hearing.defence-counsel-removed` | hearing | `CounselModifiedEventProcessor` | +| `public.hearing.prosecution-counsel-added` | hearing | `CounselModifiedEventProcessor` | +| `public.hearing.prosecution-counsel-updated` | hearing | `CounselModifiedEventProcessor` | +| `public.hearing.prosecution-counsel-removed` | hearing | `CounselModifiedEventProcessor` | +| `public.hearing.selected-offences-removed-from-existing-hearing` | hearing | `RemoveSelectedOffencesEventProcessor` | +| `public.progression.defendant-offences-changed` | progression | `ListingEventProcessor` | +| `public.progression.case-defendant-changed` | progression | `ListingEventProcessor` | +| `public.progression.court-application-changed` | progression | `ListingEventProcessor` | +| `public.progression.defendants-added-to-court-proceedings` | progression | `ListingEventProcessor` | +| `public.progression.case-markers-updated` | progression | `ListingEventProcessor` | +| `public.progression.hearing-resulted-case-updated` | progression | `ListingEventProcessor` | +| `public.progression.defendant-legalaid-status-updated` | progression | `ListingEventProcessor` | +| `public.progression.case-linked` | progression | `ListingEventProcessor` | +| `public.progression.case-removed-from-group-cases` | progression | `CaseRemovedFromGroupCasesEventProcessor` | +| `public.progression.application-offences-updated` | progression | `ListingEventProcessor` | +| `public.progression.application-laa-reference-updated-for-application` | progression | `ListingEventProcessor` | +| `public.progression.events.case-or-application-ejected` | progression | `EjectCaseEventProcessor` | +| `public.progression.events.cps-prosecutor-updated` | progression | `CpsProsCounselEventProcessor` | +| `public.progression.events.hearing-extended` | progression | `ListingEventProcessor` | +| `public.progression.events.court-application-deleted` | progression | `CourtApplicationDeletedEventProcessor` | +| `public.progression.related-hearing-updated-for-adhoc-hearing` | progression | `ListingEventProcessor` | +| `public.referencedata.event.courtroom-added` | reference-data | `CacheRefDataCourtroomView` | +| `public.referencedata.event.courtroom-closed` | reference-data | `CacheRefDataCourtroomView` | + +### Outbound REST API Calls (listing → other services) + +| Target Service | Endpoint | Called From | Purpose | +|---|---|---|---| +| **courtscheduler** | `GET /courtschedule/search.court-schedules-by-id` | `CourtSchedulerService` | Fetch court schedules for enrichment | +| **courtscheduler** | `GET /hearingslots` | `HearingSlotsService`, `CourtSchedulerServiceAdapter` | Search hearing slots / judicial roles | +| **courtscheduler** | `GET /searchlist/hearingslots` | `HearingSlotsService` | Search and book hearing slots | +| **courtscheduler** | `GET /multidaysearchandbook/hearingslots` | `HearingSlotsService` | Multi-day search and book | +| **courtscheduler** | `PUT /list/hearingslots` | `HearingSlotsService` | List hearings in court sessions | +| **courtscheduler** | `POST /validate-session-availability` | `HearingSlotsService` | Validate session availability | +| **courtscheduler** | `GET /provisionalBooking` | `ProvisionalBookingService` | Get provisional booking slots | +| **courtscheduler** | `DELETE /provisionalBooking` | `ProvisionalBookingService` | Remove provisional booking | +| **progression** | `progression.query.prosecutioncase` | `ProgressionService` (Requester) | Get prosecution case details | +| **progression** | `progression.query.case-notes` | `ProgressionService` (Requester) | Get case notes | +| **progression** | `progression.query.application-notes` | `ProgressionService` (Requester) | Get application notes | +| **defence** | `defence.query.get-case-by-person-defendant` | `HearingQueryApi` (Requester) | Search cases by person defendant | +| **defence** | `defence.query.get-case-by-organisation-defendant` | `HearingQueryApi` (Requester) | Search cases by organisation defendant | +| **reference-data** | `referencedata.query.courtroom` | `ReferenceDataService` (Requester) | Get court centre details | +| **reference-data** | `referencedata.query.judiciaries` | `ReferenceDataService` (Requester) | Get judiciary names | +| **reference-data** | `referencedata.query.hearing-types` | `ReferenceDataService` (Requester) | Get hearing type descriptions | +| **reference-data** | `referencedata.query.courtrooms` | `CacheRefDataCourtroomLoader` (Requester) | Load all courtrooms for cache | +| **users-groups** | `usersgroups.is-logged-in-user-has-permission-for-action` | `UsersGroupsService` (Requester) | Check user permissions | +| **document-generator** | `generateDocument()` | `DocumentGeneratorClient` | Generate court list PDFs | +| **xhibit-gateway** | `WebDAV PUT` | `XhibitSession` | Send court list exports to Xhibit | + +### Cross-Context Commands (other services → listing) + +| Command | Sent By | Handler in Listing | +|---|---|---| +| `listing.command.list-court-hearing` | progression | `ListingCommandApi.handleListCourtHearing()` | +| `listing.command.list-unscheduled-court-hearing` | progression | `ListingCommandApi.handleListUnscheduledCourtHearing()` | diff --git a/docs/development-guidelines/transactions.md b/docs/development-guidelines/transactions.md new file mode 100644 index 000000000..7e2b13c05 --- /dev/null +++ b/docs/development-guidelines/transactions.md @@ -0,0 +1,125 @@ +Here's how to debug transactions in IntelliJ IDEA: + + ──────────────────────────────────────── + + + + 1. See How Annotations Affect Transactions + + Your project uses two different transaction annotation styles: + + ┌───────────────────────────────┬───────────────────────────────────────────────────────────────┬───────────────────────────────────────────────┐ + │ Annotation │ Used in │ Behavior │ + ├───────────────────────────────┼───────────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤ + │ @TransactionAttribute(REQUIRE │ CourtScheduleRetryService │ Container starts a new TX, suspends any │ + │ S_NEW) │ │ existing one │ + │ (EJB) │ │ │ + │ @Transactional(REQUIRES_NEW) │ RotaFilePartialProcessor, JudiciaryAssignmentService, │ Same as above, but via CDI interceptor │ + │ (JTA) │ JudiciaryUnassignmentService │ │ + │ @Transactional (no qualifier) │ SessionsService, CourtScheduleRepository │ Uses REQUIRED (default) -- joins existing TX │ + │ │ │ or creates new one │ + │ @TransactionAttribute(NOT_SUP │ RotaFileProcessorService │ Suspends any existing TX, runs without one │ + │ PORTED) │ │ │ + └───────────────────────────────┴───────────────────────────────────────────────────────────────┴───────────────────────────────────────────────┘ + + To debug which path is taken in IntelliJ: + Set breakpoints on the transaction interceptor/proxy classes: + • Run > Debug Configurations -- connect to your WildFly remote debug port (default 8787) + • Set breakpoints in these framework classes (use Ctrl+N to open): + • com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionImple -- the actual Narayana/Arjuna TX implementation in WildFly + • com.arjuna.ats.jta.UserTransaction -- for begin()/commit()/rollback() + • Or more practically, set method breakpoints on your annotated methods and check the call stack to see the EJB/CDI interceptor + creating/joining transactions. + + + + 2. See Which Transaction Is Active at a Breakpoint + + When stopped at any breakpoint, use IntelliJ's Evaluate Expression (Alt+F8) to inspect the active transaction: + + // Get the current transaction status + com.arjuna.ats.jta.TransactionManager.transactionManager().getTransaction() + + + // Check transaction status (0 = STATUS_ACTIVE, 1 = STATUS_MARKED_ROLLBACK, etc.) + com.arjuna.ats.jta.TransactionManager.transactionManager().getStatus() + + + // Get the transaction ID (Arjuna UID) + com.arjuna.ats.jta.TransactionManager.transactionManager().getTransaction().toString() + + You can also evaluate: + + // Check if EntityManager is joined to a TX + entityManager.isJoinedToTransaction() + + Tip: Add these as watches in the Variables/Watches panel so they update automatically at every breakpoint. + + + 3. See Actual SQL Queries with Parameter Values + + + + Option A: Enable Hibernate SQL Logging (Recommended) + + Your persistence.xml already has hibernate.show_sql set to false. To see queries with parameters, change these properties temporarily: + + + + + + But show_sql only prints queries without bind parameter values. To see parameter values, you need to set the Hibernate log level. Add this to + your WildFly standalone.xml (or via CLI): + + + + + + + + + Or via WildFly CLI: + + /subsystem=logging/logger=org.hibernate.SQL:add(level=DEBUG) + /subsystem=logging/logger=org.hibernate.type.descriptor.sql.BasicBinder:add(level=TRACE) + + This will log output like: + + DEBUG org.hibernate.SQL - insert into court_schedule (id, ...) values (?, ?, ?) + TRACE o.h.t.d.s.BasicBinder - binding parameter [1] as [VARCHAR] - [abc-123] + TRACE o.h.t.d.s.BasicBinder - binding parameter [2] as [DATE] - [2026-03-01] + + + + Option B: Use a JDBC Proxy (P6Spy / Datasource Proxy) + + For the cleanest view of all queries with inline parameter values, add p6spy as a JDBC wrapper. But this requires modifying the datasource config + on WildFly, so Option A is usually easier. + + + Option C: IntelliJ Database Console + Breakpoint Combo + + 1. Connect IntelliJ's Database tool (View > Tool Windows > Database) to your database + 2. Run the query SELECT * FROM pg_stat_activity (PostgreSQL) or equivalent to see live queries while you're paused at a breakpoint + + + + 4. Practical Debugging Workflow + + Here's a step-by-step workflow for a typical scenario, e.g. debugging RotaFilePartialProcessor.processFullRotaFile(): + 1. Start WildFly in debug mode (add -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8787 to JAVA_OPTS) + 2. In IntelliJ, create a Remote JVM Debug run configuration pointing to localhost:8787 + 3. Set a breakpoint on line 72-73 of RotaFilePartialProcessor (the @Transactional(REQUIRES_NEW) method) + 4. When it hits, check the Frames panel -- you'll see the CDI transaction interceptor in the call stack + 5. In Evaluate Expression, run: + + + com.arjuna.ats.jta.TransactionManager.transactionManager().getTransaction().toString() + + This gives you the Arjuna TX ID. Note it down. + 6. Step into sessionsService.updateSlotsAndSchedules() (which has @Transactional = REQUIRED) and evaluate the same expression -- you'll see the + same TX ID, confirming it joined the existing transaction + 7. Step into courtScheduleRetryService.upsertOne() (which has @TransactionAttribute(REQUIRES_NEW)) -- you'll see a different TX ID, confirming + a new transaction was created + + This way you can map exactly how the annotation semantics play out across your service call chain. diff --git a/docs/future-improvements/junit4-migration.md b/docs/future-improvements/junit4-migration.md new file mode 100644 index 000000000..859f36320 --- /dev/null +++ b/docs/future-improvements/junit4-migration.md @@ -0,0 +1,100 @@ +# JUnit 4 to JUnit 5 Migration Notes + +## Current State + +This project uses **both JUnit 4.13.1 and JUnit 5 (Jupiter)**. The JUnit Vintage Engine is included to run legacy JUnit 4 tests on the JUnit 5 platform. Most modules have already migrated to JUnit 5. + +## Files Still Using JUnit 4 + +### 1. CourtServicesMapperTest.java — Full JUnit 4 test class (Medium effort) + +**Path:** `listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/mapper/CourtServicesMapperTest.java` + +**JUnit 4 usage:** +- `org.junit.Test` +- `org.junit.Before` (3 separate `@Before` methods) +- `@Test(expected = InvalidDataException.class)` +- `MockitoAnnotations.initMocks(this)` (deprecated Mockito pattern) + +**Migration steps:** +| JUnit 4 | JUnit 5 | +|----------|---------| +| `import org.junit.Test` | `import org.junit.jupiter.api.Test` | +| `import org.junit.Before` | `import org.junit.jupiter.api.BeforeEach` | +| `@Before` | `@BeforeEach` | +| `@Test(expected = X.class)` | `assertThrows(X.class, () -> { ... })` | +| `MockitoAnnotations.initMocks(this)` | `@ExtendWith(MockitoExtension.class)` on the class | + +--- + +### 2. PersistenceTestsIT.java — Full JUnit 4 test class (Hard — blocked by CdiTestRunner) + +**Path:** `listing-integration-test-persistence/src/test/java/uk/gov/moj/cpp/listing/persistence/repository/PersistenceTestsIT.java` + +**JUnit 4 usage:** +- `@RunWith(CdiTestRunner.class)` (DeltaSpike JUnit 4 runner) +- `org.junit.Test`, `org.junit.Before`, `org.junit.Assert` + +**Why it's hard:** +DeltaSpike's `CdiTestRunner` is a JUnit 4 runner. Migration requires either: +1. A DeltaSpike version that provides a JUnit 5 extension (DeltaSpike 2.x+) +2. Switching to an alternative like `@EnableAutoWeld` from Weld JUnit 5 extensions +3. Keeping the JUnit Vintage Engine for this module (current strategy) + +**Note:** The `listing-integration-test-persistence/pom.xml` explicitly includes `junit-vintage-engine` for this reason. + +--- + +### 3. EventAggregateConverterTest.java — Mixed: JUnit 5 test with JUnit 4 assertion (Easy fix) + +**Path:** `listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/EventAggregateConverterTest.java` + +**Issue:** Uses `org.junit.jupiter.api.Test` (JUnit 5) but calls `org.junit.Assert.assertThat()` (JUnit 4, deprecated). + +**Fix:** Replace the one `Assert.assertThat()` call on line 44: +```java +// Before +import org.junit.Assert; +Assert.assertThat(result, is(expected)); + +// After (already used on lines 54 and 63 of this file) +import org.hamcrest.MatcherAssert; +MatcherAssert.assertThat(result, is(expected)); +``` + +Then remove the `import org.junit.Assert;` line. + +--- + +### 4. ListCourtHearingSteps.java — Cucumber steps using JUnit 4 assertion (Easy fix) + +**Path:** `listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListCourtHearingSteps.java` + +**Issue:** Uses `org.junit.Assert.assertFalse` in 2 places (lines 2241, 2281). + +**Fix:** +```java +// Before +import org.junit.Assert; +Assert.assertFalse(newCaseIds.contains(allocatedHearingCaseId)); + +// After +import static org.junit.jupiter.api.Assertions.assertFalse; +assertFalse(newCaseIds.contains(allocatedHearingCaseId)); +``` + +--- + +## Suggested Migration Order + +1. **EventAggregateConverterTest.java** — trivial one-line fix +2. **ListCourtHearingSteps.java** — trivial two-line fix +3. **CourtServicesMapperTest.java** — moderate refactor, no external blockers +4. **PersistenceTestsIT.java** — blocked by DeltaSpike/CdiTestRunner dependency; defer or keep Vintage Engine + +## Post-Migration Cleanup + +Once files 1-3 are migrated: +- Remove `junit:junit:4.13.1` from root `pom.xml` `` (if PersistenceTestsIT still needs it, scope it to that module only) +- Remove `junit-vintage-engine` from modules that no longer need it +- Keep `junit-vintage-engine` only in `listing-integration-test-persistence/pom.xml` until CdiTestRunner is replaced diff --git a/docs/future-improvements/springboot-query-migration.md b/docs/future-improvements/springboot-query-migration.md new file mode 100644 index 000000000..74894aa56 --- /dev/null +++ b/docs/future-improvements/springboot-query-migration.md @@ -0,0 +1,312 @@ +# Spring Boot Migration: listing-query → Standalone Service + +**Status:** Proposal | **Date:** 2026-03-25 + +## Problem + +The listing-query module is tightly coupled to the CPP framework (Wildfly, CDI, DeltaSpike, RAML, Envelope/JsonEnvelope). This creates: + +1. **Slow startup** — Wildfly WAR deployment adds overhead vs embedded Tomcat/Netty +2. **Framework lock-in** — DeltaSpike repositories, `@Handles` annotations, `Requester` pattern, and RAML-generated resource classes are all CPP-framework-specific +3. **Monolithic deployment** — Query and command share the same Wildfly instance despite CQRS separation +4. **Testing friction** — Integration tests require cpp-developers-docker or Tilt for the full Wildfly stack +5. **Developer experience** — No hot-reload, slow feedback loops, no Spring ecosystem tooling + +## Proposal + +Create a new Spring Boot application (`listing-query-springboot`) that serves all 19 existing query endpoints. The current listing-query module becomes a thin proxy that forwards requests to the new Spring Boot app during the migration period. + +### Architecture + +``` + ┌──────────────────────────────────┐ + UI / Other │ listing-query (Wildfly) │ + Contexts ───► │ (proxy — forwards to Spring) │ + └──────────────┬───────────────────┘ + │ HTTP forward + ▼ + ┌──────────────────────────────────┐ + │ listing-query-springboot (new) │ + │ ┌────────────────────────────┐ │ + │ │ REST Controllers │ │ + │ │ (Spring MVC) │ │ + │ ├────────────────────────────┤ │ + │ │ Service Layer │ │ + │ │ (business logic) │ │ + │ ├────────────────────────────┤ │ + │ │ Spring Data JPA │ │ + │ │ Repositories │ │ + │ ├────────────────────────────┤ │ + │ │ WebClient / RestClient │ │ + │ │ (cross-context calls) │ │ + │ └────────────────────────────┘ │ + │ PostgreSQL (viewstore) │ + └──────────────────────────────────┘ +``` + +## Requirements + +### R1 — Functional Parity + +All 19 query endpoints must return identical responses. No behavioural changes. + +| # | Endpoint | Method | Action | +|---|---|---|---| +| 1 | `/hearings` | GET | `listing.search.hearings` | +| 2 | `/hearings/{id}` | GET | `listing.search.hearing` | +| 3 | `/hearings/range-search` | GET | `listing.range.search.hearings` | +| 4 | `/hearings/range-search` | GET | `listing.range.search.hearings.court.calendar` | +| 5 | `/hearings/available-search` | GET | `listing.available.search.hearings` | +| 6 | `/hearings/allocated-and-unallocated` | GET | `listing.allocated.and.unallocated.hearings` | +| 7 | `/hearings/any-allocation` | GET | `listing.any-allocation.search.hearings` | +| 8 | `/hearings/cotr-search` | GET | `listing.cotr.search.hearings` | +| 9 | `/hearings/unscheduled` | GET | `listing.unscheduled.search.hearings` | +| 10 | `/hearings/range-search` (judge) | GET | `listing.range.search.hearings.for.judge.list` | +| 11 | `/hearingSlots` | GET | `listing.search.hearing.slots` | +| 12 | `/sessionAvailabilityValidation` | POST | `listing.validate.session.availability` | +| 13 | `/courtlist` | GET | `listing.search.court.list` | +| 14 | `/courtlistpayload` | GET | `listing.search.court.list.payload` | +| 15 | `/courtListPublishStatus/{id}` | GET | `listing.court.list.publish.status` | +| 16 | `/cases/by-person-defendant-and-hearingDate` | GET | `listing.get.cases-by-person-defendant` | +| 17 | `/cases/by-organisation-defendant-and-hearingDate` | GET | `listing.get.cases-by-organisation-defendant` | +| 18 | `/cache-refdata-courtrooms/*` | GET/POST | `listing.update.add-courtroom`, `close-courtroom`, `refresh` | +| 19 | `/hearings/download-hearing-csv-report` | GET | `listing.query.download-hearing-csv-report` | + +### R2 — Cross-Context Service Calls + +The query module calls 5 external services via the CPP `Requester` pattern. These must be replaced with Spring `WebClient` or `RestClient` HTTP calls. + +| Service | Current (Requester) | Spring Boot Replacement | +|---|---|---| +| **courtscheduler** | `CourtSchedulerServiceAdapter` (Apache HttpClient) | `WebClient` to courtscheduler REST API | +| **progression** | `ProgressionService` → `Requester` | `WebClient` to progression query API | +| **defence** | `HearingQueryApi` → `Requester` | `WebClient` to defence query API | +| **reference-data** | `ReferenceDataService` → `Requester` | `WebClient` to reference-data query API | +| **users-groups** | `UsersGroupsService` → `Requester` | `WebClient` to users-groups query API | +| **document-generator** | `DocumentGeneratorClient` (platform lib) | `WebClient` to document-generator API | + +**Challenge:** The `Requester` pattern resolves service URLs at runtime via JNDI. Spring Boot will need explicit service URL configuration (environment variables or service discovery). + +### R3 — Database Access + +The viewstore is PostgreSQL with Hibernate/JPA and DeltaSpike Data repositories. Migration path: + +| Current | Spring Boot | +|---|---| +| DeltaSpike `@Repository` interfaces | Spring Data JPA `JpaRepository` interfaces | +| Native SQL via `@Query` (DeltaSpike) | `@Query` (Spring Data JPA) — syntax is nearly identical | +| `HearingJdbcRepository` (raw JDBC) | `JdbcTemplate` or `NamedParameterJdbcTemplate` | +| `CourtListPublishStatusJdbcRepository` | `JdbcTemplate` | +| Hibernate JSONB via `JsonNodeBinaryType` | Hypersistence `JsonType` or custom `AttributeConverter` | +| `PersistenceUnit` via `persistence.xml` | Spring auto-configuration via `application.yml` | + +**Key entities:** `Hearing`, `HearingDays`, `ListedCases`, `CourtApplications`, `Notes`, `CacheRefDataCourtroom`, `CaseByDefendant`, `PublishedCourtList` + +### R4 — Security / Access Control + +| Current | Spring Boot | +|---|---| +| DRL (Drools) files for per-endpoint authorization | Spring Security with custom `@PreAuthorize` or filter chain | +| `CJSCPPUID` header for user identity | Preserve same header; extract in Spring filter | +| `capability-manifest.json` | Not needed (Spring Boot manages its own capabilities) | + +### R5 — Proxy Mode in listing-query (Wildfly) + +During migration, the existing listing-query-api WAR acts as a reverse proxy: + +- Each `@Handles` method in `HearingQueryView` and `HearingQueryApi` is modified to forward the request to the Spring Boot app via HTTP +- The proxy adds the `CJSCPPUID` header and passes all query parameters +- If the Spring Boot app is unreachable, fall back to the original implementation (feature-flagged) +- Feature flag: `listing.query.springboot.enabled` (default: `false`) +- Base URL: `listing.query.springboot.baseUrl` (default: `http://localhost:8090`) + +### R6 — Observability + +| Concern | Implementation | +|---|---| +| Health checks | Spring Actuator `/actuator/health` | +| Metrics | Micrometer → Prometheus (align with cpp-environment-dashboard) | +| Logging | SLF4J + Logback (same as current) | +| Tracing | Micrometer Tracing (propagate existing correlation IDs) | + +### R7 — Deployment + +| Concern | Implementation | +|---|---| +| Docker image | Multi-stage Dockerfile (build + JRE-slim runtime) | +| Helm chart | New chart in `cpp-helm-chart` or sub-chart of existing listing chart | +| AKS | Deploy alongside listing-service in same namespace | +| Environments | Feature-flagged rollout: dev → sit → ste → nft → prp → prd | + +## Migration Steps + +### Phase 1 — Scaffold & Core (estimate: 3-5 days) + +1. **Create Maven module** `listing-query-springboot` under cpp-context-listing + - Spring Boot 3.x, Java 17, Spring Web, Spring Data JPA, Spring Actuator + - PostgreSQL driver, Hypersistence Utils (JSONB support) + - Add as a module in the root `pom.xml` + +2. **Configure datasource** in `application.yml` + - Point to same PostgreSQL viewstore database + - Configure connection pool (HikariCP) + - Hibernate dialect for PostgreSQL + +3. **Migrate JPA entities** from `listing-viewstore-persistence` + - Copy entity classes (`Hearing`, `HearingDays`, `ListedCases`, etc.) + - Replace `JsonNodeBinaryType` with Hypersistence `JsonType` + - Replace `PostgresUUIDType` with Hibernate 6 native UUID support + - Keep same table/column mappings + +4. **Create Spring Data JPA repositories** + - Translate DeltaSpike `@Query` annotations to Spring Data equivalents + - Migrate `HearingJdbcRepository` to `JdbcTemplate` + - Migrate `CourtListPublishStatusJdbcRepository` to `JdbcTemplate` + - Add pagination support via `Pageable` + +### Phase 2 — Service Layer (estimate: 3-5 days) + +5. **Migrate cross-context service clients** + - Create `WebClient` beans for each external service + - `CourtSchedulerClient` — replace `HearingSlotsService` + `CourtSchedulerServiceAdapter` + - `ProgressionClient` — replace `ProgressionService` + - `ReferenceDataClient` — replace `ReferenceDataService` + `CacheRefDataCourtroomLoader` + - `DefenceClient` — replace defence requester calls + - `UsersGroupsClient` — replace `UsersGroupsService` + - `DocumentGeneratorClient` — replace platform lib client + - Configure base URLs via environment variables + +6. **Migrate business logic** + - `RangeSearchQuery` → `RangeSearchService` + - `CourtListService` → `CourtListService` (same name, Spring `@Service`) + - `HearingToJsonConverter` and other converters → keep as-is or replace with Jackson serialization + - `HearingCsvReportService` → `HearingCsvReportService` + - Template assemblers for document generation → keep as-is + +### Phase 3 — REST Controllers (estimate: 2-3 days) + +7. **Create Spring MVC controllers** + - `HearingController` — hearing search endpoints (9 endpoints) + - `CourtListController` — court list endpoints (3 endpoints) + - `CaseController` — case search endpoints (2 endpoints) + - `HearingSlotController` — hearing slots and session availability (2 endpoints) + - `CacheController` — courtroom cache management (3 endpoints) + - `ReportController` — CSV report download (1 endpoint) + - Ensure request/response formats match existing RAML contracts exactly + +8. **Add security** + - Spring Security filter chain + - Extract `CJSCPPUID` from request header + - Replicate DRL authorization rules as `@PreAuthorize` expressions or custom voters + - Add `SecurityContext` holder for user identity + +### Phase 4 — Proxy & Testing (estimate: 3-5 days) + +9. **Implement proxy in listing-query-api (Wildfly)** + - Add `HttpClient` dependency to listing-query-api + - Create `SpringBootQueryProxy` service class + - Modify each `@Handles` method to check feature flag and forward + - Preserve all headers, query params, and path variables + - Add circuit breaker / fallback to original implementation + +10. **Contract testing** + - For each endpoint, create a test that: + 1. Sends a request to the Wildfly proxy + 2. Sends the same request directly to Spring Boot + 3. Asserts both responses are identical (JSON diff) + - Use existing integration test payloads as seed data + - Run against a shared PostgreSQL instance + +11. **Integration testing** + - Add Testcontainers-based tests for Spring Boot module + - PostgreSQL + Liquibase (reuse existing `listing-viewstore-liquibase`) + - WireMock for cross-context service stubs (courtscheduler, progression, etc.) + +### Phase 5 — Deployment & Rollout (estimate: 2-3 days) + +12. **Dockerize** + - Multi-stage Dockerfile + - Add to existing CI/CD pipeline (`context-validation.yaml`) + - Push to Azure Container Registry + +13. **Helm chart** + - Create chart for `listing-query-springboot` + - Configure service, ingress, health probes + - Environment-specific values (DB URL, service URLs, feature flags) + +14. **Rollout** + - Deploy to dev with feature flag `listing.query.springboot.enabled=false` + - Enable proxy on dev, run smoke tests + - Progressive rollout through environments + - Monitor latency, error rates, response parity + - Once stable: remove proxy, point consumers directly to Spring Boot + +### Phase 6 — Cleanup (estimate: 1-2 days) + +15. **Remove proxy code** from listing-query-api +16. **Update RAML** to point to new base URL (or keep reverse proxy in ingress) +17. **Update documentation** — journeys.md, integration-map.mmd +18. **Archive** old listing-query-view handler classes + +## Tech Stack (Spring Boot App) + +| Layer | Technology | +|---|---| +| Framework | Spring Boot 3.x | +| Web | Spring MVC (or WebFlux for reactive) | +| Database | Spring Data JPA + JdbcTemplate | +| Connection Pool | HikariCP | +| JSONB | Hypersistence Utils | +| HTTP Clients | Spring WebClient (reactive) or RestClient (blocking) | +| Security | Spring Security | +| Observability | Spring Actuator + Micrometer | +| Testing | JUnit 5, Testcontainers, WireMock, Spring MockMvc | +| Build | Maven, Spring Boot Maven Plugin | +| Deployment | Docker, Helm, AKS | + +## Risks and Mitigations + +| Risk | Likelihood | Impact | Mitigation | +|---|---|---|---| +| Response format differences between old and new | High | High | Contract tests comparing JSON responses field-by-field | +| Cross-context service URL configuration | Medium | Medium | Use K8s service DNS (`http://listing-courtscheduler:8080`) | +| JSONB handling differences between Hibernate versions | Medium | Medium | Hypersistence Utils handles this; test with production data snapshots | +| DRL authorization rules are complex to translate | Medium | High | Extract rules into a shared config; start with permissive and tighten | +| Performance regression on complex queries | Low | High | Benchmark with production query patterns before rollout | +| Wildfly proxy adds latency during migration | Medium | Low | Proxy period should be short; connection pooling mitigates | +| Two apps reading same viewstore | Low | Low | Query module is read-only; no write conflicts | + +## Key Classes to Migrate + +### From listing-query-view (31 @Handles methods in HearingQueryView) + +Split into focused services: + +| Current Handler Method | New Spring Service | +|---|---| +| `searchHearings()`, `searchAvailableHearings()`, `searchAllocatedAndUnallocatedHearings()`, `searchHearingsWithAnyAllocationState()`, `searchUnscheduledHearings()`, `searchHearingsForCotr()` | `HearingSearchService` | +| `rangeSearchHearings()`, `rangeSearchHearingsForCourtCalendar()`, `searchHearingsForJudge()` | `RangeSearchService` | +| `retrieveCourtList()`, `getCourtListContent()`, `getPublishedCourtLists()`, `getCourtListPublishStatus()` | `CourtListService` | +| `getCasesByDefendantAndHearingDate()` | `CaseSearchService` | +| `getHearingById()` | `HearingDetailService` | +| `generateHearingCsvReport()` | `HearingReportService` | + +### From listing-query-api + +| Current Class | New Spring Class | +|---|---| +| `HearingQueryApi` (RAML-generated) | `HearingController` (@RestController) | +| `DefaultQueryApiHearingSlotsResource` | `HearingSlotController` | +| `DefaultQueryApiSessionAvailabilityValidationResource` | `SessionAvailabilityController` | +| `CacheRefDataCourtroomApi` | `CacheController` | +| `DocumentGeneratorClient` | `DocumentGeneratorWebClient` | +| DRL access control rules | Spring Security config | + +## Dependencies on Other Work + +- **Testcontainers migration** (see `tilt-testcontainers-migration-analysis/`) — would simplify Spring Boot integration testing +- **Parent POM** — Spring Boot has its own parent; may need to use `spring-boot-dependencies` BOM instead of inheriting from `cpp-platform-maven-service-parent-pom` +- **listing-common** — Shared classes (`CourtSchedulerServiceAdapter`, domain objects, CSV constants) will need to be either: + - Kept as a dependency (if no CDI annotations) + - Duplicated into the Spring Boot module (if tightly coupled to CDI) + - Refactored into a framework-agnostic shared JAR diff --git a/docs/future-improvements/thingstoresolve.md b/docs/future-improvements/thingstoresolve.md new file mode 100644 index 000000000..562edc3d5 --- /dev/null +++ b/docs/future-improvements/thingstoresolve.md @@ -0,0 +1,41 @@ +# Things to Resolve in cpp-context-listing + +## 1. Cyclic Calls + +The following handlers emit events that route back to themselves, creating 2-node cycles. +These are likely intentional fan-out patterns (processing items one at a time), but should +be reviewed to confirm they have proper termination conditions. + +### listing.command.mark-hearing-as-duplicate-for-case +- **Cycle:** `listing.command.mark-hearing-as-duplicate-for-case` (COMMAND_HANDLER) -> `listing.events.hearing-marked-as-duplicate` (EVENT_LISTENER) -> back to start +- **Risk:** If there's no guard, marking one hearing as duplicate could trigger an infinite loop across cases. + +### listing.command.remove-offences-from-existing-hearing +- **Cycle:** `listing.command.remove-offences-from-existing-hearing` (COMMAND_HANDLER) -> `listing.events.remove-offences-from-existing-hearing-requested` (EVENT_PROCESSOR) -> back to start +- **Risk:** The handler fires a "requested" event that loops back to itself — likely iterating through offences individually. + +### listing.command.list-unscheduled-next-hearing +- **Cycle:** `listing.command.list-unscheduled-next-hearing` (COMMAND_HANDLER) -> `listing.events.unscheduled-next-hearing-requested` (EVENT_PROCESSOR) -> back to start +- **Risk:** Processes one hearing, fires a "requested" event for the next — needs a clear stop condition. + +--- + +## 2. Command API Naming — Needs Renaming + +The following COMMAND_API handlers are incorrectly named `listing.command.X` instead of +`listing.X`. This means the COMMAND_API and COMMAND_HANDLER share the exact same action +name, which breaks the naming convention and makes flow tracing ambiguous. + +The convention is: COMMAND_API handles `listing.X` and forwards to `listing.command.X` +on the COMMAND_HANDLER. These 7 all skip that and go straight to `listing.command.X` on +both sides. + +| # | Current action name (used by both API & Handler) | COMMAND_API class.method | COMMAND_HANDLER class.method | Correct API name | +|---|--------------------------------------------------|--------------------------|------------------------------|------------------| +| 1 | `listing.command.restrict-court-list` | `ListingCommandApi.handleRestrictCourtList()` | `ListingCommandHandler.restrictFromCourtList()` | `listing.restrict-court-list` | +| 2 | `listing.command.sequence-hearings` | `ListingCommandApi.handleSequenceHearings()` | `ListingCommandHandler.sequenceHearings()` | `listing.sequence-hearings` | +| 3 | `listing.command.change-judiciary-for-hearings` | `ListingCommandApi.handleChangeJudiciaryForHearings()` | `ListingCommandHandler.changeJudiciaryForHearings()` | `listing.change-judiciary-for-hearings` | +| 4 | `listing.command.court-list-request-export` | `ListingCommandApi.handleCourtListRequestExport()` | `ListingCommandHandler.courtListRequestExport()` | `listing.court-list-request-export` | +| 5 | `listing.command.create-listing-note` | `ListingCommandApi.handleCreateNote()` | `ListingNoteCommandHandler.handleCreateNote()` | `listing.create-listing-note` | +| 6 | `listing.command.delete-hearing` | `ListingCommandApi.handleDeleteHearing()` | `ListingCommandHandler.deleteHearing()` | `listing.delete-hearing` | +| 7 | `listing.command.publish-court-list` | `ListingCommandApi.handlePublishCourtList()` | `ListingCommandHandler.publishCourtList()` | `listing.publish-court-list` | diff --git a/docs/future-improvements/tilt-testcontainers-migration-analysis/01-technology-analysis.md b/docs/future-improvements/tilt-testcontainers-migration-analysis/01-technology-analysis.md new file mode 100644 index 000000000..08c5cba73 --- /dev/null +++ b/docs/future-improvements/tilt-testcontainers-migration-analysis/01-technology-analysis.md @@ -0,0 +1,231 @@ +# Technology Analysis: Tilt.dev & Testcontainers for cpp-context-listing + +## 1. Executive Summary + +This document analyses the suitability of **Tilt.dev** and **Testcontainers** as replacements for the current `cpp-developers-docker`-based integration test and local development workflow for the `cpp-context-listing` microservice. + +| Criterion | Tilt.dev | Testcontainers | +|-----------|----------|----------------| +| **Primary purpose** | Local development orchestration with live-reload | Programmatic test-scoped container lifecycle | +| **Removes cpp-developers-docker?** | Yes (fully) | Yes (for integration tests only) | +| **CI/CD suitability** | Moderate (requires K8s in CI) | Excellent (Docker-only, no K8s) | +| **Team adoption effort** | Medium-High | Low-Medium | +| **Best fit for listing** | Development workflow | Integration testing | + +--- + +## 2. Current State — What cpp-developers-docker Provides + +The `listing` context's `runIntegrationTests.sh` depends on cpp-developers-docker for: + +| Service | Purpose | Config | +|---------|---------|--------| +| **PostgreSQL 15** | 3 databases: `listingeventstore`, `listingviewstore`, `listingsystem` | User: `listing` / Pass: `listing` | +| **Apache Artemis 2.18** | JMS message broker for event topics (`listing.event`, `public.event`) | Port 61616 | +| **Wildfly 26.1.3** | Java EE application server for WAR deployment | Port 9080 (via HAProxy 8080) | +| **HAProxy 2.0** | Reverse proxy with URL rewriting | Port 8080 → Wildfly 9080 | +| **Liquibase Runner** | One-shot container for schema migrations | 6 separate migration sets | +| **Shell functions** | `buildWars`, `deployWars`, `healthchecks`, `integrationTests`, `runLiquibase` | Sourced from cpp-developers-docker scripts | + +### Current Execution Flow +``` +runIntegrationTests.sh + → loginToDockerContainerRegistry + → buildWars (mvn install) + → undeployWarsFromDocker (rm WARs from container) + → buildAndStartContainers (docker compose down -v → up -d) + → runLiquibase (6 migrations: eventstore, viewstore, system, event-buffer, event-tracking, aggregate-snapshot) + → deployWiremock + → deployWars (docker cp WAR into Wildfly container) + → healthchecks (poll /internal/metrics/ping + /internal/healthchecks/all) + → integrationTests (mvn verify -Plisting-integration-test) +``` + +--- + +## 3. Technology Analysis: Tilt.dev + +### 3.1 What Is Tilt.dev? + +Tilt.dev is a local Kubernetes development tool that watches source files, builds container images, and deploys them to a local K8s cluster (kind, minikube, Docker Desktop K8s). It provides: + +- **Live-reload**: File changes trigger automatic rebuild + redeploy +- **Dashboard**: Web UI showing service status, logs, and build progress +- **Multi-service orchestration**: Manage all dependent services from a single `Tiltfile` +- **Resource dependencies**: Define startup ordering between services + +### 3.2 Suitability for cpp-context-listing + +| Aspect | Assessment | Notes | +|--------|------------|-------| +| **PostgreSQL** | Excellent | K8s StatefulSet or Helm chart | +| **Artemis** | Good | Official Docker image, K8s Deployment | +| **Wildfly + WAR** | Good | Custom image with WAR, live-sync for class files | +| **HAProxy** | Good | K8s Service handles routing; HAProxy as optional Deployment | +| **Liquibase** | Good | K8s Job or init-container | +| **WireMock** | Excellent | Lightweight container, K8s Deployment | +| **6-database Liquibase** | Moderate | Needs orchestration for 6 separate migration sets | +| **Live development** | Excellent | Primary strength of Tilt | +| **CI/CD integration** | Moderate | Needs K8s in CI (kind + ctlptl) | + +### 3.3 Benefits for listing + +1. **Live-reload during development**: Change Java source → automatic WAR rebuild → auto-deploy to Wildfly → no manual restart cycle +2. **Self-contained**: No external repo dependency; everything declared in `Tiltfile` +3. **Dashboard visibility**: Real-time logs, build status, health for all services +4. **Production-like**: Runs on K8s, closer to actual AKS deployment topology +5. **Multi-context development**: Easy to add hearing, progression, etc. alongside listing + +### 3.4 Drawbacks for listing + +1. **K8s prerequisite**: Every developer needs kind/minikube + kubectl + ctlptl +2. **Resource-heavy**: K8s cluster + PostgreSQL + Artemis + Wildfly = significant RAM/CPU +3. **Learning curve**: Starlark scripting, K8s manifests, Helm chart knowledge +4. **Slower cold start**: K8s pod scheduling + image pulls vs. Docker Compose +5. **CI complexity**: Azure DevOps pipelines need kind cluster setup + +--- + +## 4. Technology Analysis: Testcontainers + +### 4.1 What Is Testcontainers? + +Testcontainers is a Java library that provides lightweight, throwaway instances of Docker containers for integration testing. Containers are managed programmatically from JUnit test code: + +- **JUnit 5 integration**: `@Testcontainers` + `@Container` annotations +- **Module library**: Pre-built modules for PostgreSQL, Kafka, etc. +- **GenericContainer**: Run any Docker image as a container +- **Network isolation**: Containers get their own Docker network per test suite +- **Automatic cleanup**: Containers destroyed when tests complete + +### 4.2 Suitability for cpp-context-listing + +| Aspect | Assessment | Notes | +|--------|------------|-------| +| **PostgreSQL** | Excellent | `PostgreSQLContainer` module with built-in Liquibase support | +| **Artemis** | Good | `GenericContainer` with official Artemis image | +| **Wildfly + WAR** | Moderate | `GenericContainer` + volume-mount WAR; or embedded Arquillian | +| **HAProxy** | Not needed | Direct container-to-container networking via `Network.newNetwork()` | +| **Liquibase** | Excellent | Run from test setup, or via JDBC container module | +| **WireMock** | Excellent | Dedicated WireMock container module available | +| **6-database Liquibase** | Good | Programmatic: create 3 databases on same PostgreSQL container, run migrations | +| **CI/CD integration** | Excellent | Only needs Docker daemon; works natively in Maven | +| **Test isolation** | Excellent | Fresh containers per test class or suite | + +### 4.3 Benefits for listing + +1. **Zero external dependencies**: No `cpp-developers-docker`, no `CPP_DOCKER_DIR`, no shell scripts +2. **Self-contained tests**: All infrastructure declared in Java test code alongside the tests +3. **CI/CD native**: Works anywhere Docker runs — Azure DevOps, GitHub Actions, local +4. **Test isolation**: Each test run gets fresh containers → no flaky shared state +5. **Dynamic port allocation**: No port conflicts; containers use random available ports +6. **Faster feedback**: Can start only the containers each test class needs +7. **Version-locked**: Container versions pinned in `pom.xml`, not in external repo + +### 4.4 Drawbacks for listing + +1. **Not a development tool**: Does not provide live-reload or development workflow +2. **Full-stack complexity**: Running Wildfly + WAR deployment inside Testcontainers requires custom container setup +3. **Test startup time**: Starting PostgreSQL + Artemis + Wildfly per test suite adds ~30-60s +4. **Resource consumption**: Docker containers during test execution (mitigated by reusable containers) +5. **Learning curve**: Developers need to understand Testcontainers API and container networking + +--- + +## 5. Key Questions Answered + +### 5.1 Will this remove the dependency on cpp-developers-docker? + +| Technology | Answer | +|-----------|--------| +| **Tilt.dev** | **Yes, fully.** All services (PostgreSQL, Artemis, Wildfly, HAProxy, Liquibase) are defined in the Tiltfile using K8s manifests. The `runIntegrationTests.sh` script and all sourced functions from cpp-developers-docker become unnecessary. | +| **Testcontainers** | **Yes, for integration tests.** All containers are declared in Java test code. The `runIntegrationTests.sh` script is replaced by `mvn verify -Plisting-integration-test`. However, Testcontainers does not replace the development workflow — developers still need some way to run the service locally for manual testing. | + +**Recommendation**: For complete removal of cpp-developers-docker, use **Testcontainers for tests** and a simple **docker-compose.yml within the listing repo** for local development. + +### 5.2 Is there anything cpp-developers-docker can do that these technologies cannot? + +| Capability | cpp-developers-docker | Tilt.dev | Testcontainers | +|-----------|----------------------|----------|----------------| +| Shared PostgreSQL for all 70+ contexts | Yes (init.sql creates all users/databases) | No (scoped to declared contexts) | No (test-scoped) | +| Multi-context WAR deployment in same Wildfly | Yes (deploy multiple WARs) | Yes (with configuration) | Not practical | +| Elasticsearch profile | Yes (`--profile es`) | Yes (K8s Deployment) | Yes (ElasticsearchContainer) | +| Docmosis profile | Yes (`--profile docmosis`) | Yes (K8s Deployment) | Yes (GenericContainer) | +| Alfresco stack | Yes (`--profile alfresco`) | Yes (complex, 6 containers) | Yes (complex setup) | +| HAProxy URL rewriting | Yes (haproxy.cfg) | K8s Ingress replaces this | Not needed (direct access) | +| Remote debug (JDWP 8787) | Yes | Yes (K8s port-forward) | N/A (test context) | +| Cross-context event flow testing | Yes (deploy multiple WARs, same Artemis) | Yes (multi-resource Tiltfile) | Possible but complex | + +**Gaps**: +- **Tilt.dev**: Cannot replicate the "shared environment for 70+ contexts" model, but this is actually a benefit — isolation prevents cross-context interference. +- **Testcontainers**: Not suited for manual/exploratory development workflows. Does not provide a persistent running environment. +- **Neither**: The existing `socat` bridge (Wildfly calling itself via HAProxy on localhost:8080) requires careful network configuration in both technologies. + +### 5.3 Is it going to be easy for team use? + +#### Tilt.dev — Team Adoption Assessment + +| Factor | Difficulty | Notes | +|--------|-----------|-------| +| **Prerequisites** | Medium | Install: Tilt CLI, kind/minikube, kubectl, ctlptl, Docker | +| **Daily workflow** | Easy | `tilt up` → dashboard → code → auto-reload | +| **Troubleshooting** | Medium | K8s knowledge needed for pod issues, networking | +| **Onboarding** | Medium | 1-2 hour setup + walkthrough needed | +| **Maintenance** | Medium | K8s manifests + Tiltfile need updating with infra changes | + +**Verdict**: Medium difficulty. Developers comfortable with Docker will need to additionally learn K8s concepts. The Tilt dashboard significantly lowers the barrier once set up. + +#### Testcontainers — Team Adoption Assessment + +| Factor | Difficulty | Notes | +|--------|-----------|-------| +| **Prerequisites** | Low | Only Docker Desktop + Maven (already required) | +| **Daily workflow** | Easy | `mvn verify -Plisting-integration-test` — no external setup | +| **Troubleshooting** | Low | Standard Java debugging; container logs accessible programmatically | +| **Onboarding** | Low | Familiar JUnit patterns; no new CLI tools | +| **Maintenance** | Low | Container versions in pom.xml; test setup in Java code | + +**Verdict**: Low difficulty. The Java-native API means no new tooling beyond what developers already use. Tests are self-documenting and version-controlled alongside the code. + +--- + +## 6. Recommendation + +### For Integration Testing: **Testcontainers** (Strong Recommendation) + +Testcontainers is the better fit for replacing `runIntegrationTests.sh` and the cpp-developers-docker dependency for test execution: + +- Completely self-contained within the listing repository +- Works identically on developer machines and CI/CD pipelines +- Lower adoption barrier for the Java development team +- Test isolation eliminates flaky test issues from shared state + +### For Local Development: **Docker Compose** (in-repo) or **Tilt.dev** (if K8s workflow desired) + +- A simple `docker-compose.yml` within the listing repo covers most local development needs +- Tilt.dev is worth investing in only if the team wants K8s-native development with live-reload across multiple contexts + +### Phased Approach + +| Phase | Action | Effort | +|-------|--------|--------| +| **Phase 1** | Adopt Testcontainers for integration tests | 2-3 weeks | +| **Phase 2** | Add in-repo `docker-compose.yml` for local dev | 1 week | +| **Phase 3** | Evaluate Tilt.dev for multi-context development | 2-4 weeks | + +--- + +## 7. Comparison Matrix + +| Criterion | cpp-developers-docker | Tilt.dev | Testcontainers | +|-----------|----------------------|----------|----------------| +| Setup complexity | High (clone repo, set env vars, learn scripts) | Medium (K8s + Tilt CLI) | Low (Maven dependency) | +| CI/CD readiness | Poor (needs Docker Compose in pipeline) | Moderate (needs kind in pipeline) | Excellent (Docker only) | +| Test isolation | Poor (shared containers across runs) | Good (K8s namespace isolation) | Excellent (fresh per test) | +| Cold start time | ~3-5 min (full compose up) | ~2-4 min (K8s pods) | ~1-2 min (per test suite) | +| Resource usage | High (all services always running) | High (K8s overhead) | Moderate (only needed containers) | +| Live development | Manual (rebuild + redeploy) | Excellent (auto-reload) | N/A | +| Version control | External repo (version drift risk) | In-repo (Tiltfile + manifests) | In-repo (pom.xml + Java code) | +| Cross-context | Built-in (70+ contexts) | Configurable (Tiltfile resources) | Complex (multiple container setups) | +| Debugging | Docker exec + logs | K8s port-forward + dashboard | Java IDE debugger + container logs | +| Team familiarity | Known (current) | New technology | Familiar (Java + JUnit) | diff --git a/docs/future-improvements/tilt-testcontainers-migration-analysis/02-tilt-migration-plan.md b/docs/future-improvements/tilt-testcontainers-migration-analysis/02-tilt-migration-plan.md new file mode 100644 index 000000000..9407f213c --- /dev/null +++ b/docs/future-improvements/tilt-testcontainers-migration-analysis/02-tilt-migration-plan.md @@ -0,0 +1,810 @@ +# Tilt.dev Migration Plan — cpp-context-listing + +## 1. Overview + +This document provides a precise implementation plan for migrating `cpp-context-listing` from `cpp-developers-docker` + `runIntegrationTests.sh` to a self-contained **Tilt.dev** local development and integration test environment. + +### Goal +Replace all `cpp-developers-docker` dependencies with a Tiltfile and Kubernetes manifests owned by the `cpp-context-listing` repository. + +### Prerequisites +- Docker Desktop (or equivalent container runtime) +- `kind` (Kubernetes in Docker) — local K8s cluster +- `kubectl` — Kubernetes CLI +- `ctlptl` — cluster lifecycle tool (recommended by Tilt) +- `tilt` CLI — v0.33+ (latest stable) +- `helm` — for chart-based deployments (optional, can use raw manifests) + +--- + +## 2. Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ kind K8s Cluster │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ +│ │PostgreSQL │ │ Artemis │ │ Wildfly 26 │ │ +│ │ 15 │ │ 2.18 │ │ listing-service │ │ +│ │ │ │ │ │ .war │ │ +│ │ 3 DBs: │ │ Topics: │ │ │ │ +│ │ eventstore│ │ listing │ │ Port: 8080 │ │ +│ │ viewstore │ │ .event │ │ Debug: 8787 │ │ +│ │ system │ │ public │ │ │ │ +│ │ │ │ .event │ │ │ │ +│ └──────────┘ └──────────┘ └───────────────────┘ │ +│ │ +│ ┌──────────┐ ┌──────────────────────────────────┐ │ +│ │ WireMock │ │ Liquibase Job (init) │ │ +│ │ stubs │ │ 6 migration sets on startup │ │ +│ └──────────┘ └──────────────────────────────────┘ │ +│ │ +│ Port-forwards: localhost:8080 → wildfly │ +│ localhost:5432 → postgres │ +│ localhost:8787 → wildfly debug │ +│ localhost:61616 → artemis │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## 3. File Structure to Create + +``` +cpp-context-listing/ +├── tilt/ # NEW: All Tilt configuration +│ ├── Tiltfile # Main Tilt orchestration file +│ ├── k8s/ # Kubernetes manifests +│ │ ├── postgres.yaml # PostgreSQL StatefulSet + Service +│ │ ├── postgres-init-configmap.yaml # DB init SQL (3 databases + user) +│ │ ├── artemis.yaml # Artemis Deployment + Service +│ │ ├── wildfly.yaml # Wildfly Deployment + Service +│ │ ├── wiremock.yaml # WireMock Deployment + Service +│ │ └── liquibase-job.yaml # Liquibase Job (runs once) +│ ├── docker/ # Custom Docker images for Tilt +│ │ ├── Dockerfile.wildfly # Wildfly with listing-service WAR +│ │ └── Dockerfile.liquibase # Liquibase runner with all JARs +│ ├── config/ # Service configuration +│ │ ├── standalone-local.xml # Wildfly standalone config for local dev +│ │ ├── wiremock-stubs/ # WireMock JSON stub mappings +│ │ │ ├── court-scheduler-stubs.json +│ │ │ ├── reference-data-stubs.json +│ │ │ ├── progression-stubs.json +│ │ │ ├── users-groups-stubs.json +│ │ │ └── prosecution-case-stubs.json +│ │ └── artemis/ +│ │ └── broker.xml # Artemis broker configuration +│ └── scripts/ +│ ├── setup-cluster.sh # One-time kind cluster creation +│ └── run-integration-tests.sh # Tilt-based integration test runner +``` + +--- + +## 4. Implementation Steps + +### Step 1: Install Prerequisites + +Create `tilt/scripts/setup-cluster.sh`: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +echo "=== Setting up Tilt.dev environment for cpp-context-listing ===" + +# Check prerequisites +command -v docker >/dev/null 2>&1 || { echo "Docker required. Install Docker Desktop."; exit 1; } +command -v kind >/dev/null 2>&1 || { echo "kind required. Install: brew install kind"; exit 1; } +command -v kubectl >/dev/null 2>&1 || { echo "kubectl required. Install: brew install kubectl"; exit 1; } +command -v tilt >/dev/null 2>&1 || { echo "tilt required. Install: curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash"; exit 1; } + +# Create kind cluster with port mappings +cat <- + -agentlib:jdwp=transport=dt_socket,address=*:8787,server=y,suspend=n + -Djboss.bind.address=0.0.0.0 + -Djboss.bind.address.management=0.0.0.0 + readinessProbe: + httpGet: + path: /listing-service/internal/metrics/ping + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + httpGet: + path: /listing-service/internal/metrics/ping + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 15 +--- +apiVersion: v1 +kind: Service +metadata: + name: wildfly +spec: + type: NodePort + selector: + app: wildfly + ports: + - name: http + port: 8080 + targetPort: 8080 + nodePort: 30080 + - name: debug + port: 8787 + targetPort: 8787 + nodePort: 30787 + - name: admin + port: 9990 + targetPort: 9990 +``` + +#### 2e. WireMock (`tilt/k8s/wiremock.yaml`) + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wiremock +spec: + replicas: 1 + selector: + matchLabels: + app: wiremock + template: + metadata: + labels: + app: wiremock + spec: + containers: + - name: wiremock + image: wiremock/wiremock:3.3.1 + ports: + - containerPort: 8080 + args: ["--verbose", "--global-response-templating"] + volumeMounts: + - name: stubs + mountPath: /home/wiremock/mappings + volumes: + - name: stubs + configMap: + name: wiremock-stubs +--- +apiVersion: v1 +kind: Service +metadata: + name: wiremock +spec: + selector: + app: wiremock + ports: + - port: 8090 + targetPort: 8080 +``` + +#### 2f. Liquibase Job (`tilt/k8s/liquibase-job.yaml`) + +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: liquibase-init +spec: + backoffLimit: 3 + template: + spec: + initContainers: + - name: wait-for-postgres + image: busybox:1.36 + command: ['sh', '-c', 'until nc -z postgres 5432; do echo waiting for postgres; sleep 2; done'] + containers: + - name: liquibase + image: listing-liquibase + env: + - name: DB_HOST + value: postgres + - name: DB_PORT + value: "5432" + - name: DB_USER + value: listing + - name: DB_PASSWORD + value: listing + restartPolicy: Never +``` + +### Step 3: Create Custom Docker Images + +#### 3a. Wildfly Image (`tilt/docker/Dockerfile.wildfly`) + +```dockerfile +FROM jboss/wildfly:26.1.3.Final-jdk17 + +ARG WILDFLY_HOME=/opt/jboss/wildfly + +# Copy Wildfly standalone configuration (datasources, JMS, etc.) +COPY config/standalone-local.xml ${WILDFLY_HOME}/standalone/configuration/standalone.xml + +# Copy listing-service WAR (built by Maven) +COPY target/listing-service-*.war ${WILDFLY_HOME}/standalone/deployments/listing-service.war + +# Enable debug mode and bind to all interfaces +ENV JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,address=*:8787,server=y,suspend=n" + +EXPOSE 8080 8787 9990 + +CMD ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0", "-bmanagement", "0.0.0.0"] +``` + +#### 3b. Liquibase Image (`tilt/docker/Dockerfile.liquibase`) + +```dockerfile +FROM eclipse-temurin:17-jdk + +WORKDIR /liquibase + +# Copy all Liquibase JARs (built/downloaded by Maven) +COPY target/liquibase-jars/*.jar /liquibase/ + +# Copy migration script +COPY tilt/scripts/run-liquibase-all.sh /liquibase/run.sh +RUN chmod +x /liquibase/run.sh + +ENTRYPOINT ["/liquibase/run.sh"] +``` + +### Step 4: Create Standalone Wildfly Configuration + +Create `tilt/config/standalone-local.xml` — a simplified Wildfly configuration with: + +- **Datasources** pointing to K8s PostgreSQL service: + - `java:/app/listing-service/DS.eventstore` → `jdbc:postgresql://postgres:5432/listingeventstore` + - `java:/app/listing-service/DS.viewstore` → `jdbc:postgresql://postgres:5432/listingviewstore` + - `java:/app/listing-service/DS.system` → `jdbc:postgresql://postgres:5432/listingsystem` +- **JMS** connection factory pointing to Artemis: `tcp://artemis:61616` +- **Topics**: `listing.event`, `public.event` +- **Logging**: Configured for local development (DEBUG level) + +> **Note**: Extract the relevant datasource and messaging subsystem sections from the existing `cpp-developers-docker/containers/wildfly/configuration/standalone.xml` and adapt the hostnames to K8s service names. + +### Step 5: Create the Tiltfile + +Create `tilt/Tiltfile`: + +```python +# Tiltfile for cpp-context-listing local development + +# ============================================================ +# Configuration +# ============================================================ +config.define_string('war-path', args=True) +cfg = config.parse() +war_path = cfg.get('war-path', '../listing-service/target') + +# ============================================================ +# Infrastructure: PostgreSQL +# ============================================================ +k8s_yaml('k8s/postgres-init-configmap.yaml') +k8s_yaml('k8s/postgres.yaml') +k8s_resource('postgres', + port_forwards=['5432:5432'], + labels=['infra'] +) + +# ============================================================ +# Infrastructure: Artemis +# ============================================================ +k8s_yaml('k8s/artemis.yaml') +k8s_resource('artemis', + port_forwards=['61616:61616', '8161:8161'], + labels=['infra'] +) + +# ============================================================ +# Infrastructure: WireMock +# ============================================================ +k8s_yaml('k8s/wiremock.yaml') +k8s_resource('wiremock', + port_forwards=['8090:8080'], + labels=['infra'] +) + +# ============================================================ +# Database Migrations: Liquibase Job +# ============================================================ +docker_build('listing-liquibase', + context='..', + dockerfile='tilt/docker/Dockerfile.liquibase' +) +k8s_yaml('k8s/liquibase-job.yaml') +k8s_resource('liquibase-init', + resource_deps=['postgres'], + labels=['setup'] +) + +# ============================================================ +# Application: Wildfly + listing-service +# ============================================================ +docker_build('listing-wildfly', + context='..', + dockerfile='tilt/docker/Dockerfile.wildfly', + live_update=[ + # Sync WAR file changes for hot reload + sync(war_path, '/opt/jboss/wildfly/standalone/deployments/'), + ] +) +k8s_yaml('k8s/wildfly.yaml') +k8s_resource('wildfly', + port_forwards=['8080:8080', '8787:8787', '9990:9990'], + resource_deps=['postgres', 'artemis', 'liquibase-init'], + labels=['app'] +) + +# ============================================================ +# Local Maven Build (trigger on source change) +# ============================================================ +local_resource('maven-build', + cmd='cd .. && mvn install -pl listing-service -am -DskipTests -q', + deps=[ + '../listing-command', + '../listing-domain', + '../listing-event', + '../listing-query', + '../listing-viewstore', + '../listing-service', + '../listing-common', + '../listing-healthchecks', + ], + labels=['build'], + resource_deps=[] +) +``` + +### Step 6: Create Integration Test Runner Script + +Create `tilt/scripts/run-integration-tests.sh`: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TILT_DIR="$(dirname "$SCRIPT_DIR")" +PROJECT_DIR="$(dirname "$TILT_DIR")" + +echo "=== Building listing-service WAR ===" +cd "$PROJECT_DIR" +mvn install -DskipTests -q + +echo "=== Starting Tilt infrastructure ===" +cd "$TILT_DIR" +tilt ci -- 2>&1 & +TILT_PID=$! + +# Wait for Wildfly healthcheck +echo "=== Waiting for listing-service to be healthy ===" +TIMEOUT=180 +ELAPSED=0 +until curl -sf http://localhost:8080/listing-service/internal/metrics/ping | grep -q pong; do + sleep 5 + ELAPSED=$((ELAPSED + 5)) + if [ $ELAPSED -ge $TIMEOUT ]; then + echo "ERROR: Timeout waiting for listing-service" + kill $TILT_PID 2>/dev/null + exit 1 + fi + echo " Waiting... ($ELAPSED/$TIMEOUT seconds)" +done +echo "=== listing-service is healthy ===" + +echo "=== Running integration tests ===" +cd "$PROJECT_DIR" +mvn -B verify -pl listing-integration-test \ + -Plisting-integration-test \ + -DINTEGRATION_HOST_KEY=localhost + +TEST_EXIT=$? + +echo "=== Tearing down Tilt ===" +kill $TILT_PID 2>/dev/null +tilt down --delete-namespaces 2>/dev/null + +exit $TEST_EXIT +``` + +### Step 7: Create Liquibase Runner Script + +Create `tilt/scripts/run-liquibase-all.sh`: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +DB_HOST="${DB_HOST:-postgres}" +DB_PORT="${DB_PORT:-5432}" +DB_USER="${DB_USER:-listing}" +DB_PASSWORD="${DB_PASSWORD:-listing}" + +run_migration() { + local jar=$1 + local db=$2 + echo "Running migration: $jar → $db" + java -jar "/liquibase/$jar" \ + --url="jdbc:postgresql://${DB_HOST}:${DB_PORT}/${db}" \ + --username="${DB_USER}" \ + --password="${DB_PASSWORD}" \ + --logLevel=info \ + update +} + +# Event Store migrations +run_migration "event-repository-liquibase.jar" "listingeventstore" +run_migration "aggregate-snapshot-repository-liquibase.jar" "listingeventstore" + +# View Store migrations +run_migration "event-buffer-liquibase.jar" "listingviewstore" +run_migration "event-tracking-liquibase.jar" "listingviewstore" +run_migration "listing-viewstore-liquibase.jar" "listingviewstore" + +# System migrations +run_migration "framework-system-liquibase.jar" "listingsystem" + +echo "=== All Liquibase migrations complete ===" +``` + +--- + +## 5. Changes to Existing Files + +### 5.1 Maven pom.xml Changes + +Add a Maven profile to download Liquibase JARs for the Tilt setup: + +**File**: `pom.xml` (root) + +```xml + + tilt-prepare + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-liquibase-jars + package + copy + + ${project.basedir}/target/liquibase-jars + + + uk.gov.justice.event-store + event-repository-liquibase + ${event-store.version} + + + uk.gov.justice.event-store + aggregate-snapshot-repository-liquibase + ${event-store.version} + + + uk.gov.justice.services + event-buffer-liquibase + ${framework.version} + + + uk.gov.justice.services + framework-system-liquibase + ${framework.version} + + + uk.gov.justice.event-store + event-tracking-liquibase + ${event-store.version} + + + + + + + + + +``` + +### 5.2 Integration Test Configuration + +**File**: `listing-integration-test/src/test/resources/endpoint.properties` + +No changes needed — tests already use `INTEGRATION_HOST_KEY=localhost` and port `8080`. + +### 5.3 runIntegrationTests.sh + +The existing `runIntegrationTests.sh` can remain for backward compatibility. Create the new Tilt-based script alongside it. Eventually, the old script can be deprecated. + +--- + +## 6. Migration Execution Order + +| Step | Action | Deliverable | Effort | +|------|--------|-------------|--------| +| 1 | Install Tilt, kind, kubectl on dev machines | Setup guide | 0.5 day | +| 2 | Create `tilt/k8s/` manifests (postgres, artemis, wiremock) | YAML files | 1 day | +| 3 | Extract and adapt `standalone-local.xml` from cpp-developers-docker | Wildfly config | 1 day | +| 4 | Create Dockerfiles for Wildfly and Liquibase | Docker images | 0.5 day | +| 5 | Create the Tiltfile with resource dependencies | Tiltfile | 1 day | +| 6 | Add `tilt-prepare` Maven profile for Liquibase JARs | pom.xml change | 0.5 day | +| 7 | Extract WireMock stubs to static JSON files | ConfigMap data | 1 day | +| 8 | Test `tilt up` → full service startup | Working environment | 1 day | +| 9 | Test integration tests against Tilt environment | Passing tests | 1 day | +| 10 | Create `run-integration-tests.sh` for CI | CI script | 0.5 day | +| 11 | Document setup for team | Developer guide | 0.5 day | +| **Total** | | | **~8 days** | + +--- + +## 7. CI/CD Integration (Azure DevOps) + +To run Tilt-based integration tests in Azure DevOps: + +```yaml +# azure-pipelines.yaml addition +- stage: IntegrationTest_Tilt + jobs: + - job: TiltIntegration + pool: + name: "MDV-ADO-AGENT-AKS-01" + demands: + - identifier -equals centos8-j17 + steps: + - script: | + # Install kind + curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64 + chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind + + # Install tilt + curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash + + # Create cluster + kind create cluster --name listing-it + + # Build and test + mvn install -DskipTests + cd tilt && tilt ci + displayName: 'Run Tilt Integration Tests' + - script: | + kind delete cluster --name listing-it + displayName: 'Cleanup' + condition: always() +``` + +--- + +## 8. Risks and Mitigations + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Kind cluster startup time (30-60s) | Slower test feedback | Use persistent cluster; only restart pods | +| Wildfly image size (~1.5GB) | Slow first build | Pre-cache in CI; use `live_update` for iterative builds | +| K8s learning curve | Team adoption friction | Provide setup scripts + documentation; Tilt dashboard helps | +| Port conflicts with cpp-developers-docker | Cannot run both | Use different NodePort ranges; document conflict resolution | +| Standalone.xml drift | Config mismatch with production | Extract from cpp-developers-docker and track changes | +| Liquibase JAR version coupling | Migrations may fail | Pin versions in Maven profile; test on version updates | + +--- + +## 9. Rollback Plan + +The migration is additive — the existing `runIntegrationTests.sh` remains functional. If Tilt adoption fails: + +1. Delete the `tilt/` directory +2. Remove the `tilt-prepare` Maven profile +3. Continue using `runIntegrationTests.sh` + `cpp-developers-docker` + +No existing functionality is modified or removed during the migration. diff --git a/docs/future-improvements/tilt-testcontainers-migration-analysis/03-testcontainers-migration-plan.md b/docs/future-improvements/tilt-testcontainers-migration-analysis/03-testcontainers-migration-plan.md new file mode 100644 index 000000000..dbf7b333a --- /dev/null +++ b/docs/future-improvements/tilt-testcontainers-migration-analysis/03-testcontainers-migration-plan.md @@ -0,0 +1,853 @@ +# Testcontainers Migration Plan — cpp-context-listing + +## 1. Overview + +This document provides a precise implementation plan for migrating `cpp-context-listing` integration tests from `cpp-developers-docker` + `runIntegrationTests.sh` to **Testcontainers** — a Java-native, self-contained approach where all infrastructure is managed programmatically from test code. + +### Goal +Eliminate all dependencies on `cpp-developers-docker`, external shell scripts, and pre-provisioned Docker environments. Integration tests should be runnable with a single Maven command: `mvn verify -Plisting-integration-test`. + +### Prerequisites +- Docker Desktop (or Podman with Testcontainers Cloud) +- Java 17 + Maven (already required) +- No other tooling needed + +--- + +## 2. Architecture + +``` +┌──────────────────────────────────────────────────────────────┐ +│ JUnit 5 Test Execution │ +│ │ +│ @Testcontainers │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Docker Network: listing-test-net │ │ +│ │ │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ │ +│ │ │PostgreSQL │ │ Artemis │ │ Wildfly 26 │ │ │ +│ │ │ 15 │ │ 2.18 │ │ listing-service │ │ │ +│ │ │ │ │ │ │ .war │ │ │ +│ │ │ 3 DBs: │ │ Topics: │ │ │ │ │ +│ │ │ eventstore│ │ listing │ │ Dynamic port │ │ │ +│ │ │ viewstore │ │ .event │ │ (mapped to host) │ │ │ +│ │ │ system │ │ public │ │ │ │ │ +│ │ │ │ │ .event │ │ │ │ │ +│ │ └──────────┘ └──────────┘ └───────────────────┘ │ │ +│ │ │ │ +│ │ ┌──────────┐ ┌──────────────────────────────────┐ │ │ +│ │ │ WireMock │ │ Liquibase (programmatic, │ │ │ +│ │ │ stubs │ │ runs in @BeforeAll) │ │ │ +│ │ └──────────┘ └──────────────────────────────────┘ │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ Tests use dynamic ports → no port conflicts │ +│ Containers destroyed after test suite → clean state │ +└──────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. File Structure — Changes Required + +``` +cpp-context-listing/ +├── listing-integration-test/ +│ ├── pom.xml # MODIFY: Add Testcontainers dependencies +│ └── src/test/java/uk/gov/moj/cpp/listing/ +│ ├── integration/ +│ │ ├── AbstractIT.java # MODIFY: Replace cpp-developers-docker setup +│ │ ├── ListCourtHearingIT.java # MODIFY: Use new base class +│ │ └── ...other IT classes... # MODIFY: Use new base class +│ └── containers/ # NEW: Container configuration package +│ ├── ListingTestEnvironment.java # NEW: Singleton container orchestration +│ ├── PostgresContainerConfig.java # NEW: PostgreSQL + Liquibase setup +│ ├── ArtemisContainerConfig.java # NEW: Artemis broker setup +│ ├── WildflyContainerConfig.java # NEW: Wildfly + WAR deployment +│ └── WiremockContainerConfig.java # NEW: WireMock stubs setup +``` + +--- + +## 4. Implementation Steps + +### Step 1: Add Maven Dependencies + +**File**: `listing-integration-test/pom.xml` + +Add to the `` section: + +```xml + + + + + org.testcontainers + testcontainers-bom + 1.19.7 + pom + import + + + + + + + org.testcontainers + testcontainers + test + + + org.testcontainers + postgresql + test + + + org.testcontainers + junit-jupiter + test + +``` + +> **Note**: If the parent POM (`service-parent-pom`) already manages a `testcontainers-bom`, use that version. Otherwise, add the BOM in the integration-test module's ``. + +### Step 2: Create ListingTestEnvironment (Singleton Container Orchestration) + +**File**: `listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/containers/ListingTestEnvironment.java` + +```java +package uk.gov.moj.cpp.listing.containers; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.lifecycle.Startables; + +/** + * Singleton test environment that starts all containers once for the entire + * test suite. Uses the "singleton container" pattern recommended by Testcontainers. + * + * Containers are shared across all IT classes and torn down when the JVM exits. + */ +public final class ListingTestEnvironment { + + private static final Network NETWORK = Network.newNetwork(); + + // PostgreSQL with 3 databases + private static final PostgreSQLContainer POSTGRES = PostgresContainerConfig.create(NETWORK); + + // Apache Artemis JMS broker + private static final GenericContainer ARTEMIS = ArtemisContainerConfig.create(NETWORK); + + // WireMock for external service stubs + private static final GenericContainer WIREMOCK = WiremockContainerConfig.create(NETWORK); + + // Wildfly with listing-service WAR + private static final GenericContainer WILDFLY = WildflyContainerConfig.create(NETWORK); + + private static boolean started = false; + + private ListingTestEnvironment() {} + + /** + * Starts all containers if not already running. + * Called from AbstractIT @BeforeAll. + */ + public static synchronized void start() { + if (started) return; + + // Start infrastructure containers in parallel + Startables.deepStart(POSTGRES, ARTEMIS, WIREMOCK).join(); + + // Run Liquibase migrations after PostgreSQL is ready + PostgresContainerConfig.runLiquibaseMigrations(POSTGRES); + + // Start Wildfly after all dependencies are ready + WILDFLY.start(); + + // Wait for deployment health check + waitForDeployment(); + + started = true; + } + + private static void waitForDeployment() { + // Wildfly readiness is handled by the container's wait strategy + // configured in WildflyContainerConfig + } + + public static String getWildflyBaseUrl() { + return String.format("http://%s:%d", + WILDFLY.getHost(), + WILDFLY.getMappedPort(8080)); + } + + public static String getPostgresJdbcUrl(String database) { + return String.format("jdbc:postgresql://%s:%d/%s", + POSTGRES.getHost(), + POSTGRES.getMappedPort(5432), + database); + } + + public static String getArtemisUrl() { + return String.format("tcp://%s:%d", + ARTEMIS.getHost(), + ARTEMIS.getMappedPort(61616)); + } + + public static String getWiremockUrl() { + return String.format("http://%s:%d", + WIREMOCK.getHost(), + WIREMOCK.getMappedPort(8080)); + } + + public static PostgreSQLContainer getPostgres() { return POSTGRES; } + public static GenericContainer getArtemis() { return ARTEMIS; } + public static GenericContainer getWildfly() { return WILDFLY; } + public static GenericContainer getWiremock() { return WIREMOCK; } +} +``` + +### Step 3: Create PostgreSQL Container Configuration + +**File**: `listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/containers/PostgresContainerConfig.java` + +```java +package uk.gov.moj.cpp.listing.containers; + +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +public final class PostgresContainerConfig { + + static final String DB_USER = "listing"; + static final String DB_PASSWORD = "listing"; + static final String EVENTSTORE_DB = "listingeventstore"; + static final String VIEWSTORE_DB = "listingviewstore"; + static final String SYSTEM_DB = "listingsystem"; + + private PostgresContainerConfig() {} + + public static PostgreSQLContainer create(final Network network) { + return new PostgreSQLContainer<>("postgres:15-alpine") + .withNetwork(network) + .withNetworkAliases("postgres") + .withDatabaseName("postgres") + .withUsername("postgres") + .withPassword("postgres") + .withInitScript("db/init.sql"); + } + + /** + * Runs all 6 Liquibase migration sets against the PostgreSQL container. + * This replaces the shell-based runLiquibase() function from cpp-developers-docker. + */ + public static void runLiquibaseMigrations(final PostgreSQLContainer postgres) { + final String host = postgres.getHost(); + final int port = postgres.getMappedPort(5432); + + // Event Store migrations (eventstore database) + runLiquibaseJar("event-repository-liquibase.jar", host, port, EVENTSTORE_DB); + runLiquibaseJar("aggregate-snapshot-repository-liquibase.jar", host, port, EVENTSTORE_DB); + + // View Store migrations (viewstore database) + runLiquibaseJar("event-buffer-liquibase.jar", host, port, VIEWSTORE_DB); + runLiquibaseJar("event-tracking-liquibase.jar", host, port, VIEWSTORE_DB); + + // Context-specific viewstore (uses Maven liquibase plugin) + runMavenLiquibase(host, port, VIEWSTORE_DB); + + // System migrations + runLiquibaseJar("framework-system-liquibase.jar", host, port, SYSTEM_DB); + } + + private static void runLiquibaseJar( + final String jarName, + final String host, + final int port, + final String database) { + try { + final String jdbcUrl = String.format( + "jdbc:postgresql://%s:%d/%s", host, port, database); + + ProcessBuilder pb = new ProcessBuilder( + "java", "-jar", + getJarPath(jarName), + "--url=" + jdbcUrl, + "--username=" + DB_USER, + "--password=" + DB_PASSWORD, + "--logLevel=info", + "update" + ); + pb.inheritIO(); + Process process = pb.start(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new RuntimeException( + "Liquibase migration failed for " + jarName + " (exit code: " + exitCode + ")"); + } + } catch (Exception e) { + throw new RuntimeException("Failed to run Liquibase: " + jarName, e); + } + } + + private static void runMavenLiquibase( + final String host, + final int port, + final String database) { + try { + final String jdbcUrl = String.format( + "jdbc:postgresql://%s:%d/%s", host, port, database); + + ProcessBuilder pb = new ProcessBuilder( + "mvn", "-f", + "listing-viewstore/listing-viewstore-liquibase/pom.xml", + "-Dliquibase.url=" + jdbcUrl, + "-Dliquibase.username=" + DB_USER, + "-Dliquibase.password=" + DB_PASSWORD, + "-Dliquibase.logLevel=info", + "resources:resources", "liquibase:update" + ); + pb.directory(new java.io.File(System.getProperty("user.dir")).getParentFile()); + pb.inheritIO(); + Process process = pb.start(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new RuntimeException( + "Viewstore Liquibase migration failed (exit code: " + exitCode + ")"); + } + } catch (Exception e) { + throw new RuntimeException("Failed to run viewstore Liquibase", e); + } + } + + private static String getJarPath(final String jarName) { + // JARs downloaded by Maven to target/liquibase-jars/ during build + return System.getProperty("user.dir") + "/../target/liquibase-jars/" + jarName; + } +} +``` + +**New test resource file**: `listing-integration-test/src/test/resources/db/init.sql` + +```sql +-- Create listing user +CREATE USER listing WITH PASSWORD 'listing'; + +-- Event Store database +CREATE DATABASE listingeventstore WITH OWNER=postgres; +GRANT ALL PRIVILEGES ON DATABASE listingeventstore TO listing; + +-- View Store database +CREATE DATABASE listingviewstore WITH OWNER=postgres; +GRANT ALL PRIVILEGES ON DATABASE listingviewstore TO listing; + +-- System database +CREATE DATABASE listingsystem WITH OWNER=postgres; +GRANT ALL PRIVILEGES ON DATABASE listingsystem TO listing; + +-- Grant schema permissions (requires connecting to each DB) +\c listingeventstore +GRANT ALL ON SCHEMA public TO listing; +\c listingviewstore +GRANT ALL ON SCHEMA public TO listing; +\c listingsystem +GRANT ALL ON SCHEMA public TO listing; +``` + +### Step 4: Create Artemis Container Configuration + +**File**: `listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/containers/ArtemisContainerConfig.java` + +```java +package uk.gov.moj.cpp.listing.containers; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; + +public final class ArtemisContainerConfig { + + private ArtemisContainerConfig() {} + + public static GenericContainer create(final Network network) { + return new GenericContainer<>("apache/activemq-artemis:2.18.0") + .withNetwork(network) + .withNetworkAliases("artemis") + .withExposedPorts(61616, 8161) + .withEnv("ARTEMIS_USER", "admin") + .withEnv("ARTEMIS_PASSWORD", "admin") + .withEnv("ANONYMOUS_LOGIN", "true") + .waitingFor(Wait.forListeningPort()); + } +} +``` + +### Step 5: Create Wildfly Container Configuration + +**File**: `listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/containers/WildflyContainerConfig.java` + +```java +package uk.gov.moj.cpp.listing.containers; + +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; + +import java.nio.file.Path; +import java.time.Duration; + +public final class WildflyContainerConfig { + + private static final String WILDFLY_HOME = "/opt/jboss/wildfly"; + private static final String DEPLOYMENTS = WILDFLY_HOME + "/standalone/deployments"; + + private WildflyContainerConfig() {} + + public static GenericContainer create(final Network network) { + // Find the WAR file from the Maven build output + final Path warPath = findWarFile(); + final Path standaloneXmlPath = Path.of( + System.getProperty("user.dir"), "src/test/resources/wildfly/standalone-testcontainers.xml"); + + return new GenericContainer<>("jboss/wildfly:26.1.3.Final-jdk17") + .withNetwork(network) + .withNetworkAliases("wildfly") + .withExposedPorts(8080, 8787, 9990) + .withFileSystemBind( + warPath.toString(), + DEPLOYMENTS + "/listing-service.war", + BindMode.READ_ONLY) + .withFileSystemBind( + standaloneXmlPath.toString(), + WILDFLY_HOME + "/standalone/configuration/standalone.xml", + BindMode.READ_ONLY) + .withEnv("JAVA_OPTS", String.join(" ", + "-agentlib:jdwp=transport=dt_socket,address=*:8787,server=y,suspend=n", + "-Djboss.bind.address=0.0.0.0", + "-Djboss.bind.address.management=0.0.0.0" + )) + .withCommand(WILDFLY_HOME + "/bin/standalone.sh", "-b", "0.0.0.0", "-bmanagement", "0.0.0.0") + .waitingFor(Wait.forHttp("/listing-service/internal/metrics/ping") + .forPort(8080) + .forResponsePredicate(response -> response.contains("pong")) + .withStartupTimeout(Duration.ofMinutes(3))); + } + + private static Path findWarFile() { + final Path serviceTarget = Path.of( + System.getProperty("user.dir"), "..", "listing-service", "target"); + try { + return java.nio.file.Files.list(serviceTarget) + .filter(p -> p.getFileName().toString().endsWith(".war")) + .filter(p -> !p.getFileName().toString().contains("original")) + .findFirst() + .orElseThrow(() -> new RuntimeException( + "listing-service WAR not found in " + serviceTarget + + ". Run 'mvn install -DskipTests' first.")); + } catch (Exception e) { + throw new RuntimeException("Cannot find WAR file", e); + } + } +} +``` + +### Step 6: Create WireMock Container Configuration + +**File**: `listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/containers/WiremockContainerConfig.java` + +```java +package uk.gov.moj.cpp.listing.containers; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; + +public final class WiremockContainerConfig { + + private WiremockContainerConfig() {} + + public static GenericContainer create(final Network network) { + return new GenericContainer<>("wiremock/wiremock:3.3.1") + .withNetwork(network) + .withNetworkAliases("wiremock") + .withExposedPorts(8080) + .withCommand("--verbose", "--global-response-templating") + .withClasspathResourceMapping( + "wiremock/mappings", + "/home/wiremock/mappings", + org.testcontainers.containers.BindMode.READ_ONLY) + .waitingFor(Wait.forHttp("/__admin/mappings") + .forStatusCode(200)); + } +} +``` + +### Step 7: Modify AbstractIT Base Class + +**File**: `listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/integration/AbstractIT.java` + +The existing `AbstractIT` extends `RestClient` and configures base URLs from `endpoint.properties`. The key changes: + +```java +package uk.gov.moj.cpp.listing.integration; + +import uk.gov.moj.cpp.listing.containers.ListingTestEnvironment; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; + +/** + * Base class for all integration tests. + * Replaces the cpp-developers-docker dependency with Testcontainers. + */ +public abstract class AbstractIT extends RestClient { + + @BeforeAll + static void startEnvironment() { + // Starts containers only once (singleton pattern) + ListingTestEnvironment.start(); + + // Override the base URL to use Testcontainers' dynamic port + System.setProperty("INTEGRATION_HOST_KEY", "localhost"); + System.setProperty("INTEGRATION_BASE_URL", + ListingTestEnvironment.getWildflyBaseUrl()); + } + + @BeforeEach + void cleanDatabases() { + // Existing database cleanup logic remains unchanged + // Uses JDBC to truncate viewstore tables before each test + cleanEventStoreTables(); + cleanViewStoreTables(); + } + + // Existing helper methods remain: + // - setupWiremockStubs() + // - cleanEventStoreTables() + // - cleanViewStoreTables() + // - getCppUidHeader() + + @Override + protected String getBaseUri() { + // Dynamic URL from Testcontainers instead of static endpoint.properties + return ListingTestEnvironment.getWildflyBaseUrl(); + } +} +``` + +### Step 8: Create Wildfly Standalone Configuration for Testcontainers + +**File**: `listing-integration-test/src/test/resources/wildfly/standalone-testcontainers.xml` + +This is a Wildfly `standalone.xml` with datasources and JMS configured to use Testcontainers network aliases: + +Key sections to configure: + +```xml + + + jdbc:postgresql://postgres:5432/listingeventstore + postgresql + + listing + listing + + + + + + jdbc:postgresql://postgres:5432/listingviewstore + postgresql + + listing + listing + + + + + + jdbc:postgresql://postgres:5432/listingsystem + postgresql + + listing + listing + + + + + + + + + + + + + +``` + +> **Important**: Extract the full `standalone.xml` from `cpp-developers-docker/containers/wildfly/configuration/standalone.xml` and replace hostname references: +> - `cpp-postgres` → `postgres` +> - `cpp-artemis` → `artemis` +> - `localhost:8080` (HAProxy) → `wildfly:8080` (direct) + +### Step 9: Add Liquibase JAR Download to Build + +**File**: `pom.xml` (root) — add to the existing `listing-integration-test` profile or create a new one: + +```xml + + testcontainers-prepare + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-liquibase-jars + generate-test-resources + copy + + ${project.basedir}/target/liquibase-jars + + + uk.gov.justice.event-store + event-repository-liquibase + ${event-store.version} + + + uk.gov.justice.event-store + aggregate-snapshot-repository-liquibase + ${event-store.version} + + + uk.gov.justice.services + event-buffer-liquibase + ${framework.version} + + + uk.gov.justice.services + framework-system-liquibase + ${framework.version} + + + uk.gov.justice.event-store + event-tracking-liquibase + ${event-store.version} + + + + + + + + + +``` + +--- + +## 5. Alternative Approach: Embedded Liquibase (Recommended Simplification) + +Instead of running Liquibase as external JARs via `ProcessBuilder`, a cleaner approach is to use Liquibase as a Java dependency and run migrations programmatically: + +```xml + + + org.liquibase + liquibase-core + 4.25.1 + test + +``` + +Then in `PostgresContainerConfig.java`: + +```java +public static void runLiquibaseMigrations(final PostgreSQLContainer postgres) { + // For framework JARs (event-repository, event-buffer, etc.): + // These are already on the classpath via Maven dependencies. + // Extract their Liquibase changelogs and run programmatically. + + try (Connection conn = DriverManager.getConnection( + postgres.getJdbcUrl().replace("/postgres", "/listingeventstore"), + DB_USER, DB_PASSWORD)) { + + Database database = DatabaseFactory.getInstance() + .findCorrectDatabaseImplementation(new JdbcConnection(conn)); + + Liquibase liquibase = new Liquibase( + "liquibase/event-store-changelog.xml", + new ClassLoaderResourceAccessor(), + database); + + liquibase.update(""); + } + + // Repeat for viewstore and system databases +} +``` + +> **Note**: This approach depends on the framework Liquibase JARs having accessible changelog resources on the classpath. If changelogs are packaged as self-executing JARs only, the `ProcessBuilder` approach in Step 3 is the fallback. + +--- + +## 6. Running Tests + +### Local Development + +```bash +# Step 1: Build the WAR (only needed once, or after code changes) +mvn install -DskipTests -pl listing-service -am + +# Step 2: Run integration tests (Testcontainers handles everything else) +mvn verify -pl listing-integration-test -Plisting-integration-test -DINTEGRATION_HOST_KEY=localhost +``` + +### CI/CD (Azure DevOps) + +```yaml +# azure-pipelines.yaml - Integration test stage +- stage: IntegrationTest_Testcontainers + jobs: + - job: TestcontainersIT + pool: + name: "MDV-ADO-AGENT-AKS-01" + demands: + - identifier -equals centos8-j17 + steps: + - script: | + # Docker must be available on the agent + docker info + + # Full build + mvn clean install -DskipTests + + # Integration tests — Testcontainers manages containers automatically + mvn verify -pl listing-integration-test \ + -Plisting-integration-test \ + -DINTEGRATION_HOST_KEY=localhost + displayName: 'Run Testcontainers Integration Tests' +``` + +No `CPP_DOCKER_DIR` environment variable, no `cpp-developers-docker` checkout, no separate Docker Compose setup. + +--- + +## 7. Migration Execution Order + +| Step | Action | Deliverable | Effort | +|------|--------|-------------|--------| +| 1 | Add Testcontainers dependencies to `listing-integration-test/pom.xml` | pom.xml changes | 0.5 day | +| 2 | Create `containers/` package with 5 configuration classes | Java source files | 2 days | +| 3 | Create `db/init.sql` test resource for PostgreSQL init | SQL file | 0.5 day | +| 4 | Extract and adapt `standalone-testcontainers.xml` from cpp-developers-docker | XML config | 1 day | +| 5 | Extract WireMock stubs to `src/test/resources/wiremock/mappings/` | JSON files | 1 day | +| 6 | Modify `AbstractIT` to use `ListingTestEnvironment` | Java changes | 1 day | +| 7 | Configure Liquibase JAR download or embedded execution | pom.xml + Java | 1 day | +| 8 | Test full integration test suite with Testcontainers | Passing tests | 2 days | +| 9 | Update CI/CD pipeline to remove cpp-developers-docker dependency | Pipeline YAML | 0.5 day | +| 10 | Document new approach for team | README update | 0.5 day | +| **Total** | | | **~10 days** | + +--- + +## 8. Handling Specific Listing Complexities + +### 8.1 Six Liquibase Migration Sets + +The listing context requires 6 separate Liquibase migrations against 3 databases. This is the most complex part: + +| Migration JAR | Target Database | Source | +|--------------|----------------|--------| +| `event-repository-liquibase` | `listingeventstore` | Framework (event-store) | +| `aggregate-snapshot-repository-liquibase` | `listingeventstore` | Framework (event-store) | +| `event-buffer-liquibase` | `listingviewstore` | Framework | +| `event-tracking-liquibase` | `listingviewstore` | Framework (event-store) | +| `listing-viewstore-liquibase` | `listingviewstore` | Context-specific | +| `framework-system-liquibase` | `listingsystem` | Framework | + +**Strategy**: Download JARs during `generate-test-resources` phase; execute in `@BeforeAll`. + +### 8.2 WireMock Stub Setup + +Current tests programmatically configure WireMock stubs in `AbstractIT.setupWiremockStubs()`. Two options: + +1. **Keep programmatic setup** (easiest): WireMock container starts empty; stubs configured via REST API in `@BeforeAll` +2. **Static JSON mappings** (cleaner): Export stubs to JSON files under `src/test/resources/wiremock/mappings/` + +Recommendation: **Option 1** — minimal changes to existing test code. + +### 8.3 JMS Integration + +Tests that use `JmsResourceManagementExtension` need the Artemis container URL. The extension currently uses JNDI to look up the connection factory from Wildfly. Since Wildfly connects to Artemis via its `standalone.xml` configuration, and the Testcontainers Artemis uses the network alias `artemis`, no test code changes are needed — only the `standalone-testcontainers.xml` needs correct Artemis hostname. + +### 8.4 Payload-Based Testing Framework + +The `PayloadGenerator` and payload-based test helpers load JSON from `test-data/` directories. These are classpath resources and are **unaffected** by the Testcontainers migration. + +--- + +## 9. Risks and Mitigations + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Wildfly startup time in container (30-90s) | Slower test suite | Singleton pattern ensures one startup per suite | +| Docker image pull time (first run) | Slow first execution | Use `testcontainers.reuse.enable=true` in `~/.testcontainers.properties` | +| Liquibase JAR compatibility | Migrations may fail | Pin versions matching `framework.version` and `event-store.version` | +| CI agent Docker availability | Tests won't run | Verify Docker daemon on CI agents; consider Testcontainers Cloud | +| Port conflicts | Container start failure | Dynamic port allocation (Testcontainers default) avoids conflicts | +| standalone.xml drift | Config mismatch | Track changes to cpp-developers-docker standalone.xml | +| Memory usage | Docker + JVM pressure | Configure Docker Desktop with 8GB+ RAM; tune Wildfly JVM args | + +--- + +## 10. Testcontainers Configuration for Faster Development + +Create `listing-integration-test/src/test/resources/testcontainers.properties`: + +```properties +# Enable reusable containers (containers persist between test runs) +# Avoids cold-start penalty during development +testcontainers.reuse.enable=true +``` + +And in each container configuration, add `.withReuse(true)`: + +```java +return new PostgreSQLContainer<>("postgres:15-alpine") + .withNetwork(network) + .withNetworkAliases("postgres") + .withReuse(true) // Keep container between test runs + // ... rest of config +``` + +> **Warning**: Reusable containers should NOT be used in CI/CD — only for local development. In CI, use fresh containers for test isolation. + +--- + +## 11. Rollback Plan + +The migration is additive: + +1. Existing `runIntegrationTests.sh` remains functional +2. Existing `AbstractIT` can be preserved alongside the new one +3. If Testcontainers adoption fails, revert the pom.xml changes and `containers/` package + +No existing infrastructure is modified or removed. diff --git a/docs/superpowers/plans/2026-04-05-crown-enrichment-reorder.md b/docs/superpowers/plans/2026-04-05-crown-enrichment-reorder.md new file mode 100644 index 000000000..bf94df26d --- /dev/null +++ b/docs/superpowers/plans/2026-04-05-crown-enrichment-reorder.md @@ -0,0 +1,770 @@ +# CROWN Enrichment Pipeline Reorder + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Reorder the CROWN enrichment pipeline so CourtSchedule enrichment runs FIRST, then HearingDays, then Duration — because the court scheduler is the source of truth for session dates, times, rooms, and judiciary. + +**Architecture:** The current pipeline is `HearingDays → Duration → CourtSchedule` for all jurisdictions. For CROWN only, we reverse this to `CourtSchedule → HearingDays → Duration`. The CourtSchedule step determines the case (no courtScheduleId → skip, multi-day → multiDaySearchAndBook, single-day → listHearingInCourtSessions) and returns enriched hearing data. HearingDays and Duration enrichment then derive from those results. MAGISTRATES remains unchanged. + +**Tech Stack:** Java 17, CDI (`@Inject`), JAX-RS `Response`, existing `HearingSlotsService`, `CourtScheduleEnrichmentService`, `HearingDaysEnrichmentService`, `HearingDurationEnrichmentService`. + +--- + +## Background: Current vs New Flow + +### Current CROWN pipeline (all 3 entry points): +``` +Input → HearingDaysEnrichment → DurationEnrichment → CourtScheduleEnrichment → Output +``` + +### New CROWN pipeline: +``` +Input → CourtScheduleEnrichment (first!) → HearingDaysEnrichment → DurationEnrichment → Output +``` + +### Three Cases (inside CourtScheduleEnrichment for CROWN): + +| Case | Condition | Action | Endpoint | +|------|-----------|--------|----------| +| 1 | No courtScheduleId in hearingDays/nonDefaultDays/bookedSlots | No court schedule enrichment; check weekCommencing, pass through | N/A | +| 2 | Has courtScheduleId + aggregated duration > 360 | Multi-day: call multiDaySearchAndBook with first courtScheduleId + total duration | `vnd.courtscheduler.multiday.searchandbook.hearing.slots+json` | +| 3 | Has courtScheduleId + aggregated duration ≤ 360 | Single-day: call listHearingInCourtSessions with courtScheduleId(s) | `vnd.courtscheduler.list.hearings-in-court-sessions+json` | + +### Duration calculation for the >360 / ≤360 decision: +1. If hearingDays exist → sum `durationMinutes` across all hearingDays +2. Else if nonDefaultDays exist → sum `duration` across all nonDefaultDays +3. Else → fall back to `estimatedMinutes` from the hearing + +### Three entry points that need the reorder: +1. `HearingEnrichmentOrchestrator.enrichListCourtHearing(List, JsonEnvelope)` — called by `listing.command.list-court-hearing` and `listing.list-next-hearings-v2` +2. `HearingEnrichmentOrchestrator.enrichUpdateHearingForListing(UpdateHearingForListing, JsonEnvelope)` — called by `listing.command.update-related-hearing` +3. `HearingEnrichmentOrchestrator.enrichUpdateHearingForListing(UpdateHearingForListing, JsonEnvelope, CourtCentreDetails)` — called by `listing.command.update-hearing-for-listing` and `listing.command.update-hearings-for-listing` + +--- + +## File Map + +| File | Action | Responsibility | +|------|--------|----------------| +| `listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestrator.java` | Modify | Reorder CROWN pipeline: CourtSchedule → HearingDays → Duration in all 3 methods | +| `listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentService.java` | Modify | Add new public method `enrichCrownCourtScheduleFirst(HearingListingNeeds)` that determines the case (1/2/3) and calls the right endpoint; reuse existing `multiDaySearchAndBook`, `listHearingSessionsAndExtractData`, `fetchCourtSchedulesByIds`, `sanityCheckAndEnrichCrown` | +| `listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorTest.java` | Modify | Update CROWN test cases to verify new call order | +| `listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentServiceTest.java` | Modify | Add tests for `enrichCrownCourtScheduleFirst` covering all 3 cases | + +--- + +## Task 1: Add duration-aggregation utility method to CourtScheduleEnrichmentService + +This method determines the total duration from the hearing for the >360 / ≤360 decision. It checks hearingDays first, then nonDefaultDays, then falls back to estimatedMinutes. + +**Files:** +- Modify: `listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentService.java` +- Test: `listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentServiceTest.java` + +- [ ] **Step 1: Write the failing tests for `calculateAggregatedDuration`** + +Add these tests to `CourtScheduleEnrichmentServiceTest.java`: + +```java +// ─── calculateAggregatedDuration ─────────────────────────────────── + +@Test +void shouldCalculateDurationFromHearingDays() { + HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withHearingDays(List.of( + HearingDay.hearingDay().withDurationMinutes(360).build(), + HearingDay.hearingDay().withDurationMinutes(360).build() + )) + .withEstimatedMinutes(100) + .build(); + + int result = CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing); + + assertThat(result, is(720)); +} + +@Test +void shouldCalculateDurationFromNonDefaultDaysWhenNoHearingDays() { + HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withNonDefaultDays(List.of( + NonDefaultDay.nonDefaultDay().withDuration(180).build(), + NonDefaultDay.nonDefaultDay().withDuration(180).build() + )) + .withEstimatedMinutes(100) + .build(); + + int result = CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing); + + assertThat(result, is(360)); +} + +@Test +void shouldFallbackToEstimatedMinutesWhenNoHearingDaysOrNonDefaultDays() { + HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withEstimatedMinutes(240) + .build(); + + int result = CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing); + + assertThat(result, is(240)); +} + +@Test +void shouldReturnZeroWhenNoDurationInfoAvailable() { + HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds().build(); + + int result = CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing); + + assertThat(result, is(0)); +} +``` + +Also add the same tests for `UpdateHearingForListing`: + +```java +@Test +void shouldCalculateAggregatedDurationForUpdateFromHearingDays() { + UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withHearingDays(List.of( + HearingDay.hearingDay().withDurationMinutes(360).build(), + HearingDay.hearingDay().withDurationMinutes(360).build() + )) + .build(); + + int result = CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing); + + assertThat(result, is(720)); +} + +@Test +void shouldCalculateAggregatedDurationForUpdateFromNonDefaultDays() { + UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withNonDefaultDays(List.of( + uk.gov.justice.listing.commands.NonDefaultDay.nonDefaultDay().withDuration(200).build() + )) + .build(); + + int result = CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing); + + assertThat(result, is(200)); +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=CourtScheduleEnrichmentServiceTest#shouldCalculateDurationFromHearingDays+shouldCalculateDurationFromNonDefaultDaysWhenNoHearingDays+shouldFallbackToEstimatedMinutesWhenNoHearingDaysOrNonDefaultDays+shouldReturnZeroWhenNoDurationInfoAvailable+shouldCalculateAggregatedDurationForUpdateFromHearingDays+shouldCalculateAggregatedDurationForUpdateFromNonDefaultDays -am` +Expected: FAIL — method does not exist yet + +- [ ] **Step 3: Implement `calculateAggregatedDuration` in `CourtScheduleEnrichmentService.java`** + +Add two static overloads — one for `HearingListingNeeds`, one for `UpdateHearingForListing`: + +```java +/** + * Calculates the total duration for the CROWN multi-day vs single-day decision. + * Priority: hearingDays durationMinutes → nonDefaultDays duration → estimatedMinutes → 0 + */ +static int calculateAggregatedDuration(final HearingListingNeeds hearing) { + if (isNotEmpty(hearing.getHearingDays())) { + return hearing.getHearingDays().stream() + .mapToInt(d -> d.getDurationMinutes() != null ? d.getDurationMinutes() : 0) + .sum(); + } + if (isNotEmpty(hearing.getNonDefaultDays())) { + return hearing.getNonDefaultDays().stream() + .mapToInt(d -> d.getDuration() != null ? d.getDuration() : 0) + .sum(); + } + return hearing.getEstimatedMinutes() != null ? hearing.getEstimatedMinutes() : 0; +} + +static int calculateAggregatedDuration(final UpdateHearingForListing hearing) { + if (isNotEmpty(hearing.getHearingDays())) { + return hearing.getHearingDays().stream() + .mapToInt(d -> d.getDurationMinutes() != null ? d.getDurationMinutes() : 0) + .sum(); + } + if (isNotEmpty(hearing.getNonDefaultDays())) { + return hearing.getNonDefaultDays().stream() + .mapToInt(d -> d.getDuration() != null ? d.getDuration() : 0) + .sum(); + } + return 0; +} +``` + +Note: `NonDefaultDay` in `HearingListingNeeds` uses `uk.gov.justice.core.courts.NonDefaultDay` which has `getDuration()`. `NonDefaultDay` in `UpdateHearingForListing` uses `uk.gov.justice.listing.commands.NonDefaultDay` which also has `getDuration()`. Verify the correct types when implementing. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=CourtScheduleEnrichmentServiceTest#shouldCalculateDurationFromHearingDays+shouldCalculateDurationFromNonDefaultDaysWhenNoHearingDays+shouldFallbackToEstimatedMinutesWhenNoHearingDaysOrNonDefaultDays+shouldReturnZeroWhenNoDurationInfoAvailable+shouldCalculateAggregatedDurationForUpdateFromHearingDays+shouldCalculateAggregatedDurationForUpdateFromNonDefaultDays -am` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentService.java +git add listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentServiceTest.java +git commit -m "Add calculateAggregatedDuration for CROWN multi-day decision" +``` + +--- + +## Task 2: Add `enrichCrownCourtScheduleFirst` method for `HearingListingNeeds` + +This is the new public entry point for CROWN court-schedule enrichment when called first in the pipeline. It determines which case applies and calls the appropriate endpoint. + +**Files:** +- Modify: `listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentService.java` +- Test: `listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentServiceTest.java` + +- [ ] **Step 1: Write the failing tests for Case 1 (no courtScheduleId)** + +```java +// ─── enrichCrownCourtScheduleFirst (HearingListingNeeds) ─────────── + +@Test +void enrichCrownCourtScheduleFirst_shouldReturnUnchanged_whenNoCourtScheduleIdAnywhere() { + // Case 1: No courtScheduleId in hearingDays, nonDefaultDays, or bookedSlots + HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withId(HEARING_ID) + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(List.of( + HearingDay.hearingDay() + .withHearingDate(LocalDate.of(2026, 4, 10)) + .withDurationMinutes(120) + .build() + )) + .build(); + + HearingListingNeeds result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + assertThat(result, is(hearing)); + verifyNoInteractions(hearingSlotsService); +} +``` + +- [ ] **Step 2: Write the failing test for Case 3 (single-day, duration ≤ 360)** + +```java +@Test +void enrichCrownCourtScheduleFirst_shouldCallListHearingInCourtSessions_whenSingleDay() { + // Case 3: Has courtScheduleId + total duration ≤ 360 + UUID courtScheduleId = UUID.randomUUID(); + HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withId(HEARING_ID) + .withJurisdictionType(JurisdictionType.CROWN) + .withEstimatedMinutes(120) + .withCourtCentre(CourtCentre.courtCentre().withId(COURT_CENTRE_ID).withRoomId(COURT_ROOM_ID).build()) + .withHearingDays(List.of( + HearingDay.hearingDay() + .withHearingDate(LocalDate.of(2026, 4, 10)) + .withCourtScheduleId(courtScheduleId) + .withCourtCentreId(COURT_CENTRE_ID) + .withCourtRoomId(COURT_ROOM_ID) + .withDurationMinutes(120) + .build() + )) + .build(); + + // Mock fetchCourtSchedulesByIds + CourtSchedule session = new CourtSchedule(); + session.setCourtScheduleId(courtScheduleId.toString()); + session.setSessionDate(LocalDate.of(2026, 4, 10)); + session.setCourtRoomId(COURT_ROOM_ID.toString()); + session.setCourtHouseId(COURT_CENTRE_ID.toString()); + session.setHearingStartTime("2026-04-10T10:00:00Z"); + session.setDraft(false); + + // Mock getCourtSchedulesById — returns sessions + Response fetchResponse = mock(Response.class); + // ... set up mock chain for fetchCourtSchedulesByIds returning `session` + // Mock listHearingInCourtSessions — returns enriched data + Response listResponse = mock(Response.class); + // ... set up mock chain for listHearingInCourtSessions + + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + // ... verify result has enriched hearing days +} +``` + +Note: The actual mock setup will follow the existing patterns in `CourtScheduleEnrichmentServiceTest.java` — look at `shouldEnrichCrownSingleDay_whenCourtScheduleIdPresent` and similar tests for reference. + +- [ ] **Step 3: Write the failing test for Case 2 (multi-day, duration > 360)** + +```java +@Test +void enrichCrownCourtScheduleFirst_shouldCallMultiDaySearchAndBook_whenMultiDay() { + // Case 2: Has courtScheduleId + total duration > 360 + UUID courtScheduleId = UUID.randomUUID(); + HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withId(HEARING_ID) + .withJurisdictionType(JurisdictionType.CROWN) + .withEstimatedMinutes(720) + .withCourtCentre(CourtCentre.courtCentre().withId(COURT_CENTRE_ID).withRoomId(COURT_ROOM_ID).build()) + .withHearingDays(List.of( + HearingDay.hearingDay() + .withHearingDate(LocalDate.of(2026, 4, 10)) + .withCourtScheduleId(courtScheduleId) + .withCourtCentreId(COURT_CENTRE_ID) + .withCourtRoomId(COURT_ROOM_ID) + .withDurationMinutes(720) + .build() + )) + .build(); + + // Mock multiDaySearchAndBook — returns 2 sessions + // ... follow existing pattern from shouldEnrichCrownMultiDay tests + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(multiDayResponse); + + HearingListingNeeds result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + verify(hearingSlotsService).multiDaySearchAndBook(anyMap()); + assertThat(result.getHearingDays().size(), is(2)); +} +``` + +- [ ] **Step 4: Run tests to verify they fail** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=CourtScheduleEnrichmentServiceTest#enrichCrownCourtScheduleFirst_shouldReturnUnchanged_whenNoCourtScheduleIdAnywhere+enrichCrownCourtScheduleFirst_shouldCallListHearingInCourtSessions_whenSingleDay+enrichCrownCourtScheduleFirst_shouldCallMultiDaySearchAndBook_whenMultiDay -am` +Expected: FAIL — method does not exist + +- [ ] **Step 5: Implement `enrichCrownCourtScheduleFirst` for `HearingListingNeeds`** + +Add to `CourtScheduleEnrichmentService.java`: + +```java +/** + * CROWN-first enrichment: determines case and calls appropriate court scheduler endpoint. + * Called BEFORE HearingDays and Duration enrichment for CROWN hearings. + * + * Case 1: No courtScheduleId anywhere → return unchanged (weekCommencing or no schedule data) + * Case 2: Has courtScheduleId + aggregated duration > 360 → multiDaySearchAndBook + * Case 3: Has courtScheduleId + aggregated duration ≤ 360 → listHearingInCourtSessions + */ +public HearingListingNeeds enrichCrownCourtScheduleFirst(final HearingListingNeeds hearing) { + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Starting for hearingId: {}", hearing.getId()); + + final boolean hasCourtScheduleId = hasAnyCourtScheduleId(hearing); + + if (!hasCourtScheduleId) { + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Case 1: No courtScheduleId found. Returning unchanged for hearingId: {}", hearing.getId()); + return hearing; + } + + final int aggregatedDuration = calculateAggregatedDuration(hearing); + final boolean isMultiDay = aggregatedDuration > HearingDurationEnrichmentService.MINUTES_IN_DAY; + + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] hearingId: {}, aggregatedDuration={}, isMultiDay={} (threshold={})", + hearing.getId(), aggregatedDuration, isMultiDay, HearingDurationEnrichmentService.MINUTES_IN_DAY); + + EnrichmentResult enrichmentResult; + if (isMultiDay) { + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Case 2: Multi-day → multiDaySearchAndBook for hearingId: {}", hearing.getId()); + enrichmentResult = handleCrownMultiDayEnrichment(hearing); + } else { + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Case 3: Single-day → listHearingInCourtSessions for hearingId: {}", hearing.getId()); + enrichmentResult = handleCrownSingleDayEnrichment(hearing); + } + + final List enrichedHearingDays = enrichmentResult.getHearingDays(); + final List enrichedJudiciaries = enrichmentResult.getJudiciaries(); + + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Result: enrichedHearingDays={}, judiciaries={} for hearingId: {}", + enrichedHearingDays.size(), enrichedJudiciaries.size(), hearing.getId()); + + HearingListingNeeds.Builder hearingBuilder = HearingListingNeeds.hearingListingNeeds() + .withValuesFrom(hearing) + .withHearingDays(enrichedHearingDays); + + if (isNotEmpty(enrichedJudiciaries)) { + hearingBuilder.withJudiciary(convertJudicialRoleDomainToCore(enrichedJudiciaries)); + } + + // Adjust court centre if scheduler returned a different room + if (isNotEmpty(enrichedHearingDays) && nonNull(hearing.getCourtCentre())) { + final CourtCentre adjustedCourtCentre = CourtCentre.courtCentre() + .withValuesFrom(hearing.getCourtCentre()) + .withRoomId(enrichedHearingDays.get(0).getCourtRoomId()) + .build(); + hearingBuilder.withCourtCentre(adjustedCourtCentre); + } + + return hearingBuilder.build(); +} + +/** + * Checks if any structure (hearingDays, nonDefaultDays, bookedSlots) contains a courtScheduleId. + */ +private static boolean hasAnyCourtScheduleId(final HearingListingNeeds hearing) { + if (isNotEmpty(hearing.getHearingDays()) + && hearing.getHearingDays().stream().anyMatch(d -> nonNull(d.getCourtScheduleId()))) { + return true; + } + // bookedSlots have courtScheduleId as String + if (isNotEmpty(hearing.getBookedSlots()) + && hearing.getBookedSlots().stream().anyMatch(s -> !isBlank(s.getCourtScheduleId()))) { + return true; + } + return false; +} +``` + +Note: This method reuses the existing `handleCrownMultiDayEnrichment` and `handleCrownSingleDayEnrichment` which already implement the multi-day and single-day logic. The key difference is the entry point and the duration calculation. + +- [ ] **Step 6: Run tests to verify they pass** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=CourtScheduleEnrichmentServiceTest#enrichCrownCourtScheduleFirst_shouldReturnUnchanged_whenNoCourtScheduleIdAnywhere+enrichCrownCourtScheduleFirst_shouldCallListHearingInCourtSessions_whenSingleDay+enrichCrownCourtScheduleFirst_shouldCallMultiDaySearchAndBook_whenMultiDay -am` +Expected: PASS + +- [ ] **Step 7: Commit** + +```bash +git add listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentService.java +git add listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentServiceTest.java +git commit -m "Add enrichCrownCourtScheduleFirst for HearingListingNeeds" +``` + +--- + +## Task 3: Add `enrichCrownCourtScheduleFirst` method for `UpdateHearingForListing` + +Same logic but for the update path. The existing `enrichCrownUpdateHearing` already handles the CROWN update flow — we need a parallel entry point that can be called FIRST. + +**Files:** +- Modify: `listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentService.java` +- Test: `listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentServiceTest.java` + +- [ ] **Step 1: Write the failing tests** + +Write 3 tests mirroring Task 2 but using `UpdateHearingForListing`: + +```java +@Test +void enrichCrownCourtScheduleFirst_update_shouldReturnUnchanged_whenNoCourtScheduleId() { + UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withHearingId(HEARING_ID) + .withJurisdictionType(JurisdictionType.CROWN) + .withStartDate(LocalDate.of(2026, 4, 10)) + .withEndDate(LocalDate.of(2026, 4, 10)) + .withHearingDays(List.of( + HearingDay.hearingDay() + .withHearingDate(LocalDate.of(2026, 4, 10)) + .withDurationMinutes(120) + .build() + )) + .build(); + + UpdateHearingForListing result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + assertThat(result, is(hearing)); + verifyNoInteractions(hearingSlotsService); +} + +@Test +void enrichCrownCourtScheduleFirst_update_shouldCallListHearingInCourtSessions_whenSingleDay() { + // Case 3 for UpdateHearingForListing + // ... mock setup similar to existing enrichCrownUpdateHearing tests +} + +@Test +void enrichCrownCourtScheduleFirst_update_shouldCallMultiDaySearchAndBook_whenMultiDay() { + // Case 2 for UpdateHearingForListing + // ... mock setup similar to existing enrichCrownUpdateHearing multi-day tests +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=CourtScheduleEnrichmentServiceTest#enrichCrownCourtScheduleFirst_update_shouldReturnUnchanged_whenNoCourtScheduleId+enrichCrownCourtScheduleFirst_update_shouldCallListHearingInCourtSessions_whenSingleDay+enrichCrownCourtScheduleFirst_update_shouldCallMultiDaySearchAndBook_whenMultiDay -am` +Expected: FAIL + +- [ ] **Step 3: Implement `enrichCrownCourtScheduleFirst` for `UpdateHearingForListing`** + +```java +/** + * CROWN-first enrichment for update path. + * Same logic as the HearingListingNeeds overload but operates on UpdateHearingForListing. + */ +public UpdateHearingForListing enrichCrownCourtScheduleFirst(final UpdateHearingForListing hearing) { + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Update path starting for hearingId: {}", hearing.getHearingId()); + + final boolean hasCourtScheduleId = hasAnyCourtScheduleIdOnUpdate(hearing); + + if (!hasCourtScheduleId) { + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Update Case 1: No courtScheduleId. Returning unchanged for hearingId: {}", hearing.getHearingId()); + return hearing; + } + + final int aggregatedDuration = calculateAggregatedDuration(hearing); + final boolean isMultiDay = aggregatedDuration > HearingDurationEnrichmentService.MINUTES_IN_DAY; + + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Update hearingId: {}, aggregatedDuration={}, isMultiDay={}", + hearing.getHearingId(), aggregatedDuration, isMultiDay); + + // Delegate to existing enrichCrownUpdateHearing which already handles the 3 cases + // The existing method checks for courtScheduleId presence internally + return enrichCrownUpdateHearing(hearing); +} + +private static boolean hasAnyCourtScheduleIdOnUpdate(final UpdateHearingForListing hearing) { + return isNotEmpty(hearing.getHearingDays()) + && hearing.getHearingDays().stream().anyMatch(d -> nonNull(d.getCourtScheduleId())); +} +``` + +Important: The existing `enrichCrownUpdateHearing` already implements the multi-day vs single-day decision and the no-courtScheduleId fallback. Review whether we can reuse it directly or need to extract the duration-based decision into it. The key difference is the duration calculation — `enrichCrownUpdateHearing` currently uses `hearing.getHearingDays().stream().mapToInt(...)` inline, which aligns with our `calculateAggregatedDuration`. Verify this during implementation. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: same test command as Step 2 +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentService.java +git add listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentServiceTest.java +git commit -m "Add enrichCrownCourtScheduleFirst for UpdateHearingForListing" +``` + +--- + +## Task 4: Reorder CROWN pipeline in `enrichListCourtHearing` + +Change the CROWN branch from `HearingDays → Duration → CourtSchedule` to `CourtSchedule → HearingDays → Duration`. + +**Files:** +- Modify: `listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestrator.java` +- Test: `listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorTest.java` + +- [ ] **Step 1: Write/update the failing test for new CROWN order in `enrichListCourtHearing`** + +Update or add a test in `HearingEnrichmentOrchestratorTest.java` that verifies the call order. Use `InOrder` from Mockito: + +```java +@Test +void enrichListCourtHearing_crown_shouldCallCourtScheduleFirst_thenHearingDays_thenDuration() { + HearingListingNeeds crownHearing = HearingListingNeeds.hearingListingNeeds() + .withId(HEARING_ID) + .withJurisdictionType(JurisdictionType.CROWN) + // ... set up with courtScheduleId on hearingDays + .build(); + + when(courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(any(HearingListingNeeds.class))) + .thenReturn(crownHearing); + when(hearingDaysEnrichmentService.enrichHearings(any(HearingListingNeeds.class), any())) + .thenReturn(crownHearing); + when(hearingDurationEnrichmentService.enrichWithDurations(any(HearingListingNeeds.class), any())) + .thenReturn(crownHearing); + + hearingEnrichmentOrchestrator.enrichListCourtHearing(List.of(crownHearing), envelope); + + InOrder inOrder = inOrder(courtScheduleEnrichmentService, hearingDaysEnrichmentService, hearingDurationEnrichmentService); + inOrder.verify(courtScheduleEnrichmentService).enrichCrownCourtScheduleFirst(any(HearingListingNeeds.class)); + inOrder.verify(hearingDaysEnrichmentService).enrichHearings(any(HearingListingNeeds.class), any()); + inOrder.verify(hearingDurationEnrichmentService).enrichWithDurations(any(HearingListingNeeds.class), any()); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=HearingEnrichmentOrchestratorTest#enrichListCourtHearing_crown_shouldCallCourtScheduleFirst_thenHearingDays_thenDuration -am` +Expected: FAIL — still calling in old order + +- [ ] **Step 3: Reorder the CROWN branch in `enrichListCourtHearing`** + +In `HearingEnrichmentOrchestrator.java`, change the CROWN branch from: + +```java +// OLD: HearingDays -> Duration -> CourtSchedule +HearingListingNeeds withHearingDays = hearingDaysEnrichmentService.enrichHearings(hearing, envelope); +HearingListingNeeds withDurations = hearingDurationEnrichmentService.enrichWithDurations(withHearingDays, envelope); +HearingListingNeeds withCourtSchedules = courtScheduleEnrichmentService.enrichWithCourtSchedules(withDurations, envelope); +enrichedHearings.add(withCourtSchedules); +``` + +To: + +```java +// NEW: CourtSchedule (first!) -> HearingDays -> Duration +LOGGER.info("[CROWN-ENRICH] Step 1: CourtSchedule enrichment STARTED for hearingId: {}", hearing.getId()); +HearingListingNeeds withCourtSchedules = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); +LOGGER.info("[CROWN-ENRICH] Step 1: CourtSchedule enrichment COMPLETED for hearingId: {}", hearing.getId()); + +LOGGER.info("[CROWN-ENRICH] Step 2: HearingDays enrichment STARTED for hearingId: {}", hearing.getId()); +HearingListingNeeds withHearingDays = hearingDaysEnrichmentService.enrichHearings(withCourtSchedules, envelope); +LOGGER.info("[CROWN-ENRICH] Step 2: HearingDays enrichment COMPLETED for hearingId: {}", hearing.getId()); + +LOGGER.info("[CROWN-ENRICH] Step 3: Duration enrichment STARTED for hearingId: {}", hearing.getId()); +HearingListingNeeds withDurations = hearingDurationEnrichmentService.enrichWithDurations(withHearingDays, envelope); +LOGGER.info("[CROWN-ENRICH] Step 3: Duration enrichment COMPLETED for hearingId: {}", hearing.getId()); + +enrichedHearings.add(withDurations); +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=HearingEnrichmentOrchestratorTest#enrichListCourtHearing_crown_shouldCallCourtScheduleFirst_thenHearingDays_thenDuration -am` +Expected: PASS + +- [ ] **Step 5: Run ALL orchestrator tests to check for regressions** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=HearingEnrichmentOrchestratorTest -am` +Expected: PASS (existing MAGS tests should be unaffected) + +- [ ] **Step 6: Commit** + +```bash +git add listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestrator.java +git add listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorTest.java +git commit -m "Reorder CROWN enrichListCourtHearing: CourtSchedule first" +``` + +--- + +## Task 5: Reorder CROWN pipeline in both `enrichUpdateHearingForListing` overloads + +Same reorder for the update path. + +**Files:** +- Modify: `listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestrator.java` +- Test: `listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorTest.java` + +- [ ] **Step 1: Write/update failing tests for new CROWN order in both `enrichUpdateHearingForListing` overloads** + +Add two tests — one for the 2-param overload (without `CourtCentreDetails`), one for the 3-param overload: + +```java +@Test +void enrichUpdateHearingForListing_crown_shouldCallCourtScheduleFirst() { + // 2-param overload + UpdateHearingForListing crownHearing = UpdateHearingForListing.updateHearingForListing() + .withHearingId(HEARING_ID) + .withJurisdictionType(JurisdictionType.CROWN) + .withStartDate(LocalDate.of(2026, 4, 10)) + .withEndDate(LocalDate.of(2026, 4, 10)) + .build(); + + when(courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(any(UpdateHearingForListing.class))) + .thenReturn(crownHearing); + when(hearingDaysEnrichmentService.enrichHearing(any(UpdateHearingForListing.class), any())) + .thenReturn(crownHearing); + when(hearingDurationEnrichmentService.enrichWithDurationForUpdate(any(UpdateHearingForListing.class), any())) + .thenReturn(crownHearing); + + hearingEnrichmentOrchestrator.enrichUpdateHearingForListing(crownHearing, envelope); + + InOrder inOrder = inOrder(courtScheduleEnrichmentService, hearingDaysEnrichmentService, hearingDurationEnrichmentService); + inOrder.verify(courtScheduleEnrichmentService).enrichCrownCourtScheduleFirst(any(UpdateHearingForListing.class)); + inOrder.verify(hearingDaysEnrichmentService).enrichHearing(any(UpdateHearingForListing.class), any()); + inOrder.verify(hearingDurationEnrichmentService).enrichWithDurationForUpdate(any(UpdateHearingForListing.class), any()); +} + +@Test +void enrichUpdateHearingForListing_crown_withCourtCentreDetails_shouldCallCourtScheduleFirst() { + // 3-param overload + // Same test structure but calls the 3-param overload + // ... +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=HearingEnrichmentOrchestratorTest#enrichUpdateHearingForListing_crown_shouldCallCourtScheduleFirst+enrichUpdateHearingForListing_crown_withCourtCentreDetails_shouldCallCourtScheduleFirst -am` +Expected: FAIL + +- [ ] **Step 3: Reorder CROWN branch in `enrichUpdateHearingForListing(hearing, envelope)` (2-param)** + +Change from: +```java +UpdateHearingForListing withHearingDays = hearingDaysEnrichmentService.enrichHearing(hearing, envelope); +UpdateHearingForListing withDuration = hearingDurationEnrichmentService.enrichWithDurationForUpdate(withHearingDays, envelope); +enrichedHearing = courtScheduleEnrichmentService.enrichWithCourtSchedules(withDuration, envelope); +``` + +To: +```java +// CROWN: CourtSchedule (first!) -> HearingDays -> Duration +UpdateHearingForListing withCourtSchedules = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); +UpdateHearingForListing withHearingDays = hearingDaysEnrichmentService.enrichHearing(withCourtSchedules, envelope); +enrichedHearing = hearingDurationEnrichmentService.enrichWithDurationForUpdate(withHearingDays, envelope); +``` + +- [ ] **Step 4: Reorder CROWN branch in `enrichUpdateHearingForListing(hearing, envelope, courtCentreDetails)` (3-param)** + +Same change: +```java +// CROWN: CourtSchedule (first!) -> HearingDays -> Duration +UpdateHearingForListing withCourtSchedules = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); +UpdateHearingForListing withHearingDays = hearingDaysEnrichmentService.enrichHearing(withCourtSchedules, envelope, courtCentreDetails); +enrichedHearing = hearingDurationEnrichmentService.enrichWithDurationForUpdate(withHearingDays, envelope); +``` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=HearingEnrichmentOrchestratorTest -am` +Expected: ALL PASS + +- [ ] **Step 6: Commit** + +```bash +git add listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestrator.java +git add listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorTest.java +git commit -m "Reorder CROWN enrichUpdateHearingForListing: CourtSchedule first" +``` + +--- + +## Task 6: Full build + integration test verification + +**Files:** None changed — verification only. + +- [ ] **Step 1: Run full module build** + +Run: `mvn clean install -pl listing-command/listing-command-api -am` +Expected: BUILD SUCCESS + +- [ ] **Step 2: Run all CourtScheduleEnrichmentService tests** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=CourtScheduleEnrichmentServiceTest -am` +Expected: ALL PASS + +- [ ] **Step 3: Run all HearingEnrichmentOrchestrator tests** + +Run: `mvn test -pl listing-command/listing-command-api -Dtest=HearingEnrichmentOrchestratorTest -am` +Expected: ALL PASS + +- [ ] **Step 4: Run full project build** + +Run: `mvn clean install -DskipTests && mvn test -pl listing-command/listing-command-api -am` +Expected: BUILD SUCCESS, ALL TESTS PASS + +- [ ] **Step 5: Run integration tests** + +Run: `./runIntegrationTests.sh` +Expected: ALL PASS + +- [ ] **Step 6: Final commit if any test fixes were needed** + +If any tests needed fixing during this task, commit those fixes. + +--- + +## Notes for the Implementer + +### What NOT to change +- **MAGISTRATES paths** — the original `HearingDays → Duration → CourtSchedule` order remains for MAGS in all methods. +- **The existing `enrichWithCourtSchedules(HearingListingNeeds, JsonEnvelope)`** method — keep it, as it's still used by MAGS. The CROWN path in `checkAndUpdateListingCourtScheduler` can remain for now; it won't be called by the orchestrator for CROWN anymore, but other code may reference it. +- **`checkAndUpdateListingCourtScheduler`** — keep the existing logic intact. The new `enrichCrownCourtScheduleFirst` methods are additive. + +### Key reuse +- `handleCrownSingleDayEnrichment(HearingListingNeeds)` — already implements single-day logic with `fetchCourtSchedulesByIds` + `sanityCheckAndEnrichCrown` + `listHearingSessionsAndExtractData` +- `handleCrownMultiDayEnrichment(HearingListingNeeds)` — already implements multi-day logic with `multiDaySearchAndBook` + `buildHearingDaysFromMultiDaySessions` + `listHearingSessionsAndExtractData` +- `enrichCrownUpdateHearing(UpdateHearingForListing)` — already implements the update path with all 3 cases + +### Duration threshold +- `HearingDurationEnrichmentService.MINUTES_IN_DAY = 360` (6 working hours) is the boundary between single-day and multi-day. + +### Log prefix convention +- All CROWN enrichment logs use `[CROWN-ENRICH]` with sub-tags: `[CourtSchedule-First]`, `[HearingDays]`, `[Duration]`. diff --git a/docs/temporary/SPRDT-638-requirements.md b/docs/temporary/SPRDT-638-requirements.md new file mode 100644 index 000000000..eddd40be5 --- /dev/null +++ b/docs/temporary/SPRDT-638-requirements.md @@ -0,0 +1,306 @@ +# SPRDT-638: Link Crown Court Scheduler to Listing + +> **Epic**: [SPRDT-638](https://tools.hmcts.net/jira/browse/SPRDT-638) +> **Status**: Analysis | **Priority**: Medium | **Assignee**: Joan Porter +> **Created**: 31 Oct 2025 | **Last Updated**: 18 Feb 2026 + +--- + +## Epic Overview + +This feature links the Crown Court scheduler to Listing so that all hearings listed in the Crown Court can be **booked against specific sessions** in the schedule. This enables the slot counter to calculate the level of hearings booked and time remaining at session level. + +### Current Problem + +- Crown Court hearing details are entered with **no reference to a schedule** +- While CP can calculate the number/duration of allocated hearings at courtroom level by date, **unallocated hearings are not accounted for** +- There is **no way** for the system to monitor or control the volume of hearings listed using parameters set in the scheduler + +### Phased Rollout + +| Phase | Description | +|-------|-------------| +| **Phase 1** | Create/View/Edit functionality for Crown Court sessions. | +| **Phase 2** | Listing hearings against Crown Court sessions and migration of current Crown Court hearings. **Potential blocker**: removal of Allocated UI. | +| **Phase 3** | Assigning judiciary to Crown Court sessions and week commencing solution. | + +### Affected Listing Paths + +The following paths currently point to "Enter hearing details" and must be redirected to the new "Find a hearing" flow: + +- All NHCC (Next Hearing in a Crown Court) results, including "List from Box" work result +- Applications +- Manual case create +- Create ad hoc hearing +- Allocation journeys from the Unallocated and Unscheduled lists +- Allocate and Reallocate pages in Court Calendar + +### Design References + +- **Figma**: https://www.figma.com/design/et3TkPsUQXUitpkmF3JK2I/CCT-1984--Common-Platform--Court-schedule?node-id=3136-31867 +- **Attachment**: "New Find a hearing screens.pptm" (attached to epic) + +--- + +## Stories + +### Common "Find a Hearing" Filter Specification + +All stories below share a common filter screen and search results pattern. The variations per journey are called out in each story. The common elements are: + +#### Filter Fields + +| Field | Type | Default | Notes | +|-------|------|---------|-------| +| **Operational unit** | Dropdown (optional) | — | Narrows by unit | +| **Court** | Dropdown | Pre-populated from previous selection | Shows selected courthouse | +| **Courtroom** | Dropdown (optional) | Varies by journey (see stories) | Options: "No courtroom selected" / "All" / "Courtroom [n]" | +| **Booking type** | Radio buttons | **Duration based** | "Slot based" or "Duration based". Duration-based shows "Search for a multi day hearing? (Yes/No)" — if Yes, requires number of days input | +| **Business type** | Dropdown | — | Crown court hearing categories (e.g. PTPH, Sentence) | +| **Session type** | Radio buttons | **Any** | "Any", "AM", "PM", or "All day" | +| **Start date** | Date picker | Varies by journey (see stories) | | +| **End date** | Date picker (optional) | Pre-populated to match current date | | + +#### Courtroom Filter → Session Visibility Rules + +| Courtroom Selection | Sessions Displayed | +|--------------------|--------------------| +| "No courtroom selected" | Courtrooms with **draft** sessions | +| "All" | Courtrooms with **assigned** sessions | +| Specific courtroom | That courtroom's **assigned** sessions | + +#### Validation Rules + +- For single-day sessions, duration must **not exceed six hours** +- "Back" button must allow changing the initial scheduling preference selection + +#### Search Result Columns + +Each result entry displays: + +| Column | Description | +|--------|-------------| +| **Selection** | Radio button to choose a specific date + court combination | +| **Date** | The calendar date for the slot | +| **Court Details** | Courthouse name; for Assigned sessions, also shows specific Courtroom (e.g. "Courtroom 1") | +| **Business** | Business type assigned to slot (e.g. PTPH, Sentence) | +| **Session** | Dropdown to select a specific time range (e.g. "10:00am to 11:00am") | +| **Time/slots remaining** | Numeric indicator of available capacity | +| **Booked** | List of currently scheduled times and booking status (e.g. "10:00am to 11:00am: 0") | + +#### Common Result Actions + +- Each result provides a link to **"Add listing note"** +- Option to select a **Hearing Type** (pre-populated with selected hearing type) +- Option to notify parties: **"Do you want to notify to all relevant parties?"** Yes/No (default: **No**) +- For **multi-day trials**: selecting the first session automatically selects consecutive sessions based on estimated duration; consecutive sessions must be of the **same type** as the first session +- On submit, **session availability is re-verified** before sharing results/notifying parties. If sessions are no longer available, user is notified and must rebook + +--- + +### Story 1: SPRDT-620 — Creating a New Crown Court Case + +> **[SPRDT-620](https://tools.hmcts.net/jira/browse/SPRDT-620)** | Status: Analysis | Assignee: Joan Porter + +**As a** crown court officer, +**I want** to choose the hearing session for a new case, +**So that** the system can direct me to the correct allocation workflow. + +#### Scenario 1: Choosing a Scheduling Option + +- **Entry point**: Creating a new case → reach the **"Find hearing date"** screen +- **Options displayed** (radio buttons): + - Date and time to be fixed + - Fixed date + - Week commencing +- Selecting **"Fixed date"** → navigates to the **"Find a hearing"** filter screen (currently "Court hearing details") +- Selecting **"Date and time to be fixed"** or **"Week commencing"** → bypasses the primary listing flow + +#### Scenario 2: Fixed Date Filters (Journey-Specific Defaults) + +- **Courtroom**: Default "No courtroom selected" +- **Start date**: Default to **current date** + +#### Scenario 3: Search Results & Submission + +- On successful selection and submission → directed to the **Defendant details** screen (for cases) +- Success message / green confirmation bar with concise listing details appears only at the **end of the full creation flow**, not immediately after session selection + +#### Scenario 4: Week Commencing + +- Directed to the current "Court hearing details" screen with existing Week Commencing functionality + +#### UI Reference + +- https://www.figma.com/design/et3TkPsUQXUitpkmF3JK2I/CCT-1984--Common-Platform--Court-schedule?node-id=3924-47025 + +--- + +### Story 2: SPRDT-619 — Creating a Hearing for a Crown Court Case or Linked Application + +> **[SPRDT-619](https://tools.hmcts.net/jira/browse/SPRDT-619)** | Status: Analysis | Assignee: Joan Porter + +**As a** crown court officer, +**I want** to choose the hearing session for a new hearing for a case or linked application, +**So that** the system can direct me to the correct allocation workflow. + +#### Scenario 1: Choosing a Scheduling Option + +- **Entry point**: Creating a new hearing → reach the **"Find hearing date"** screen +- **Options displayed** (radio buttons): + - Fixed date + - Week commencing +- Selecting **"Fixed date"** → navigates to **"List for Court Hearing"** screen, then the **"Find a hearing"** tab + +#### Scenario 2: Fixed Date Filters (Journey-Specific Defaults) + +- **Courtroom**: Default "No courtroom selected" +- **Start date**: Pre-populated with the **Hearing date** (if hearing has a current or future date, otherwise default to current date) + +#### Scenario 3: Search Results & Submission + +- On submission → directed to: + - **"Case at a glance"** screen (for case creation), with confirmation message + - OR **"Check application"** screen (for linked application creation) + +#### Scenario 4: Week Commencing + +- Directed to the current "List for court hearing" screen with existing Week Commencing functionality + +#### UI Reference + +- https://www.figma.com/design/et3TkPsUQXUitpkmF3JK2I/CCT-1984--Common-Platform--Court-schedule?node-id=3924-47025 + +--- + +### Story 3: SPRDT-618 — Box Work Hearing (Case / Application) + +> **[SPRDT-618](https://tools.hmcts.net/jira/browse/SPRDT-618)** | Status: Analysis | Assignee: Joan Porter + +**As a** crown court officer, +**I want** to choose the hearing session for a new Case or Application Boxwork Hearing, +**So that** the system can direct me to the correct allocation workflow. + +#### Scenario 1: Choosing a Scheduling Option + +- **Entry point**: Creating a court hearing for a pre-existing "hearing" in box work (**LHBW**) → select a Crown Court → directed to **"Find hearing date"** screen +- **Options displayed** (radio buttons): + - Date and time to be fixed + - Fixed date + - Week commencing +- Selecting **"Fixed date"** → navigates to **"Find a hearing"** (currently "Search for available sessions") filter screen + +#### Scenario 2: Fixed Date Filters (Journey-Specific Defaults) + +- **Courtroom**: Default "No courtroom selected" +- **Start date**: Default to **current date** + +#### Scenario 3: Search Results & Submission + +- On submission → directed to the **"Enter results"** screen to continue Case or Application boxwork, with confirmation message + +#### Scenario 4: Week Commencing + +- Directed to the current "Court hearing details" screen with existing Week Commencing functionality + +#### UI Reference + +- https://www.figma.com/design/et3TkPsUQXUitpkmF3JK2I/CCT-1984--Common-Platform--Court-schedule?node-id=3924-47025 + +--- + +### Story 4: SPRDT-617 — Unscheduled Hearing + +> **[SPRDT-617](https://tools.hmcts.net/jira/browse/SPRDT-617)** | Status: Analysis | Assignee: Joan Porter + +**As a** crown court officer, +**I want** to choose the hearing session for an Unscheduled hearing, +**So that** the system can direct me to the correct allocation workflow. + +#### Scenario 1: Entry & Navigation + +- **Entry point**: Creating a hearing session for an Unscheduled hearing → select a Crown Court +- System navigates to **"Allocate a hearing"** screen +- Selecting the **"Find a hearing"** tab applies filter constraints + +**Note**: No "Find hearing date" radio selection screen — goes directly to the Allocate a hearing / Find a hearing tab. + +#### Scenario 2: Fixed Date Filters (Journey-Specific Defaults) + +- **Courtroom**: Default "No courtroom selected" (with options: "No courtroom selected" / "All" / "Courtroom [n]") +- **Start date**: Default to **current date** + +#### Scenario 3: Search Results & Submission + +- On submission → directed to the **"Unscheduled list"** screen, with confirmation message + +#### UI Reference + +- https://www.figma.com/design/et3TkPsUQXUitpkmF3JK2I/CCT-1984--Common-Platform--Court-schedule?node-id=3924-47025 + +--- + +### Story 5: SPRDT-616 — Unallocated Hearing + +> **[SPRDT-616](https://tools.hmcts.net/jira/browse/SPRDT-616)** | Status: Analysis | Assignee: Joan Porter + +**As a** crown court officer, +**I want** to choose the hearing session for an Unallocated hearing, +**So that** the system can direct me to the correct allocation workflow. + +#### Scenario 1: Entry & Navigation + +- **Entry point**: Creating a hearing session for an Unallocated hearing → select a Crown Court +- System navigates to **"Allocate a hearing"** screen +- Selecting the **"Find a hearing"** tab applies filter constraints + +**Note**: No "Find hearing date" radio selection screen — goes directly to the Allocate a hearing / Find a hearing tab. + +#### Scenario 2: Fixed Date Filters (Journey-Specific Defaults) + +- **Courtroom**: Default **"All"** (options: "All" / "Courtroom [n]" — **NO** "No courtroom selected" option) +- **Start date**: Pre-populated with the **Hearing date** (if hearing has a current or future date, otherwise default to current date) + +#### Scenario 3: Search Results & Submission + +- On submission → directed to the **"Unallocated list"** screen, with confirmation message + +#### UI Reference + +- https://www.figma.com/design/et3TkPsUQXUitpkmF3JK2I/CCT-1984--Common-Platform--Court-schedule?node-id=3924-47025 + +--- + +## Cross-Story Comparison Matrix + +| Aspect | SPRDT-620 (New Case) | SPRDT-619 (New Hearing / App) | SPRDT-618 (Box Work) | SPRDT-617 (Unscheduled) | SPRDT-616 (Unallocated) | +|--------|---------------------|------------------------------|---------------------|------------------------|------------------------| +| **Radio options** | Fixed / Week / DTTBF | Fixed / Week | Fixed / Week / DTTBF | None (direct) | None (direct) | +| **Entry screen** | Find hearing date | Find hearing date | Find hearing date | Allocate a hearing | Allocate a hearing | +| **Courtroom default** | No courtroom selected | No courtroom selected | No courtroom selected | No courtroom selected | **All** | +| **"No courtroom" option** | Yes | Yes | Yes | Yes | **No** | +| **Start date default** | Current date | Hearing date (or current) | Current date | Current date | Hearing date (or current) | +| **Redirect after submit** | Defendant details | Case at a glance / Check application | Enter results | Unscheduled list | Unallocated list | +| **Confirmation timing** | End of full creation flow | On redirect | On redirect | On redirect | On redirect | + +--- + +## Appendix + +### Key Terminology + +| Term | Meaning | +|------|---------| +| **NHCC** | Next Hearing in a Crown Court | +| **LHBW** | List Hearing from Box Work | +| **DTTBF** | Date and Time to Be Fixed | +| **Draft session** | A session not yet assigned to a specific courtroom | +| **Assigned session** | A session allocated to a specific courtroom | +| **Slot based** | Booking by number of slots | +| **Duration based** | Booking by time duration | +| **BDF** | Bulk Data File (for importing existing hearings) | + +### Comments / Notes from Epic + +- **Figma (Sarat KumarPenumarthy, 20 Jan 2026)**: https://www.figma.com/design/et3TkPsUQXUitpkmF3JK2I/CCT-1984--Common-Platform--Court-schedule?node-id=3136-31867 +- **Attachment**: "New Find a hearing screens.pptm" — design changes for converting "Enter hearing details" to "Find a hearing" search diff --git a/listing-command/listing-command-api/pom.xml b/listing-command/listing-command-api/pom.xml index 6f6639ba5..e86306f36 100644 --- a/listing-command/listing-command-api/pom.xml +++ b/listing-command/listing-command-api/pom.xml @@ -4,7 +4,7 @@ listing-command uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentService.java b/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentService.java index 957bacc8b..e4ebca3ff 100644 --- a/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentService.java +++ b/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentService.java @@ -3,15 +3,14 @@ import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import static java.util.UUID.fromString; -import static uk.gov.justice.services.messaging.JsonObjects.createArrayBuilder; -import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.apache.commons.collections.CollectionUtils.isNotEmpty; import static org.apache.commons.lang3.StringUtils.isBlank; +import static uk.gov.justice.services.messaging.JsonObjects.createArrayBuilder; +import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; import static uk.gov.moj.cpp.listing.command.api.service.HearingDaysEnrichmentService.log; import uk.gov.justice.core.courts.CourtCentre; -import uk.gov.moj.cpp.listing.domain.JudicialRole; import uk.gov.justice.core.courts.JurisdictionType; import uk.gov.justice.core.courts.RotaSlot; import uk.gov.justice.listing.commands.HearingDay; @@ -25,6 +24,7 @@ import uk.gov.moj.cpp.listing.common.service.HearingSlotsService; import uk.gov.moj.cpp.listing.domain.CourtSchedule; import uk.gov.moj.cpp.listing.domain.HearingSlotSearchResponse; +import uk.gov.moj.cpp.listing.domain.JudicialRole; import uk.gov.moj.cpp.listing.domain.JudicialRoleType; import uk.gov.moj.cpp.listing.domain.ListUpdateHearing; import uk.gov.moj.cpp.listing.domain.utils.DateAndTimeUtils; @@ -55,8 +55,12 @@ @SuppressWarnings("java:S3776") public class CourtScheduleEnrichmentService implements EnrichmentService { private static final String HEARING_SLOTS = "hearingSlots"; + private static final String COURT_SCHEDULE_ID = "courtScheduleId"; private static final String COURT_SCHEDULE_IDS = "courtScheduleIds"; + private static final String COURT_SCHEDULES = "courtSchedules"; + private static final String DURATION = "duration"; private static final String JUDICIARIES = "judiciaries"; + private static final String IS_DRAFT = "isDraft"; @Inject private CourtSchedulerService courtSchedulerService; @Inject @@ -86,6 +90,9 @@ public HearingListingNeeds enrichWithCourtSchedules(final HearingListingNeeds he } public UpdateHearingForListing enrichWithCourtSchedules(final UpdateHearingForListing updateHearingForListing, final JsonEnvelope envelope) { + if (JurisdictionType.CROWN.equals(updateHearingForListing.getJurisdictionType())) { + return enrichCrownUpdateHearing(updateHearingForListing); + } //HearingDays courtscheduleId provided in payload, we can list them directly List hearingDaysWithCourScheduleId = new ArrayList<>(); @@ -124,10 +131,267 @@ public UpdateHearingForListing enrichWithCourtSchedules(final UpdateHearingForLi return hearingBuilder.build(); } + /** + * CROWN-first enrichment: determines case and calls appropriate court scheduler endpoint. + * Called BEFORE HearingDays and Duration enrichment for CROWN hearings. + * + * Case 1: No courtScheduleId anywhere -> return unchanged + * Case 2: Has courtScheduleId + aggregated duration > 360 -> multiDaySearchAndBook + * Case 3: Has courtScheduleId + aggregated duration <= 360 -> listHearingInCourtSessions + */ + public HearingListingNeeds enrichCrownCourtScheduleFirst(final HearingListingNeeds hearing) { + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Starting for hearingId: {}", hearing.getId()); + + final boolean hasCourtScheduleIdOnHearingDays = anyHearingDayHasCourtScheduleId(hearing); + final boolean hasCourtScheduleIdOnBookedSlots = hasBookedSlotsWithCourtScheduleId(hearing); + final boolean hasCourtScheduleId = hasCourtScheduleIdOnHearingDays || hasCourtScheduleIdOnBookedSlots; + + if (!hasCourtScheduleId) { + // TODO CROWN without any courtScheduleId (hearingDays or bookedSlots): + // a later ticket will call courtscheduler searchAndBook here to discover a slot. + // For now, return unchanged so the existing handleAllocationCandidate path (via + // checkAndUpdateListingCourtScheduler) can still be reached for CROWN adhoc-without-slot. + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] No courtScheduleId anywhere; returning unchanged for hearingId: {}", hearing.getId()); + return hearing; + } + + // NOTE: we intentionally do NOT skip when courtScheduleId is on bookedSlots only. + // Multi-day resolution anchors off bookedSlots[0].courtScheduleId; single-day + // resolution prefers hearingDays but falls through to bookedSlots where present. + + final int aggregatedDuration = calculateAggregatedDuration(hearing); + final boolean isMultiDay = aggregatedDuration > HearingDurationEnrichmentService.MINUTES_IN_DAY; + + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] hearingId: {}, aggregatedDuration={}, isMultiDay={} (threshold={})", + hearing.getId(), aggregatedDuration, isMultiDay, HearingDurationEnrichmentService.MINUTES_IN_DAY); + + EnrichmentResult enrichmentResult; + if (isMultiDay) { + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Case 2: Multi-day -> multiDaySearchAndBook for hearingId: {}", hearing.getId()); + enrichmentResult = handleCrownMultiDayEnrichment(hearing, aggregatedDuration); + } else { + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Case 3: Single-day -> listHearingInCourtSessions for hearingId: {}", hearing.getId()); + enrichmentResult = handleCrownSingleDayEnrichment(hearing); + } + + final List enrichedHearingDays = enrichmentResult.getHearingDays(); + final List enrichedJudiciaries = enrichmentResult.getJudiciaries(); + + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Result: enrichedHearingDays={}, judiciaries={} for hearingId: {}", + enrichedHearingDays.size(), enrichedJudiciaries.size(), hearing.getId()); + + HearingListingNeeds.Builder hearingBuilder = HearingListingNeeds.hearingListingNeeds() + .withValuesFrom(hearing) + .withHearingDays(enrichedHearingDays); + + if (isNotEmpty(enrichedJudiciaries)) { + hearingBuilder.withJudiciary(convertJudicialRoleDomainToCore(enrichedJudiciaries)); + } + + // Adjust court centre if scheduler returned a different room (only for non-draft sessions) + if (isNotEmpty(enrichedHearingDays) && nonNull(hearing.getCourtCentre()) && nonNull(enrichedHearingDays.get(0).getCourtRoomId())) { + final CourtCentre adjustedCourtCentre = CourtCentre.courtCentre() + .withValuesFrom(hearing.getCourtCentre()) + .withRoomId(enrichedHearingDays.get(0).getCourtRoomId()) + .build(); + hearingBuilder.withCourtCentre(adjustedCourtCentre); + } + + return hearingBuilder.build(); + } + + /** + * CROWN-first enrichment for update path. + * Called BEFORE HearingDays and Duration enrichment for CROWN update hearings. + */ + public UpdateHearingForListing enrichCrownCourtScheduleFirst(final UpdateHearingForListing hearing) { + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Update path starting for hearingId: {}", hearing.getHearingId()); + + final boolean hasCourtScheduleIdOnHearingDays = !isEmpty(hearing.getHearingDays()) + && hearing.getHearingDays().stream().anyMatch(d -> nonNull(d.getCourtScheduleId())); + final boolean hasCourtScheduleIdOnNonDefaultDays = !isEmpty(hearing.getNonDefaultDays()) + && hearing.getNonDefaultDays().stream().anyMatch(d -> nonNull(d.getCourtScheduleId())); + final boolean hasCourtScheduleId = hasCourtScheduleIdOnHearingDays || hasCourtScheduleIdOnNonDefaultDays; + + if (!hasCourtScheduleId) { + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Update Case 1: No courtScheduleId on hearingDays or nonDefaultDays. Returning unchanged for hearingId: {}", hearing.getHearingId()); + return hearing; + } + + final int aggregatedDuration = calculateAggregatedDuration(hearing); + LOGGER.info("[CROWN-ENRICH][CourtSchedule-First] Update hearingId: {}, aggregatedDuration={}, isMultiDay={}", + hearing.getHearingId(), aggregatedDuration, aggregatedDuration > HearingDurationEnrichmentService.MINUTES_IN_DAY); + + // Delegate to existing enrichCrownUpdateHearing which already handles multi-day vs single-day + return enrichCrownUpdateHearing(hearing); + } + + private UpdateHearingForListing enrichCrownUpdateHearing(final UpdateHearingForListing hearing) { + LOGGER.info("CROWN update enrichment for hearingId: {}", hearing.getHearingId()); + + final boolean anyHearingDayHasCourtScheduleId = !isEmpty(hearing.getHearingDays()) + && hearing.getHearingDays().stream().anyMatch(d -> nonNull(d.getCourtScheduleId())); + if (!anyHearingDayHasCourtScheduleId) { + if (isCandidateForAllocation(hearing)) { + LOGGER.info("CROWN update: no courtScheduleIds but allocation candidate for hearingId {}. Searching and booking.", hearing.getHearingId()); + return handleCrownUpdateSearchAndBook(hearing); + } + LOGGER.info("CROWN update: no courtScheduleIds on hearingDays for hearingId {}. Skipping court schedule enrichment.", hearing.getHearingId()); + return hearing; + } + + final int totalDuration = hearing.getHearingDays().stream() + .mapToInt(d -> d.getDurationMinutes() != null ? d.getDurationMinutes() : 0) + .sum(); + final boolean isMultiDay = totalDuration > HearingDurationEnrichmentService.MINUTES_IN_DAY; + + EnrichmentResult enrichmentResult; + if (isMultiDay) { + final HearingDay firstDay = hearing.getHearingDays().stream() + .filter(d -> nonNull(d.getCourtScheduleId())) + .findFirst().orElse(null); + + if (firstDay == null) { + LOGGER.error("CROWN multi-day update: no courtScheduleId on hearingDays for hearingId {}", hearing.getHearingId()); + return hearing; + } + + final List sessions = multiDaySearchAndBook( + firstDay.getCourtScheduleId().toString(), + totalDuration, + hearing.getHearingId().toString()); + + if (isEmpty(sessions)) { + LOGGER.warn("CROWN multi-day update: no sessions found for hearingId {}.", hearing.getHearingId()); + return hearing; + } + + final int daysNeeded = sessions.size(); + final int durationPerDay = totalDuration / daysNeeded; + final List expandedDays = sessions.stream().map(session -> { + HearingDay.Builder dayBuilder = HearingDay.hearingDay() + .withCourtScheduleId(fromString(session.getCourtScheduleId())) + .withStartTime(nonNull(session.getHearingStartTime()) ? ZonedDateTime.parse(session.getHearingStartTime()) : null) + .withHearingDate(session.getSessionDate()) + .withDurationMinutes(durationPerDay) + .withIsDraft(session.isDraft()); + if (!session.isDraft()) { + dayBuilder.withCourtCentreId(fromString(session.getCourtHouseId())); + dayBuilder.withCourtRoomId(fromString(session.getCourtRoomId())); + } + return dayBuilder.build(); + }).toList(); + + final boolean allNonDraft = sessions.stream().noneMatch(CourtSchedule::isDraft); + if (!allNonDraft) { + LOGGER.info("CROWN multi-day update: isDraft=true sessions for hearingId {}. Listing in court sessions for slot deduction, allocation decided by aggregate.", hearing.getHearingId()); + } + + enrichmentResult = listHearingSessionsAndExtractData(hearing.getHearingId(), expandedDays); + } else { + final List courtScheduleIds = hearing.getHearingDays().stream() + .filter(d -> nonNull(d.getCourtScheduleId())) + .map(d -> d.getCourtScheduleId().toString()) + .toList(); + + final List sessions = fetchCourtSchedulesByIds(courtScheduleIds); + + if (isEmpty(sessions)) { + LOGGER.warn("CROWN single-day update: failed to fetch court schedules for hearingId {}. Returning unchanged.", hearing.getHearingId()); + return hearing; + } + + final boolean allNonDraft = sessions.stream().noneMatch(CourtSchedule::isDraft); + final List sanityCheckedDays = sanityCheckAndEnrichCrown(hearing.getHearingDays(), sessions, hearing.getHearingId()); + + if (!allNonDraft) { + LOGGER.info("CROWN single-day update: isDraft=true sessions for hearingId {}. Listing in court sessions for slot deduction, allocation decided by aggregate.", hearing.getHearingId()); + } + + enrichmentResult = listHearingSessionsAndExtractData(hearing.getHearingId(), sanityCheckedDays); + } + + final List enrichedHearingDays = enrichmentResult.getHearingDays(); + final List enrichedJudiciaries = enrichmentResult.getJudiciaries(); + + UpdateHearingForListing.Builder hearingBuilder = UpdateHearingForListing.updateHearingForListing() + .withValuesFrom(hearing) + .withHearingDays(enrichedHearingDays); + + if (isNotEmpty(hearing.getJudiciary())) { + hearingBuilder.withJudiciary(hearing.getJudiciary()); + } else if (isNotEmpty(enrichedJudiciaries)) { + hearingBuilder.withJudiciary(convertJudicialRoleDomainToCore(enrichedJudiciaries)); + } + + return hearingBuilder.build(); + } + + private UpdateHearingForListing handleCrownUpdateSearchAndBook(final UpdateHearingForListing hearing) { + List hearingDaysWithCourtScheduleId = new ArrayList<>(); + List judicialRolesBySearchAndBook = new ArrayList<>(); + + hearing.getHearingDays().forEach(hearingDay -> { + if (isNull(hearingDay.getCourtScheduleId())) { + final String hearingDate = nonNull(hearingDay.getHearingDate()) + ? hearingDay.getHearingDate().toString() + : hearing.getStartDate().toString(); + final String startTime = nonNull(hearingDay.getStartTime()) + ? DateAndTimeUtils.toIsoString(hearingDay.getStartTime()) + : null; + final UUID courtRoomId = nonNull(hearingDay.getCourtRoomId()) + ? hearingDay.getCourtRoomId() + : hearing.getCourtRoomId(); + HearingSlotSearchResponse hearingSlotSearchResponse = searchAndBookSlots( + hearing.getHearingId().toString(), + hearing.getCourtCentreId().toString(), + hearingDate, + nonNull(courtRoomId) ? courtRoomId.toString() : null, + nonNull(hearing.getEndDate()) ? hearing.getEndDate().toString() : null, + startTime, + hearingDay.getDurationMinutes(), + false + ); + if (hearingSlotSearchResponse == null) { + hearingDaysWithCourtScheduleId.add(hearingDay); + } else { + // Only take courtScheduleId and isDraft from searchAndBook; preserve hearing day's original courtRoomId/courtCentreId/dates + hearingDaysWithCourtScheduleId.add(HearingDay.hearingDay() + .withValuesFrom(hearingDay) + .withCourtScheduleId(fromString(hearingSlotSearchResponse.courtScheduleId())) + .withIsDraft(hearingSlotSearchResponse.isDraft()) + .build()); + if (hearingSlotSearchResponse.judiciaries() != null && !hearingSlotSearchResponse.judiciaries().isEmpty()) { + judicialRolesBySearchAndBook.addAll(hearingSlotSearchResponse.judiciaries()); + } + } + } else { + hearingDaysWithCourtScheduleId.add(hearingDay); + } + }); + + if (hearingDaysWithCourtScheduleId.stream().allMatch(d -> isNull(d.getCourtScheduleId()))) { + LOGGER.warn("CROWN update searchAndBook: no slots found for hearingId {}. Returning unchanged.", hearing.getHearingId()); + return hearing; + } + + UpdateHearingForListing.Builder hearingBuilder = UpdateHearingForListing.updateHearingForListing() + .withValuesFrom(hearing) + .withHearingDays(hearingDaysWithCourtScheduleId); + + if (isNotEmpty(hearing.getJudiciary())) { + hearingBuilder.withJudiciary(hearing.getJudiciary()); + } else if (isNotEmpty(judicialRolesBySearchAndBook)) { + hearingBuilder.withJudiciary(convertJudicialRoleDomainToCore(judicialRolesBySearchAndBook)); + } + + return hearingBuilder.build(); + } + public static boolean isCandidateForAllocation(final HearingListingNeeds hearing) { //This is derived from Hearing aggregate canAllocate() boolean hasValidStartDateTime = nonNull(hearing.getListedStartDateTime()) || nonNull(hearing.getEarliestStartDateTime()); - boolean hasAssignedCourtRoom = nonNull(hearing.getCourtCentre().getRoomId()); + boolean hasAssignedCourtRoom = nonNull(hearing.getCourtCentre()) && nonNull(hearing.getCourtCentre().getRoomId()); boolean hasJurisdictionType = nonNull(hearing.getJurisdictionType()); @@ -164,18 +428,26 @@ private HearingListingNeeds checkAndUpdateListingCourtScheduler(final HearingLis if (needsCourtScheduleEnrichment(hearing)) { EnrichmentResult enrichmentResult; + if (JurisdictionType.CROWN.equals(hearing.getJurisdictionType()) && anyHearingDayHasCourtScheduleId(hearing)) { + enrichmentResult = handleCrownEnrichment(hearing); + } // Case 1: All nondefault days have courtScheduleId - if (allHearingDaysHaveCourtScheduleId(hearing)) { + else if (allHearingDaysHaveCourtScheduleId(hearing)) { enrichmentResult = handleDirectListingCase(hearing); } - // Case 2: Has booking reference (provisional booking) - else if (nonNull(hearing.getBookingReference())) { - enrichmentResult = handleProvisionalBookingCase(hearing); - } - // Case 3: Has booked slots with courtScheduleId + // Case 2: Has booked slots with courtScheduleId (Crown or MAGS) else if (hasBookedSlotsWithCourtScheduleId(hearing)) { enrichmentResult = handleBookedSlotsCase(hearing); } + // Crown without courtScheduleIds: go directly to searchAndBook, skip provisional booking + else if (JurisdictionType.CROWN.equals(hearing.getJurisdictionType()) && isCandidateForAllocation(hearing)) { + LOGGER.info("CROWN hearing without courtScheduleIds, searching and booking for hearingId: {}", hearing.getId()); + enrichmentResult = handleAllocationCandidate(hearing, envelope); + } + // Case 3: Has booking reference (provisional booking) — MAGS only at this point + else if (nonNull(hearing.getBookingReference())) { + enrichmentResult = handleProvisionalBookingCase(hearing); + } // Case 4: Is candidate for allocation else if (isCandidateForAllocation(hearing)) { LOGGER.info("Hearing is candidate for allocation, so we need to search and book hearingId : {}, hearingDays : {}", hearing.getId(), log(hearing.getHearingDays())); @@ -194,9 +466,11 @@ else if (isCandidateForAllocation(hearing)) { } /**in case we land in a different courtroom then requested, this should be reflected to main CourtCentre Object will be removed with LPT-1090 along with LPT-1355*/ - final CourtCentre adjustedCourtCentre = CourtCentre.courtCentre().withValuesFrom(hearing.getCourtCentre()) - .withRoomId(enrichedHearingDays.get(0).getCourtRoomId()) - .build(); + final CourtCentre.Builder courtCentreBuilder = CourtCentre.courtCentre().withValuesFrom(hearing.getCourtCentre()); + if (nonNull(enrichedHearingDays.get(0).getCourtRoomId())) { + courtCentreBuilder.withRoomId(enrichedHearingDays.get(0).getCourtRoomId()); + } + final CourtCentre adjustedCourtCentre = courtCentreBuilder.build(); HearingListingNeeds.Builder hearingBuilder = HearingListingNeeds.hearingListingNeeds() .withValuesFrom(hearing) @@ -275,6 +549,12 @@ private boolean allHearingDaysHaveCourtScheduleId(HearingListingNeeds hearing) { .noneMatch(day -> isNull(day.getCourtScheduleId())); } + private boolean anyHearingDayHasCourtScheduleId(HearingListingNeeds hearing) { + return !isEmpty(hearing.getHearingDays()) && + hearing.getHearingDays().stream() + .anyMatch(day -> nonNull(day.getCourtScheduleId())); + } + private boolean hasBookedSlotsWithCourtScheduleId(HearingListingNeeds hearing) { return isNotEmpty(hearing.getBookedSlots()) && hearing.getBookedSlots().stream() @@ -285,16 +565,22 @@ private AllocationResult handleAllocationCandidate(HearingListingNeeds hearing, List hearingDaysBySearchAndBook = new ArrayList<>(); List judicialRolesBySearchAndBook = new ArrayList<>(); + final ZonedDateTime effectiveStartDateTime = nonNull(hearing.getListedStartDateTime()) + ? hearing.getListedStartDateTime() + : hearing.getEarliestStartDateTime(); + hearing.getHearingDays().forEach(hearingDay -> { if (isNull(hearingDay.getCourtScheduleId())) { - boolean isPolice = isPolice(hearing, envelope); + boolean isPolice = JurisdictionType.CROWN.equals(hearing.getJurisdictionType()) + ? false + : isPolice(hearing, envelope); HearingSlotSearchResponse hearingSlotSearchResponse = searchAndBookSlots( hearing.getId().toString(), hearing.getCourtCentre().getId().toString(), - hearing.getListedStartDateTime().toLocalDate().toString(), + effectiveStartDateTime.toLocalDate().toString(), hearing.getCourtCentre().getRoomId().toString(), hearing.getEndDate(), - DateAndTimeUtils.toIsoString(hearing.getListedStartDateTime()), + DateAndTimeUtils.toIsoString(effectiveStartDateTime), hearing.getEstimatedMinutes(), isPolice ); @@ -313,6 +599,248 @@ private AllocationResult handleAllocationCandidate(HearingListingNeeds hearing, return new AllocationResult(hearingDaysBySearchAndBook, judicialRolesBySearchAndBook); } + private EnrichmentResult handleCrownEnrichment(final HearingListingNeeds hearing) { + // Use aggregated duration (hearingDays / nonDefaultDays / bookedSlots / estimatedMinutes priority) + // rather than raw estimatedMinutes — the UI has been observed to send a wrong estimatedMinutes + // for multi-day Crown hearings, so we trust the bookedSlots sum when available. + final int aggregatedDuration = calculateAggregatedDuration(hearing); + final boolean isMultiDay = aggregatedDuration > HearingDurationEnrichmentService.MINUTES_IN_DAY; + if (isMultiDay) { + return handleCrownMultiDayEnrichment(hearing, aggregatedDuration); + } + return handleCrownSingleDayEnrichment(hearing); + } + + private EnrichmentResult handleCrownSingleDayEnrichment(final HearingListingNeeds hearing) { + LOGGER.info("CROWN single-day enrichment for hearingId: {}", hearing.getId()); + + // courtScheduleIds can live on hearingDays (direct-listing shape) or on bookedSlots + // (adhoc / MCC shape where hearingDays have not been materialised yet). + final List courtScheduleIds = collectSingleDayCourtScheduleIds(hearing); + + if (courtScheduleIds.isEmpty()) { + LOGGER.warn("CROWN single-day: no courtScheduleId on hearingDays or bookedSlots for hearingId {}. Unchanged.", hearing.getId()); + return new EnrichmentResult(hearing.getHearingDays(), new ArrayList<>()); + } + + final List sessions = fetchCourtSchedulesByIds(courtScheduleIds); + + if (isEmpty(sessions)) { + LOGGER.warn("CROWN single-day: failed to fetch court schedules for hearingId {}. Returning unchanged.", hearing.getId()); + return new EnrichmentResult(hearing.getHearingDays(), new ArrayList<>()); + } + + final boolean allNonDraft = sessions.stream().noneMatch(CourtSchedule::isDraft); + + // If hearingDays is empty, materialise one from the fetched session (single-day = 1 session). + // Otherwise preserve existing hearingDays and merge session data via sanity check. + final List preparedDays = isEmpty(hearing.getHearingDays()) + ? buildHearingDaysFromSingleDaySessions(sessions, hearing) + : sanityCheckAndEnrichCrown(hearing.getHearingDays(), sessions, hearing.getId()); + + if (!allNonDraft) { + LOGGER.info("CROWN single-day: isDraft=true sessions for hearingId {}. Listing in court sessions for slot deduction, allocation decided by aggregate.", hearing.getId()); + } + + return listHearingSessionsAndExtractData(hearing.getId(), preparedDays); + } + + private List collectSingleDayCourtScheduleIds(final HearingListingNeeds hearing) { + final List fromHearingDays = isEmpty(hearing.getHearingDays()) + ? Collections.emptyList() + : hearing.getHearingDays().stream() + .filter(d -> nonNull(d.getCourtScheduleId())) + .map(d -> d.getCourtScheduleId().toString()) + .toList(); + if (!fromHearingDays.isEmpty()) { + return fromHearingDays; + } + return isEmpty(hearing.getBookedSlots()) + ? Collections.emptyList() + : hearing.getBookedSlots().stream() + .map(RotaSlot::getCourtScheduleId) + .filter(id -> !isBlank(id)) + .toList(); + } + + private List buildHearingDaysFromSingleDaySessions(final List sessions, final HearingListingNeeds hearing) { + // For single-day we expect exactly one session. Duration falls back to estimatedMinutes. + final Integer fallbackDuration = hearing.getEstimatedMinutes(); + return sessions.stream().limit(1).map(session -> HearingDay.hearingDay() + .withCourtCentreId(fromString(session.getCourtHouseId())) + .withCourtScheduleId(fromString(session.getCourtScheduleId())) + .withCourtRoomId(session.isDraft() || isBlank(session.getCourtRoomId()) ? null : fromString(session.getCourtRoomId())) + .withStartTime(nonNull(session.getHearingStartTime()) ? ZonedDateTime.parse(session.getHearingStartTime()) : null) + .withHearingDate(session.getSessionDate()) + .withDurationMinutes(fallbackDuration != null ? fallbackDuration : 0) + .withIsDraft(session.isDraft()) + .build() + ).toList(); + } + + private EnrichmentResult handleCrownMultiDayEnrichment(final HearingListingNeeds hearing, final int aggregatedDuration) { + LOGGER.info("CROWN multi-day enrichment for hearingId: {}, aggregatedDuration: {}", hearing.getId(), aggregatedDuration); + + // Anchor off the first bookedSlot. For CROWN adjournment + MCC, courtScheduleId lives on + // bookedSlots, not hearingDays. The scheduler expands from this anchor into N consecutive + // sessions, each with its own courtScheduleId and sessionDate. + final String anchorCourtScheduleId = isNotEmpty(hearing.getBookedSlots()) + ? hearing.getBookedSlots().get(0).getCourtScheduleId() + : null; + + if (isBlank(anchorCourtScheduleId)) { + LOGGER.error("CROWN multi-day: no bookedSlot courtScheduleId to anchor search for hearingId {}", hearing.getId()); + return new EnrichmentResult(hearing.getHearingDays(), new ArrayList<>()); + } + + // Use aggregatedDuration (bookedSlots / hearingDays / nonDefaultDays sum) not estimatedMinutes — + // UI has been observed to submit a stale estimatedMinutes that would pick the wrong slot count. + final List sessions = multiDaySearchAndBook( + anchorCourtScheduleId, + aggregatedDuration, + hearing.getId().toString()); + + if (isEmpty(sessions)) { + LOGGER.warn("CROWN multi-day: no consecutive sessions found for hearingId {}. Unallocated.", hearing.getId()); + return new EnrichmentResult(hearing.getHearingDays(), new ArrayList<>()); + } + + // Defensive: courtscheduler returned fewer sessions than the duration requires. This typically means + // the anchor slot was not a true multi-day-capable (AD) session — often because the slot search that + // produced the anchor omitted `isMultiday=true` / `courtSession=AD`. Log a clear warning so callers + // can correct their slot-search parameters. We still emit whatever the scheduler gave us so the + // mismatch surfaces in downstream assertions (caller expected N hearingDays, got M expandedDays = buildHearingDaysFromMultiDaySessions(sessions, aggregatedDuration); + + final boolean allNonDraft = sessions.stream().noneMatch(CourtSchedule::isDraft); + if (!allNonDraft) { + LOGGER.info("CROWN multi-day: isDraft=true sessions for hearingId {}. Listing in court sessions for slot deduction, allocation decided by aggregate.", hearing.getId()); + } + + return listHearingSessionsAndExtractData(hearing.getId(), expandedDays); + } + + private List fetchCourtSchedulesByIds(final List courtScheduleIds) { + final Map params = new HashMap<>(); + params.put(COURT_SCHEDULE_IDS, String.join(",", courtScheduleIds)); + final Response response = hearingSlotsService.getCourtSchedulesById(params); + + if (!isSuccess(response)) { + LOGGER.error("fetchCourtSchedulesByIds failed with status {}", response.getStatus()); + return new ArrayList<>(); + } + + final JsonObject responseJson = objectToJsonObjectConverter.convert(response.getEntity()); + if (responseJson == null || responseJson.isEmpty()) { + return new ArrayList<>(); + } + + final JsonArray schedulesArray = responseJson.getJsonArray(COURT_SCHEDULES); + if (schedulesArray == null || schedulesArray.isEmpty()) { + return new ArrayList<>(); + } + + final List schedules = new ArrayList<>(); + for (int i = 0; i < schedulesArray.size(); i++) { + final CourtSchedule cs = jsonObjectConverter.convert(schedulesArray.getJsonObject(i), CourtSchedule.class); + schedules.add(cs); + } + return schedules; + } + + private List multiDaySearchAndBook(final String courtScheduleId, final Integer durationInMinutes, final String hearingId) { + final Map params = new HashMap<>(); + params.put(COURT_SCHEDULE_ID, courtScheduleId); + params.put(DURATION_MINUTES, String.valueOf(durationInMinutes)); + params.put(HEARING_ID, hearingId); + final Response response = hearingSlotsService.multiDaySearchAndBook(params); + + if (!isSuccess(response)) { + LOGGER.error("multiDaySearchAndBook failed with status {} for hearingId {}", response.getStatus(), hearingId); + return new ArrayList<>(); + } + + final JsonObject responseJson = objectToJsonObjectConverter.convert(response.getEntity()); + if (responseJson == null || responseJson.isEmpty()) { + return new ArrayList<>(); + } + + final JsonArray schedulesArray = responseJson.getJsonArray(COURT_SCHEDULES); + if (schedulesArray == null || schedulesArray.isEmpty()) { + return new ArrayList<>(); + } + + final List schedules = new ArrayList<>(); + for (int i = 0; i < schedulesArray.size(); i++) { + final CourtSchedule cs = jsonObjectConverter.convert(schedulesArray.getJsonObject(i), CourtSchedule.class); + schedules.add(cs); + } + return schedules; + } + + private List sanityCheckAndEnrichCrown(final List hearingDays, final List sessions, final UUID hearingId) { + final Map sessionsById = sessions.stream() + .collect(Collectors.toMap(CourtSchedule::getCourtScheduleId, s -> s)); + + return hearingDays.stream().map(hd -> { + if (isNull(hd.getCourtScheduleId())) { + return hd; + } + final CourtSchedule session = sessionsById.get(hd.getCourtScheduleId().toString()); + if (session == null) { + LOGGER.error("CROWN sanity: no session for courtScheduleId {} hearingId {}", hd.getCourtScheduleId(), hearingId); + return hd; + } + if (nonNull(hd.getHearingDate()) && !hd.getHearingDate().equals(session.getSessionDate())) { + LOGGER.error("CROWN sanity: hearingDate={} but sessionDate={} for hearingId {}. Using scheduler value.", + hd.getHearingDate(), session.getSessionDate(), hearingId); + } + final HearingDay.Builder builder = HearingDay.hearingDay() + .withValuesFrom(hd) + .withHearingDate(session.getSessionDate()) + .withIsDraft(session.isDraft()); + if (session.isDraft()) { + // Draft sessions: clear any inherited courtRoomId — room is not confirmed + builder.withCourtRoomId(null); + } else { + if (nonNull(session.getCourtRoomId())) { + builder.withCourtRoomId(fromString(session.getCourtRoomId())); + } + if (nonNull(session.getCourtHouseId())) { + builder.withCourtCentreId(fromString(session.getCourtHouseId())); + } + } + if (nonNull(session.getHearingStartTime())) { + builder.withStartTime(ZonedDateTime.parse(session.getHearingStartTime())); + } + return builder.build(); + }).toList(); + } + + private List buildHearingDaysFromMultiDaySessions(final List sessions, final int aggregatedDuration) { + final int daysNeeded = sessions.size(); + final int durationPerDay = aggregatedDuration / daysNeeded; + + return sessions.stream().map(session -> HearingDay.hearingDay() + .withCourtCentreId(fromString(session.getCourtHouseId())) + .withCourtScheduleId(fromString(session.getCourtScheduleId())) + .withCourtRoomId(fromString(session.getCourtRoomId())) + .withStartTime(nonNull(session.getHearingStartTime()) ? ZonedDateTime.parse(session.getHearingStartTime()) : null) + .withHearingDate(session.getSessionDate()) + .withDurationMinutes(durationPerDay) + .withIsDraft(session.isDraft()) + .build() + ).toList(); + } + private List generateHearingDaysFromCourtSchedule(final List hearingDays, final List courtScheduleList, final HearingListingNeeds hearing) { final List hearingDaysUpdatedByCourtSchedules = new ArrayList<>(); final Map hearingDaysMapByDate = hearingDays.stream().collect(Collectors.toMap(HearingDay::getHearingDate, HearingDay -> HearingDay)); @@ -338,8 +866,78 @@ private List generateHearingDaysFromCourtSchedule(final List nonNull(d.getCourtScheduleId())); + final boolean onBookedSlots = isNotEmpty(hearing.getBookedSlots()) + && hearing.getBookedSlots().stream().anyMatch(s -> !isBlank(s.getCourtScheduleId())); + return onHearingDays || onBookedSlots; + } + static boolean needsCourtScheduleEnrichment(final HearingListingNeeds hearing) { - return hearing.getJurisdictionType().equals(JurisdictionType.MAGISTRATES) && (!isEmpty(hearing.getNonDefaultDays()) || nonNull(hearing.getBookingReference()) || nonNull(hearing.getBookedSlots()) || isCandidateForAllocation(hearing)); + if (JurisdictionType.MAGISTRATES.equals(hearing.getJurisdictionType())) { + return !isEmpty(hearing.getNonDefaultDays()) || nonNull(hearing.getBookingReference()) + || nonNull(hearing.getBookedSlots()) || isCandidateForAllocation(hearing); + } + if (JurisdictionType.CROWN.equals(hearing.getJurisdictionType())) { + return isCrownFixedDateWithCourtScheduleId(hearing) + || (isNull(hearing.getWeekCommencingDate()) && isCandidateForAllocation(hearing)); + } + return false; + } + + /** + * Calculates the total duration for the CROWN multi-day vs single-day decision. + * Priority: hearingDays durationMinutes → nonDefaultDays duration → bookedSlots duration → estimatedMinutes → 0. + * bookedSlots sits above estimatedMinutes because for CROWN adjournment / MCC the bookedSlots + * are the authoritative booked window whereas estimatedMinutes can be 0 or a per-offence value. + */ + static int calculateAggregatedDuration(final HearingListingNeeds hearing) { + if (isNotEmpty(hearing.getHearingDays())) { + return hearing.getHearingDays().stream() + .mapToInt(d -> d.getDurationMinutes() != null ? d.getDurationMinutes() : 0) + .sum(); + } + if (isNotEmpty(hearing.getNonDefaultDays())) { + return hearing.getNonDefaultDays().stream() + .mapToInt(d -> d.getDuration() != null ? d.getDuration() : 0) + .sum(); + } + if (isNotEmpty(hearing.getBookedSlots())) { + final int bookedSlotsTotal = hearing.getBookedSlots().stream() + .mapToInt(s -> s.getDuration() != null ? s.getDuration() : 0) + .sum(); + if (bookedSlotsTotal > 0) { + return bookedSlotsTotal; + } + } + return hearing.getEstimatedMinutes() != null ? hearing.getEstimatedMinutes() : 0; + } + + static int calculateAggregatedDuration(final UpdateHearingForListing hearing) { + if (isNotEmpty(hearing.getHearingDays())) { + return hearing.getHearingDays().stream() + .mapToInt(d -> d.getDurationMinutes() != null ? d.getDurationMinutes() : 0) + .sum(); + } + if (isNotEmpty(hearing.getNonDefaultDays())) { + return hearing.getNonDefaultDays().stream() + .mapToInt(d -> d.getDuration() != null ? d.getDuration() : 0) + .sum(); + } + return 0; + } + + private static boolean isCrownFixedDateWithCourtScheduleId(final HearingListingNeeds hearing) { + if (nonNull(hearing.getWeekCommencingDate())) { + return false; + } + return !isEmpty(hearing.getHearingDays()) + && hearing.getHearingDays().stream().anyMatch(d -> nonNull(d.getCourtScheduleId())); } @@ -377,10 +975,11 @@ protected HearingSlotSearchResponse searchAndBookSlots(final String hearingId, return null; } final String bookedHearingId = responseJson.getString(HEARING_ID); - final String bookedCourtScheduleId = responseJson.getString("courtScheduleId"); + final String bookedCourtScheduleId = responseJson.getString(COURT_SCHEDULE_ID); final String bookedCourtRoomId = responseJson.getString(COURT_ROOM_ID); final String bookedSessionStartTime = responseJson.getString(HEARING_START_TIME); - final Integer duration = responseJson.getInt("duration"); + final Integer duration = responseJson.getInt(DURATION); + final Boolean isDraft = responseJson.containsKey(IS_DRAFT) ? responseJson.getBoolean(IS_DRAFT) : false; // Extract judiciaries if present List judiciaries = new ArrayList<>(); @@ -395,7 +994,7 @@ protected HearingSlotSearchResponse searchAndBookSlots(final String hearingId, } } - return new HearingSlotSearchResponse(bookedHearingId, bookedCourtScheduleId, bookedCourtRoomId, bookedSessionStartTime, duration, judiciaries); + return new HearingSlotSearchResponse(bookedHearingId, bookedCourtScheduleId, bookedCourtRoomId, bookedSessionStartTime, duration, judiciaries, isDraft); } String responsePayload = ""; @@ -442,7 +1041,7 @@ private HearingSlotSearchResponse getFirstAvailableSlot(final UpdateHearingForLi } final JsonObject firstSlot = slotsArray.getJsonObject(0); - final String courtScheduleId = firstSlot.getString("courtScheduleId"); + final String courtScheduleId = firstSlot.getString(COURT_SCHEDULE_ID); final String courtRoomId = firstSlot.getString(COURT_ROOM_ID); final String sessionStartTime = firstSlot.getString("sessionStartTime"); @@ -459,7 +1058,9 @@ private HearingSlotSearchResponse getFirstAvailableSlot(final UpdateHearingForLi } } - return new HearingSlotSearchResponse(null, courtScheduleId, courtRoomId, sessionStartTime, hearingDay.getDurationMinutes(), judiciaries); + final Boolean isDraft = firstSlot.containsKey(IS_DRAFT) && firstSlot.getBoolean(IS_DRAFT); + + return new HearingSlotSearchResponse(null, courtScheduleId, courtRoomId, sessionStartTime, hearingDay.getDurationMinutes(), judiciaries, isDraft); } else { String responsePayload = ""; if (searchResponse.hasEntity()) { @@ -486,6 +1087,7 @@ private HearingDay populateHearingDaysByHearingSlotSearch(final HearingDay heari .withStartTime(startTime) .withDurationMinutes(duration) .withEndTime(endTime) + .withIsDraft(hearingSlotSearchResponse.isDraft()) .build(); } @@ -560,16 +1162,24 @@ static List populateBookedSlots(final List bookedSlots, fina List newlyPopulatedRotaSlots = new ArrayList<>(); for (RotaSlot listUpdateHearing : bookedSlots) { for (HearingDay hearingDay : hearingDays) { - if (listUpdateHearing.getCourtCentreId().equals(hearingDay.getCourtCentreId().toString()) && - listUpdateHearing.getRoomId().equals(hearingDay.getCourtRoomId().toString()) && - listUpdateHearing.getStartTime().isEqual(hearingDay.getStartTime()) && - listUpdateHearing.getDuration().equals(hearingDay.getDurationMinutes())) { - newlyPopulatedRotaSlots.add(RotaSlot.rotaSlot() + final boolean centreMatch = nonNull(listUpdateHearing.getCourtCentreId()) && nonNull(hearingDay.getCourtCentreId()) + && listUpdateHearing.getCourtCentreId().equals(hearingDay.getCourtCentreId().toString()); + final boolean roomMatch = isNull(listUpdateHearing.getRoomId()) || isNull(hearingDay.getCourtRoomId()) + || listUpdateHearing.getRoomId().equals(hearingDay.getCourtRoomId().toString()); + final boolean timeMatch = nonNull(listUpdateHearing.getStartTime()) && nonNull(hearingDay.getStartTime()) + && listUpdateHearing.getStartTime().isEqual(hearingDay.getStartTime()); + final boolean durationMatch = nonNull(listUpdateHearing.getDuration()) && nonNull(hearingDay.getDurationMinutes()) + && listUpdateHearing.getDuration().equals(hearingDay.getDurationMinutes()); + if (centreMatch && roomMatch && timeMatch && durationMatch) { + RotaSlot.Builder slotBuilder = RotaSlot.rotaSlot() .withValuesFrom(listUpdateHearing) - .withCourtScheduleId(hearingDay.getCourtScheduleId().toString()) + .withCourtScheduleId(nonNull(hearingDay.getCourtScheduleId()) ? hearingDay.getCourtScheduleId().toString() : listUpdateHearing.getCourtScheduleId()) .withStartTime(hearingDay.getStartTime()) - .withDuration(hearingDay.getDurationMinutes()) - .build()); + .withDuration(hearingDay.getDurationMinutes()); + if (nonNull(hearingDay.getCourtRoomId())) { + slotBuilder.withRoomId(hearingDay.getCourtRoomId().toString()); + } + newlyPopulatedRotaSlots.add(slotBuilder.build()); } } } @@ -624,10 +1234,10 @@ private EnrichmentResult handleBookedSlotsCase(final HearingListingNeeds hearing private JudicialRole buildJudicialRoleFromJson(final JsonObject judicialRoleJson) { // Extract fields from JSON and map to domain model - UUID judicialId = UUID.fromString(judicialRoleJson.getString("judiciaryId")); + UUID judicialId = UUID.fromString(judicialRoleJson.getString("id")); String judiciaryType = judicialRoleJson.getString("judiciaryType"); - boolean benchChairman = judicialRoleJson.getBoolean("benchChairman", false); - boolean deputy = judicialRoleJson.getBoolean("deputy", false); + boolean benchChairman = judicialRoleJson.getBoolean("isBenchChairman", false); + boolean deputy = judicialRoleJson.getBoolean("isDeputy", false); // Create JudicialRoleType JudicialRoleType roleType = JudicialRoleType.judicialRoleType() diff --git a/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingDaysEnrichmentService.java b/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingDaysEnrichmentService.java index f60e6811c..e4a6112af 100644 --- a/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingDaysEnrichmentService.java +++ b/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingDaysEnrichmentService.java @@ -68,9 +68,14 @@ public HearingListingNeeds enrichHearings(HearingListingNeeds hearing, JsonEnvel builder.withNonDefaultDays(emptyList()); builder.withNonSittingDays(emptyList()); } else if (JurisdictionType.CROWN.equals(hearing.getJurisdictionType())) { - // builder.withNonSittingDays(enrichNonSittingDaysForCrown(hearing)); no nonsittingdays on this journey - // builder.withNonDefaultDays(enrichNonDefaultDaysForCrown(hearing)); no nondefaultdays on this journey - if (isNull(hearing.getWeekCommencingDate())) { + // CROWN flow: + // * If enrichCrownCourtScheduleFirst has populated hearingDays (single-day with one session, + // multi-day with N sessions each carrying its own courtScheduleId + hearingDate) we leave + // them untouched — overwriting would collapse multi-day expansion back to one day. + // * If hearingDays is still empty (allocation candidate, no courtScheduleId anywhere) we fall + // back to the pre-existing enrichment that builds hearingDays from bookedSlots / nonDefaultDays + // / candidate so the downstream allocation-candidate path can search-and-book. + if (isNull(hearing.getWeekCommencingDate()) && isEmpty(hearing.getHearingDays())) { builder.withHearingDays(enrichHearingDaysForCrown(hearing)); } } @@ -231,17 +236,7 @@ private List calculateHearingDaysForUpdate(final UpdateHearingForLis .findFirst(); if (matchingNonDefaultDay.isPresent()) { - // Use the non-default day attributes - NonDefaultDay nonDefaultDay = matchingNonDefaultDay.get(); - HearingDay.Builder hdbuilder = HearingDay.hearingDay() - .withHearingDate(nonDefaultDay.getStartTime().toLocalDate()) - .withCourtCentreId(fromString(nonDefaultDay.getCourtCentreId())) - .withStartTime(nonDefaultDay.getStartTime()) - .withDurationMinutes(nonDefaultDay.getDuration()); - if (nonNull(nonDefaultDay.getRoomId())) { - hdbuilder.withCourtRoomId(fromString(nonDefaultDay.getRoomId())); - } - hearingDays.add(hdbuilder.build()); + hearingDays.add(createHearingDayFromNonDefaultDay(matchingNonDefaultDay.get())); } else { // Use default court hours for this date HearingDay hearingDay = createDefaultHearingDay(currentDate, courtCentreIdForDefaultDays, courtRoomIdForDefaultDays, defaultStartTime); @@ -276,6 +271,22 @@ private HearingDay createDefaultHearingDay(LocalDate currentDate, UUID courtCent } } + private static HearingDay createHearingDayFromNonDefaultDay(final NonDefaultDay nonDefaultDay) { + HearingDay.Builder hdbuilder = HearingDay.hearingDay() + .withHearingDate(nonDefaultDay.getStartTime().toLocalDate()) + .withCourtCentreId(fromString(nonDefaultDay.getCourtCentreId())) + .withStartTime(nonDefaultDay.getStartTime()) + .withDurationMinutes(nonDefaultDay.getDuration()) + .withEndTime(nonDefaultDay.getStartTime().plusMinutes(nonDefaultDay.getDuration())); + if (nonNull(nonDefaultDay.getRoomId())) { + hdbuilder.withCourtRoomId(fromString(nonDefaultDay.getRoomId())); + } + if (nonNull(nonDefaultDay.getCourtScheduleId())) { + hdbuilder.withCourtScheduleId(fromString(nonDefaultDay.getCourtScheduleId())); + } + return hdbuilder.build(); + } + private static LocalTime getDefaultStartTime(CourtCentreDetails courtCentreDetails) { LocalTime defaultStartTime = LocalTime.of(10, 0); // Default fallback if (courtCentreDetails != null && courtCentreDetails.getDefaultStartTime() != null) { @@ -373,7 +384,8 @@ private List convertNonDefaultDaysToHearingDays(List hdbuilder.withHearingDate(nonDefaultDay.getStartTime().toLocalDate()) .withCourtCentreId(fromString(nonDefaultDay.getCourtCentreId())) .withDurationMinutes(nonDefaultDay.getDuration()) - .withStartTime(nonDefaultDay.getStartTime()); + .withStartTime(nonDefaultDay.getStartTime()) + .withEndTime(nonDefaultDay.getStartTime().plusMinutes(nonDefaultDay.getDuration())); if (nonNull(nonDefaultDay.getRoomId())) { hdbuilder.withCourtRoomId(fromString(nonDefaultDay.getRoomId())); } @@ -403,6 +415,10 @@ private void enrichByHearingDaysIfPresent(UpdateHearingForListing hearing, Updat private void enrichCandidate(HearingListingNeeds hearing, HearingListingNeeds.Builder builder) { if (noHearingDaysPopulatedonPriorSteps(builder)) { final ZonedDateTime startTime = nonNull(hearing.getListedStartDateTime()) ? hearing.getListedStartDateTime() : hearing.getEarliestStartDateTime(); + if (isNull(startTime)) { + LOGGER.warn("Cannot enrich hearing days for hearing {}: both listedStartDateTime and earliestStartDateTime are null", hearing.getId()); + return; + } LOGGER.info("enriching HearingDays by By AllocationCandidate hearingid: {}", hearing.getId()); builder.withHearingDays(List.of(HearingDay.hearingDay() .withHearingDate(startTime.toLocalDate()) @@ -410,6 +426,7 @@ private void enrichCandidate(HearingListingNeeds hearing, HearingListingNeeds.Bu .withCourtCentreId(hearing.getCourtCentre().getId()) .withCourtRoomId(hearing.getCourtCentre().getRoomId()) .withDurationMinutes(hearing.getEstimatedMinutes()) + .withEndTime(nonNull(hearing.getEstimatedMinutes()) ? startTime.plusMinutes(hearing.getEstimatedMinutes()) : null) .build())); } } @@ -418,10 +435,6 @@ static boolean noHearingDaysPopulatedonPriorSteps(final HearingListingNeeds.Buil return isEmpty(builder.build().getHearingDays()); } - private List enrichNonDefaultDaysForCrown(HearingListingNeeds hearingListingNeeds) { - return null; - } - private List enrichNonDefaultDaysForCrown(UpdateHearingForListing updateHearingForListing, List nonSittingDays) { List validNonDefaultDays = getValidNonDefaultDays(updateHearingForListing.getNonDefaultDays(), updateHearingForListing.getStartDate(), updateHearingForListing.getEndDate(), nonSittingDays); return enrichValidNonDefaultDays(updateHearingForListing, validNonDefaultDays); @@ -456,16 +469,13 @@ static UUID getCourtRoomId(final UpdateHearingForListing updateHearingForListing return nonNull(updateHearingForListing.getSelectedCourtCentre()) ? updateHearingForListing.getSelectedCourtCentre().getCourtRoomId() : updateHearingForListing.getCourtRoomId(); } - private List enrichNonSittingDaysForCrown(HearingListingNeeds hearingListingNeeds) { - return null; - } - private List enrichNonSittingDaysForCrown(UpdateHearingForListing updateHearingForListing) { return isNotEmpty(updateHearingForListing.getNonSittingDays()) ? updateHearingForListing.getNonSittingDays() : emptyList(); } private List enrichHearingDaysForCrown(HearingListingNeeds hearingListingNeeds) { HearingListingNeeds.Builder builder = HearingListingNeeds.hearingListingNeeds().withValuesFrom(hearingListingNeeds); + enrichByBookedSlotsIfPresent(hearingListingNeeds, builder); enrichByNonDefaultDaysIfPresent(hearingListingNeeds, builder); enrichCandidate(hearingListingNeeds, builder); return builder.build().getHearingDays(); diff --git a/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestrator.java b/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestrator.java index bee842d04..f851fa698 100644 --- a/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestrator.java +++ b/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestrator.java @@ -54,10 +54,22 @@ public List enrichListCourtHearing(List Duration - HearingListingNeeds withHearingDays = hearingDaysEnrichmentService.enrichHearings(hearing, envelope); - HearingListingNeeds withDurations = hearingDurationEnrichmentService.enrichWithDurations(withHearingDays, envelope); - enrichedHearings.add(withDurations); + if (hasCourtScheduleId(hearing)) { + // CROWN with courtScheduleId (bookedSlots or hearingDays): CourtSchedule-first flow + // expands multi-day into N hearingDays, then HearingDays computes start/end, + // then Duration runs. + HearingListingNeeds withCourtSchedules = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + HearingListingNeeds withHearingDays = hearingDaysEnrichmentService.enrichHearings(withCourtSchedules, envelope); + HearingListingNeeds withDurations = hearingDurationEnrichmentService.enrichWithDurations(withHearingDays, envelope); + enrichedHearings.add(withDurations); + } else { + // CROWN allocation candidate (no courtScheduleId anywhere): legacy HearingDays -> + // Duration -> CourtSchedule order so handleAllocationCandidate can search-and-book. + HearingListingNeeds withHearingDays = hearingDaysEnrichmentService.enrichHearings(hearing, envelope); + HearingListingNeeds withDurations = hearingDurationEnrichmentService.enrichWithDurations(withHearingDays, envelope); + HearingListingNeeds withCourtSchedules = courtScheduleEnrichmentService.enrichWithCourtSchedules(withDurations, envelope); + enrichedHearings.add(withCourtSchedules); + } } else { throw new IllegalArgumentException(UNSUPPORTED_JURISDICTION_TYPE + hearing.getJurisdictionType()); } @@ -78,10 +90,10 @@ public UpdateHearingForListing enrichUpdateHearingForListing(UpdateHearingForLis enrichedHearing = courtScheduleEnrichmentService.enrichWithCourtSchedules(withDuration,envelope); } else if (JurisdictionType.CROWN.equals(jurisdictionType)) { LOGGER.info("Enrich update hearing for CROWN hearingid: {}", hearing.getHearingId()); - // For crown: Hearing Days -> Duration + // For crown: Hearing Days -> Duration -> Court Schedule UpdateHearingForListing withHearingDays = hearingDaysEnrichmentService.enrichHearing(hearing, envelope); - enrichedHearing = hearingDurationEnrichmentService.enrichWithDurationForUpdate(withHearingDays, envelope); - //enrichedHearing = hearingDurationEnrichmentService.enrichWithDurationForUpdate(withDuration, envelope); + UpdateHearingForListing withDuration = hearingDurationEnrichmentService.enrichWithDurationForUpdate(withHearingDays, envelope); + enrichedHearing = courtScheduleEnrichmentService.enrichWithCourtSchedules(withDuration, envelope); } else { throw new IllegalArgumentException(UNSUPPORTED_JURISDICTION_TYPE + jurisdictionType); } @@ -101,10 +113,10 @@ public UpdateHearingForListing enrichUpdateHearingForListing(UpdateHearingForLis enrichedHearing = courtScheduleEnrichmentService.enrichWithCourtSchedules(withDuration,envelope); } else if (JurisdictionType.CROWN.equals(jurisdictionType)) { LOGGER.info("Enrich update hearing for CROWN hearingid: {}", hearing.getHearingId()); - // For crown: Hearing Days -> Duration + // For crown: Hearing Days -> Duration -> Court Schedule UpdateHearingForListing withHearingDays = hearingDaysEnrichmentService.enrichHearing(hearing, envelope, courtCentreDetails); - enrichedHearing = hearingDurationEnrichmentService.enrichWithDurationForUpdate(withHearingDays, envelope); - //enrichedHearing = hearingDurationEnrichmentService.enrichWithDurationForUpdate(withDuration, envelope); + UpdateHearingForListing withDuration = hearingDurationEnrichmentService.enrichWithDurationForUpdate(withHearingDays, envelope); + enrichedHearing = courtScheduleEnrichmentService.enrichWithCourtSchedules(withDuration, envelope); } else { throw new IllegalArgumentException(UNSUPPORTED_JURISDICTION_TYPE + jurisdictionType); } @@ -265,4 +277,19 @@ static int getTotalDuration(final List sequencedHearingDays) { hearingDay.getDurationMinutes() : DEFAULT_MIN) .sum(); } + + /** + * Returns true if the hearing has a courtScheduleId on any hearingDay or on any bookedSlot. + * Used by the CROWN branch to decide between CourtSchedule-first flow (has id) and the legacy + * allocation-candidate flow (no id, needs search-and-book). + */ + private static boolean hasCourtScheduleId(final HearingListingNeeds hearing) { + if (hearing.getHearingDays() != null + && hearing.getHearingDays().stream().anyMatch(d -> d.getCourtScheduleId() != null)) { + return true; + } + return hearing.getBookedSlots() != null + && hearing.getBookedSlots().stream() + .anyMatch(s -> s.getCourtScheduleId() != null && !s.getCourtScheduleId().isBlank()); + } } \ No newline at end of file diff --git a/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/util/NonDefaultDayConverter.java b/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/util/NonDefaultDayConverter.java index 0b4697da9..5628f2e7e 100644 --- a/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/util/NonDefaultDayConverter.java +++ b/listing-command/listing-command-api/src/main/java/uk/gov/moj/cpp/listing/command/api/util/NonDefaultDayConverter.java @@ -37,7 +37,8 @@ public static List convertCoreNonDefaultDaysToHearingDays(final List .withCourtRoomId(UUID.fromString(nonDefaultDay.getRoomId())) .withDurationMinutes(nonDefaultDay.getDuration()) .withStartTime(nonDefaultDay.getStartTime()) - .withHearingDate(nonDefaultDay.getStartTime().toLocalDate()); + .withHearingDate(nonDefaultDay.getStartTime().toLocalDate()) + .withEndTime(nonDefaultDay.getStartTime().plusMinutes(nonDefaultDay.getDuration())); if (nonNull(nonDefaultDay.getCourtScheduleId())) { builder.withCourtScheduleId(UUID.fromString(nonDefaultDay.getCourtScheduleId())); } @@ -49,14 +50,17 @@ public static List convertCoreNonDefaultDaysToHearingDays(final List public static List convertBookedSlotsToHearingDays(final List bookedSlots) { List hearingDayList = new ArrayList<>(); for (RotaSlot slot : bookedSlots) { - hearingDayList.add(HearingDay.hearingDay() + HearingDay.Builder builder = HearingDay.hearingDay() .withCourtCentreId(UUID.fromString(slot.getCourtCentreId())) - .withCourtRoomId(UUID.fromString(slot.getRoomId())) .withCourtScheduleId(UUID.fromString(slot.getCourtScheduleId())) .withDurationMinutes(slot.getDuration()) .withHearingDate(slot.getStartTime().toLocalDate()) .withStartTime(slot.getStartTime()) - .build()); + .withEndTime(slot.getStartTime().plusMinutes(slot.getDuration())); + if (nonNull(slot.getRoomId())) { + builder.withCourtRoomId(UUID.fromString(slot.getRoomId())); + } + hearingDayList.add(builder.build()); } return hearingDayList; } diff --git a/listing-command/listing-command-api/src/raml/json/schema/listing.command.restrict-court-list.json b/listing-command/listing-command-api/src/raml/json/schema/listing.command.restrict-court-list.json index 1c60d8ef5..4db9d5f51 100644 --- a/listing-command/listing-command-api/src/raml/json/schema/listing.command.restrict-court-list.json +++ b/listing-command/listing-command-api/src/raml/json/schema/listing.command.restrict-court-list.json @@ -40,13 +40,6 @@ "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" } }, - "courtApplicationSubjectIds": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" - } - }, "courtApplicationIds": { "type": "array", "minItems": 1, @@ -97,11 +90,6 @@ "courtApplicationIds", "courtApplicationType" ] - }, - { - "required": [ - "courtApplicationSubjectIds" - ] } ], "required": [ diff --git a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/ListingCommandApiTest.java b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/ListingCommandApiTest.java index cd85ad3df..679b43e03 100644 --- a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/ListingCommandApiTest.java +++ b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/ListingCommandApiTest.java @@ -370,9 +370,9 @@ public void shouldUpdateHearingForListingWithProsecutionCases() { .map(JsonObject.class::cast) .forEach(judiciaryJsonObject -> judicialRoles.add(JudicialRole.judicialRole() - .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("benchChairman"))) - .withIsDeputy(of(judiciaryJsonObject.getBoolean("deputy"))) - .withJudicialId(UUID.fromString(judiciaryJsonObject.getString("judiciaryId"))) + .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("isBenchChairman"))) + .withIsDeputy(of(judiciaryJsonObject.getBoolean("isDeputy"))) + .withJudicialId(UUID.fromString(judiciaryJsonObject.getString("id"))) .withJudicialRoleType( JudicialRoleType.judicialRoleType() .withJudiciaryType(judiciaryJsonObject.getString("judiciaryType")) @@ -421,9 +421,9 @@ public void shouldUpdateHearingForListing() { .map(JsonObject.class::cast) .forEach(judiciaryJsonObject -> judicialRoles.add(JudicialRole.judicialRole() - .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("benchChairman"))) - .withIsDeputy(of(judiciaryJsonObject.getBoolean("deputy"))) - .withJudicialId(UUID.fromString(judiciaryJsonObject.getString("judiciaryId"))) + .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("isBenchChairman"))) + .withIsDeputy(of(judiciaryJsonObject.getBoolean("isDeputy"))) + .withJudicialId(UUID.fromString(judiciaryJsonObject.getString("id"))) .withJudicialRoleType( JudicialRoleType.judicialRoleType() .withJudiciaryType(judiciaryJsonObject.getString("judiciaryType")) @@ -787,9 +787,9 @@ public void shouldCallNonDefaultDayDurationBuilder() { .map(JsonObject.class::cast) .forEach(judiciaryJsonObject -> judicialRoles.add(JudicialRole.judicialRole() - .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("benchChairman"))) - .withIsDeputy(of(judiciaryJsonObject.getBoolean("deputy"))) - .withJudicialId(UUID.fromString(judiciaryJsonObject.getString("judiciaryId"))) + .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("isBenchChairman"))) + .withIsDeputy(of(judiciaryJsonObject.getBoolean("isDeputy"))) + .withJudicialId(UUID.fromString(judiciaryJsonObject.getString("id"))) .withJudicialRoleType( JudicialRoleType.judicialRoleType() .withJudiciaryType(judiciaryJsonObject.getString("judiciaryType")) diff --git a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentServiceTest.java b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentServiceTest.java index 78161facb..3012dfeb4 100644 --- a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentServiceTest.java +++ b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/CourtScheduleEnrichmentServiceTest.java @@ -1,27 +1,48 @@ package uk.gov.moj.cpp.listing.command.api.service; -import uk.gov.justice.services.messaging.JsonObjects; - import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static uk.gov.moj.cpp.listing.command.api.util.FileUtil.givenPayload; +import uk.gov.justice.core.courts.CourtCentre; +import uk.gov.justice.core.courts.JurisdictionType; +import uk.gov.justice.core.courts.RotaSlot; +import uk.gov.justice.core.courts.WeekCommencingDate; +import uk.gov.justice.listing.commands.HearingDay; +import uk.gov.justice.listing.commands.HearingListingNeeds; +import uk.gov.justice.listing.commands.NonDefaultDay; +import uk.gov.justice.listing.commands.UpdateHearingForListing; import uk.gov.justice.listing.courts.SelectedCourtCentre; import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter; import uk.gov.justice.services.common.converter.ObjectToJsonObjectConverter; +import uk.gov.justice.services.messaging.JsonEnvelope; +import uk.gov.justice.services.messaging.JsonObjects; import uk.gov.moj.cpp.listing.command.api.util.SlotsToJsonStringConverter; import uk.gov.moj.cpp.listing.common.service.HearingSlotsService; +import uk.gov.moj.cpp.listing.domain.CourtSchedule; import uk.gov.moj.cpp.listing.domain.HearingSlotSearchResponse; +import uk.gov.moj.cpp.listing.domain.ListUpdateHearing; import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.UUID; -import javax.inject.Inject; import javax.json.JsonObject; import javax.ws.rs.core.Response; @@ -74,221 +95,3267 @@ void searchAndBookShouldReturnBookedHearingSlots() { @Test void enrichShouldAddMultiDayParamsOnSearch() { - // Arrange: two hearing days -> isMultiDay = true final UUID hearingId = UUID.randomUUID(); final UUID courtRoomId = UUID.randomUUID(); final LocalDate day1 = LocalDate.now(); final LocalDate day2 = day1.plusDays(1); - final uk.gov.justice.listing.commands.HearingDay hd1 = - uk.gov.justice.listing.commands.HearingDay.hearingDay() - .withCourtRoomId(courtRoomId) - .withHearingDate(day1) - .withStartTime(java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC)) - .withDurationMinutes(30) - .build(); - - final uk.gov.justice.listing.commands.HearingDay hd2 = - uk.gov.justice.listing.commands.HearingDay.hearingDay() - .withCourtRoomId(courtRoomId) - .withHearingDate(day2) - .withStartTime(java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC).plusHours(1)) - .withDurationMinutes(30) - .build(); - - // Build a minimal UpdateHearingForListing with 2 days - final uk.gov.justice.listing.commands.UpdateHearingForListing update = - uk.gov.justice.listing.commands.UpdateHearingForListing.updateHearingForListing() - .withHearingId(hearingId) - // ensure ouCode resolution does not hit CourtCentreFactory - .withSelectedCourtCentre( - SelectedCourtCentre.selectedCourtCentre() - .withOuCode("OU123") - .build()) - .withCourtRoomId(courtRoomId) - .withStartDate(day1) // non-null, not strictly used here but safe - .withHearingDays(java.util.Arrays.asList(hd1, hd2)) - .build(); - - // Mock search response (first available slot). Shape must match getFirstAvailableSlot() - final String bookedCourtScheduleId = java.util.UUID.randomUUID().toString(); - final javax.json.JsonObject searchJson = - JsonObjects.createObjectBuilder() - .add("hearingSlots", JsonObjects.createArrayBuilder() - .add(JsonObjects.createObjectBuilder() - .add("courtScheduleId", bookedCourtScheduleId) - .add("courtRoomId", courtRoomId.toString()) - .add("sessionStartTime", "2020-01-01T09:00:00Z"))) - .build(); - - final Response searchResponse = org.mockito.Mockito.mock(Response.class); - when(searchResponse.getStatus()).thenReturn(org.apache.http.HttpStatus.SC_OK); + final HearingDay hd1 = HearingDay.hearingDay() + .withCourtRoomId(courtRoomId) + .withHearingDate(day1) + .withStartTime(ZonedDateTime.now(ZoneOffset.UTC)) + .withDurationMinutes(30) + .build(); + + final HearingDay hd2 = HearingDay.hearingDay() + .withCourtRoomId(courtRoomId) + .withHearingDate(day2) + .withStartTime(ZonedDateTime.now(ZoneOffset.UTC).plusHours(1)) + .withDurationMinutes(30) + .build(); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withSelectedCourtCentre(SelectedCourtCentre.selectedCourtCentre().withOuCode("OU123").build()) + .withCourtRoomId(courtRoomId) + .withStartDate(day1) + .withHearingDays(Arrays.asList(hd1, hd2)) + .build(); + + final String bookedCourtScheduleId = UUID.randomUUID().toString(); + final JsonObject searchJson = JsonObjects.createObjectBuilder() + .add("hearingSlots", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", bookedCourtScheduleId) + .add("courtRoomId", courtRoomId.toString()) + .add("sessionStartTime", "2020-01-01T09:00:00Z"))) + .build(); + + final Response searchResponse = mock(Response.class); + when(searchResponse.getStatus()).thenReturn(HttpStatus.SC_OK); when(searchResponse.getEntity()).thenReturn(searchJson); when(hearingSlotsService.search(anyMap())).thenReturn(searchResponse); when(objectToJsonObjectConverter.convert(searchJson)).thenReturn(searchJson); - // Stub SlotsToJsonStringConverter so getUpdateSlotsPayload() never sees nulls - when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(org.mockito.ArgumentMatchers.anyList())) - .thenReturn(JsonObjects.createArrayBuilder() - .add(bookedCourtScheduleId) - .build()); + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(bookedCourtScheduleId).build()); - // Mock list response used by combineSearchAndBookResponseAndListResponse() - final javax.json.JsonObject listJson = - JsonObjects.createObjectBuilder() - .add("hearings", JsonObjects.createArrayBuilder() - .add(JsonObjects.createObjectBuilder() - .add("courtScheduleId", bookedCourtScheduleId) - .add("hearingStartTime", "2020-01-01T09:00:00Z") - .add("duration", 30))) - .build(); - - final Response listResponse = org.mockito.Mockito.mock(Response.class); - when(listResponse.getStatus()).thenReturn(org.apache.http.HttpStatus.SC_OK); + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", bookedCourtScheduleId) + .add("hearingStartTime", "2020-01-01T09:00:00Z") + .add("duration", 30))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); when(listResponse.getEntity()).thenReturn(listJson); - when(hearingSlotsService.listHearingInCourtSessions(any(javax.json.JsonObject.class))).thenReturn(listResponse); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); - // jsonObjectConverter must translate each "hearings" item to ListUpdateHearing (POJO with setters) - when(jsonObjectConverter.convert( - org.mockito.ArgumentMatchers.any(javax.json.JsonObject.class), - org.mockito.ArgumentMatchers.eq(uk.gov.moj.cpp.listing.domain.ListUpdateHearing.class))) + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) .thenAnswer(inv -> { - javax.json.JsonObject jo = inv.getArgument(0); - uk.gov.moj.cpp.listing.domain.ListUpdateHearing luh = new uk.gov.moj.cpp.listing.domain.ListUpdateHearing(); + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); luh.setCourtScheduleId(jo.getString("courtScheduleId")); luh.setHearingStartTime(jo.getString("hearingStartTime")); luh.setDuration(jo.getInt("duration")); return luh; }); - // Capture the search query maps for both days @SuppressWarnings("unchecked") final org.mockito.ArgumentCaptor> mapCaptor = org.mockito.ArgumentCaptor.forClass(java.util.Map.class); - // Act - courtScheduleEnrichmentService.enrichWithCourtSchedules( - update, - org.mockito.Mockito.mock(uk.gov.justice.services.messaging.JsonEnvelope.class)); + courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); - // Assert: search() called twice (two days) and includes multi-day params verify(hearingSlotsService, times(2)).search(mapCaptor.capture()); - // Each captured map must contain the multi-day flags for (java.util.Map qp : mapCaptor.getAllValues()) { - org.hamcrest.MatcherAssert.assertThat(qp.get("courtSession"), is("AD")); - org.hamcrest.MatcherAssert.assertThat(qp.get("showOverbookedSlots"), is(Boolean.TRUE.toString())); - org.hamcrest.MatcherAssert.assertThat(qp.get("isSlotBased"), is(Boolean.FALSE.toString())); + assertThat(qp.get("courtSession"), is("AD")); + assertThat(qp.get("showOverbookedSlots"), is(Boolean.TRUE.toString())); + assertThat(qp.get("isSlotBased"), is(Boolean.FALSE.toString())); } } @Test void enrichShouldNotIncludeStartTimeForMultiDaySearch() { - // Arrange: two hearing days -> isMultiDay = true final UUID hearingId = UUID.randomUUID(); final UUID courtRoomId = UUID.randomUUID(); final LocalDate day1 = LocalDate.now(); final LocalDate day2 = day1.plusDays(1); - final uk.gov.justice.listing.commands.HearingDay hd1 = - uk.gov.justice.listing.commands.HearingDay.hearingDay() - .withCourtRoomId(courtRoomId) - .withHearingDate(day1) - .withStartTime(java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC)) - .withDurationMinutes(30) - .build(); - - final uk.gov.justice.listing.commands.HearingDay hd2 = - uk.gov.justice.listing.commands.HearingDay.hearingDay() - .withCourtRoomId(courtRoomId) - .withHearingDate(day2) - .withStartTime(java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC).plusHours(1)) - .withDurationMinutes(30) - .build(); - - final uk.gov.justice.listing.commands.UpdateHearingForListing update = - uk.gov.justice.listing.commands.UpdateHearingForListing.updateHearingForListing() - .withHearingId(hearingId) - .withSelectedCourtCentre( - uk.gov.justice.listing.courts.SelectedCourtCentre.selectedCourtCentre() - .withOuCode("OU123") - .build()) - .withCourtRoomId(courtRoomId) - .withStartDate(day1) - .withHearingDays(java.util.Arrays.asList(hd1, hd2)) - .build(); - - // Mock search response (first available slot) - final String bookedCourtScheduleId = java.util.UUID.randomUUID().toString(); - final javax.json.JsonObject searchJson = - JsonObjects.createObjectBuilder() - .add("hearingSlots", JsonObjects.createArrayBuilder() - .add(JsonObjects.createObjectBuilder() - .add("courtScheduleId", bookedCourtScheduleId) - .add("courtRoomId", courtRoomId.toString()) - .add("sessionStartTime", "2020-01-01T09:00:00Z"))) - .build(); - - final Response searchResponse = org.mockito.Mockito.mock(Response.class); - when(searchResponse.getStatus()).thenReturn(org.apache.http.HttpStatus.SC_OK); + final HearingDay hd1 = HearingDay.hearingDay() + .withCourtRoomId(courtRoomId) + .withHearingDate(day1) + .withStartTime(ZonedDateTime.now(ZoneOffset.UTC)) + .withDurationMinutes(30) + .build(); + + final HearingDay hd2 = HearingDay.hearingDay() + .withCourtRoomId(courtRoomId) + .withHearingDate(day2) + .withStartTime(ZonedDateTime.now(ZoneOffset.UTC).plusHours(1)) + .withDurationMinutes(30) + .build(); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withSelectedCourtCentre(SelectedCourtCentre.selectedCourtCentre().withOuCode("OU123").build()) + .withCourtRoomId(courtRoomId) + .withStartDate(day1) + .withHearingDays(Arrays.asList(hd1, hd2)) + .build(); + + final String bookedCourtScheduleId = UUID.randomUUID().toString(); + final JsonObject searchJson = JsonObjects.createObjectBuilder() + .add("hearingSlots", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", bookedCourtScheduleId) + .add("courtRoomId", courtRoomId.toString()) + .add("sessionStartTime", "2020-01-01T09:00:00Z"))) + .build(); + + final Response searchResponse = mock(Response.class); + when(searchResponse.getStatus()).thenReturn(HttpStatus.SC_OK); when(searchResponse.getEntity()).thenReturn(searchJson); when(hearingSlotsService.search(anyMap())).thenReturn(searchResponse); when(objectToJsonObjectConverter.convert(searchJson)).thenReturn(searchJson); - // Ensure payload building doesn't see nulls - when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(org.mockito.ArgumentMatchers.anyList())) + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) .thenReturn(JsonObjects.createArrayBuilder().add(bookedCourtScheduleId).build()); - // Mock list response used by combineSearchAndBookResponseAndListResponse() - final javax.json.JsonObject listJson = - JsonObjects.createObjectBuilder() - .add("hearings", JsonObjects.createArrayBuilder() - .add(JsonObjects.createObjectBuilder() - .add("courtScheduleId", bookedCourtScheduleId) - .add("hearingStartTime", "2020-01-01T09:00:00Z") - .add("duration", 30))) - .build(); - - final Response listResponse = org.mockito.Mockito.mock(Response.class); - when(listResponse.getStatus()).thenReturn(org.apache.http.HttpStatus.SC_OK); + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", bookedCourtScheduleId) + .add("hearingStartTime", "2020-01-01T09:00:00Z") + .add("duration", 30))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); when(listResponse.getEntity()).thenReturn(listJson); - when(hearingSlotsService.listHearingInCourtSessions(any(javax.json.JsonObject.class))).thenReturn(listResponse); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); - // Map each "hearings" item to ListUpdateHearing - when(jsonObjectConverter.convert( - org.mockito.ArgumentMatchers.any(javax.json.JsonObject.class), - org.mockito.ArgumentMatchers.eq(uk.gov.moj.cpp.listing.domain.ListUpdateHearing.class))) + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) .thenAnswer(inv -> { - javax.json.JsonObject jo = inv.getArgument(0); - uk.gov.moj.cpp.listing.domain.ListUpdateHearing luh = new uk.gov.moj.cpp.listing.domain.ListUpdateHearing(); + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); luh.setCourtScheduleId(jo.getString("courtScheduleId")); luh.setHearingStartTime(jo.getString("hearingStartTime")); luh.setDuration(jo.getInt("duration")); return luh; }); - // Capture the search query maps for both days @SuppressWarnings("unchecked") final org.mockito.ArgumentCaptor> mapCaptor = org.mockito.ArgumentCaptor.forClass(java.util.Map.class); - // Act - courtScheduleEnrichmentService.enrichWithCourtSchedules( - update, - org.mockito.Mockito.mock(uk.gov.justice.services.messaging.JsonEnvelope.class)); + courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); - // Assert: search() called twice and multi-day flags present… verify(hearingSlotsService, times(2)).search(mapCaptor.capture()); for (java.util.Map qp : mapCaptor.getAllValues()) { - // multi-day flags - org.hamcrest.MatcherAssert.assertThat(qp.get("courtSession"), is("AD")); - org.hamcrest.MatcherAssert.assertThat(qp.get("isSlotBased"), is(Boolean.FALSE.toString())); - // …and hearingStartTime MUST NOT be present - org.hamcrest.MatcherAssert.assertThat("hearingStartTime should not be sent for multi-day search", + assertThat(qp.get("courtSession"), is("AD")); + assertThat(qp.get("isSlotBased"), is(Boolean.FALSE.toString())); + assertThat("hearingStartTime should not be sent for multi-day search", qp.containsKey(CourtScheduleEnrichmentService.HEARING_START_TIME), is(false)); } } -} \ No newline at end of file + // ─── CROWN needsCourtScheduleEnrichment tests ──────────────────────── + + @Test + void shouldReturnTrueForCrownFixedDateWithCourtScheduleId() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(UUID.randomUUID()) + .withHearingDate(LocalDate.now()) + .withDurationMinutes(240) + .build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(true)); + } + + @Test + void shouldReturnFalseForCrownWeekCommencing() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withWeekCommencingDate(WeekCommencingDate.weekCommencingDate() + .withStartDate(LocalDate.now().toString()) + .withDuration(1) + .build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(UUID.randomUUID()) + .withHearingDate(LocalDate.now()) + .withDurationMinutes(240) + .build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(false)); + } + + @Test + void shouldReturnFalseForCrownWithNoCourtScheduleIds() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now()) + .withDurationMinutes(240) + .build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(false)); + } + + @Test + void shouldReturnFalseForCrownWithEmptyHearingDays() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.emptyList()) + .build(); + + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(false)); + } + + @Test + void shouldNeedEnrichmentForCrownAllocationCandidateWithoutCourtScheduleIds() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withListedStartDateTime(ZonedDateTime.now()) + .withCourtCentre(CourtCentre.courtCentre().withId(UUID.randomUUID()).withRoomId(UUID.randomUUID()).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now()) + .withDurationMinutes(240) + .build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(true)); + } + + @Test + void shouldNotNeedEnrichmentForCrownWeekCommencingEvenIfAllocationCandidate() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withListedStartDateTime(ZonedDateTime.now()) + .withCourtCentre(CourtCentre.courtCentre().withId(UUID.randomUUID()).withRoomId(UUID.randomUUID()).build()) + .withWeekCommencingDate(WeekCommencingDate.weekCommencingDate() + .withStartDate(LocalDate.now().toString()) + .withDuration(1) + .build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now()) + .withDurationMinutes(240) + .build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(false)); + } + + // ─── CROWN single-day enrichment tests ─────────────────────────────── + + @Test + void shouldEnrichCrownSingleDayWithNonDraftSession() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(240) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(sessionDate) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds + final CourtSchedule cs = new CourtSchedule(); + cs.setCourtScheduleId(courtScheduleId.toString()); + cs.setSessionDate(sessionDate); + cs.setCourtRoomId(courtRoomId.toString()); + cs.setCourtHouseId(courtHouseId.toString()); + cs.setDraft(false); + cs.setHearingStartTime("2026-03-16T10:00:00Z"); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("isDraft", false))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("hearingStartTime", "2026-03-16T10:00:00Z") + .add("duration", 240))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + // listHearingInCourtSessions should be called for non-draft sessions + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + assertThat(result.getHearingDays().size(), is(1)); + assertThat(result.getHearingDays().get(0).getCourtScheduleId().toString(), is(courtScheduleId.toString())); + } + + @Test + void shouldNotCallListHearingWhenCrownSingleDaySessionIsDraft() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(240) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(sessionDate) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds - isDraft=true + final CourtSchedule cs = new CourtSchedule(); + cs.setCourtScheduleId(courtScheduleId.toString()); + cs.setSessionDate(sessionDate); + cs.setCourtRoomId(courtRoomId.toString()); + cs.setCourtHouseId(courtHouseId.toString()); + cs.setDraft(true); + cs.setHearingStartTime("2026-03-16T10:00:00Z"); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("isDraft", true))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions (now always called, even for draft) + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("hearingStartTime", "2026-03-16T10:00:00Z") + .add("duration", 240))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + // listHearingInCourtSessions IS now called for draft sessions (for slot deduction) + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + // HearingDays should carry isDraft=true from sanity check + assertThat(result.getHearingDays().size(), is(1)); + assertThat(result.getHearingDays().get(0).getIsDraft(), is(true)); + // courtRoomId should be null for draft sessions + assertThat(result.getHearingDays().get(0).getCourtRoomId(), is((UUID) null)); + } + + @Test + void shouldSkipCrownEnrichmentWhenWeekCommencing() { + final UUID hearingId = UUID.randomUUID(); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withWeekCommencingDate(WeekCommencingDate.weekCommencingDate() + .withStartDate(LocalDate.now().toString()) + .withDuration(1) + .build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(UUID.randomUUID()) + .withHearingDate(LocalDate.now()) + .withDurationMinutes(240) + .build())) + .build(); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + // No court scheduler calls should be made + verify(hearingSlotsService, never()).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + verify(hearingSlotsService, never()).multiDaySearchAndBook(anyMap()); + // Hearing should be returned unchanged + assertThat(result.getId(), is(hearingId)); + } + + @Test + void shouldSearchAndBookForCrownListWithoutCourtScheduleIdsWhenAllocationCandidate() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtCentreId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withListedStartDateTime(ZonedDateTime.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(5).toString()) + .withCourtCentre(CourtCentre.courtCentre().withId(courtCentreId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now().plusDays(5)) + .withStartTime(ZonedDateTime.now().plusDays(5).withHour(10).withMinute(0)) + .withDurationMinutes(240) + .build())) + .withEstimatedMinutes(240) + .build(); + + final JsonObject searchBookResponse = givenPayload("/courtscheduler.search.book.hearing.slots.json"); + when(hearingSlotsService.searchBookSlots(anyMap())).thenReturn(response); + when(response.getStatus()).thenReturn(HttpStatus.SC_OK); + when(objectToJsonObjectConverter.convert(any())).thenReturn(searchBookResponse); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + // searchAndBook should be called for Crown without courtScheduleIds + verify(hearingSlotsService).searchBookSlots(anyMap()); + assertThat(result.getHearingDays().size(), is(1)); + assertThat(result.getHearingDays().get(0).getCourtScheduleId().toString(), is("23681024-8eac-4890-8c44-4651ad48cb24")); + assertThat(result.getHearingDays().get(0).getIsDraft(), is(false)); + } + + @Test + void shouldSetIsDraftTrueOnHearingDaysWhenSearchAndBookReturnsDraftForCrownAllocationCandidate() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtCentreId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withListedStartDateTime(ZonedDateTime.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(5).toString()) + .withCourtCentre(CourtCentre.courtCentre().withId(courtCentreId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now().plusDays(5)) + .withStartTime(ZonedDateTime.now().plusDays(5).withHour(10).withMinute(0)) + .withDurationMinutes(240) + .build())) + .withEstimatedMinutes(240) + .build(); + + final JsonObject searchBookResponse = givenPayload("/courtscheduler.search.book.hearing.slots.draft.json"); + when(hearingSlotsService.searchBookSlots(anyMap())).thenReturn(response); + when(response.getStatus()).thenReturn(HttpStatus.SC_OK); + when(objectToJsonObjectConverter.convert(any())).thenReturn(searchBookResponse); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + verify(hearingSlotsService).searchBookSlots(anyMap()); + assertThat(result.getHearingDays().size(), is(1)); + assertThat(result.getHearingDays().get(0).getCourtScheduleId().toString(), is("23681024-8eac-4890-8c44-4651ad48cb24")); + assertThat(result.getHearingDays().get(0).getIsDraft(), is(true)); + } + + // ─── CROWN multi-day enrichment tests ──────────────────────────────── + + @Test + void shouldEnrichCrownMultiDayWithConsecutiveSessions() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId1 = UUID.randomUUID(); + final UUID courtScheduleId2 = UUID.randomUUID(); + final UUID courtScheduleId3 = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate day1 = LocalDate.now().plusDays(5); + + // Multi-day: estimatedMinutes > 360. Anchor lives on bookedSlots (new contract). + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(1080) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId1) + .withHearingDate(day1) + .withDurationMinutes(1080) + .build())) + .withBookedSlots(Collections.singletonList( + RotaSlot.rotaSlot() + .withCourtScheduleId(courtScheduleId1.toString()) + .withCourtCentreId(courtHouseId.toString()) + .withRoomId(courtRoomId.toString()) + .build())) + .build(); + + // Mock multiDaySearchAndBook + final CourtSchedule cs1 = buildCourtSchedule(courtScheduleId1, courtRoomId, courtHouseId, day1, false); + final CourtSchedule cs2 = buildCourtSchedule(courtScheduleId2, courtRoomId, courtHouseId, day1.plusDays(1), false); + final CourtSchedule cs3 = buildCourtSchedule(courtScheduleId3, courtRoomId, courtHouseId, day1.plusDays(2), false); + + final JsonObject multiDayResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(buildCsJson(cs1)) + .add(buildCsJson(cs2)) + .add(buildCsJson(cs3))) + .build(); + + final Response multiDayResponse = mock(Response.class); + when(multiDayResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(multiDayResponse); + when(objectToJsonObjectConverter.convert(multiDayResponse.getEntity())).thenReturn(multiDayResponseJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))) + .thenReturn(cs1, cs2, cs3); + + // Mock listHearingInCourtSessions for booking + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId1, "2026-03-16T10:00:00Z", 360)) + .add(buildListHearingJson(courtScheduleId2, "2026-03-17T10:00:00Z", 360)) + .add(buildListHearingJson(courtScheduleId3, "2026-03-18T10:00:00Z", 360))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder() + .add(courtScheduleId1.toString()) + .add(courtScheduleId2.toString()) + .add(courtScheduleId3.toString()) + .build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + verify(hearingSlotsService).multiDaySearchAndBook(anyMap()); + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + assertThat(result.getHearingDays().size(), is(3)); + } + + @Test + void shouldReturnUnchangedWhenMultiDaySearchReturnsEmpty() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(1080) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(LocalDate.now().plusDays(5)) + .withDurationMinutes(1080) + .build())) + .withBookedSlots(Collections.singletonList( + RotaSlot.rotaSlot() + .withCourtScheduleId(courtScheduleId.toString()) + .withCourtCentreId(courtHouseId.toString()) + .withRoomId(courtRoomId.toString()) + .build())) + .build(); + + // Mock multiDaySearchAndBook returning empty + final JsonObject emptyResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder()) + .build(); + + final Response multiDayResponse = mock(Response.class); + when(multiDayResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(multiDayResponse); + when(objectToJsonObjectConverter.convert(multiDayResponse.getEntity())).thenReturn(emptyResponseJson); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + // Original hearingDays should be preserved + assertThat(result.getHearingDays().size(), is(1)); + } + + @Test + void shouldNotCallListHearingWhenMultiDaySessionsHaveDraft() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId1 = UUID.randomUUID(); + final UUID courtScheduleId2 = UUID.randomUUID(); + final UUID courtScheduleId3 = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate day1 = LocalDate.now().plusDays(5); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(1080) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId1) + .withHearingDate(day1) + .withDurationMinutes(1080) + .build())) + .withBookedSlots(Collections.singletonList( + RotaSlot.rotaSlot() + .withCourtScheduleId(courtScheduleId1.toString()) + .withCourtCentreId(courtHouseId.toString()) + .withRoomId(courtRoomId.toString()) + .build())) + .build(); + + // Mock multiDaySearchAndBook - one session is draft + final CourtSchedule cs1 = buildCourtSchedule(courtScheduleId1, courtRoomId, courtHouseId, day1, false); + final CourtSchedule cs2 = buildCourtSchedule(courtScheduleId2, courtRoomId, courtHouseId, day1.plusDays(1), true); // draft! + final CourtSchedule cs3 = buildCourtSchedule(courtScheduleId3, courtRoomId, courtHouseId, day1.plusDays(2), false); + + final JsonObject multiDayResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(buildCsJson(cs1)) + .add(buildCsJson(cs2)) + .add(buildCsJson(cs3))) + .build(); + + final Response multiDayResponse = mock(Response.class); + when(multiDayResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(multiDayResponse); + when(objectToJsonObjectConverter.convert(multiDayResponse.getEntity())).thenReturn(multiDayResponseJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))) + .thenReturn(cs1, cs2, cs3); + + // Mock listHearingInCourtSessions (now always called, even when some sessions are draft) + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId1, day1 + "T10:00:00Z", 360)) + .add(buildListHearingJson(courtScheduleId2, day1.plusDays(1) + "T10:00:00Z", 360)) + .add(buildListHearingJson(courtScheduleId3, day1.plusDays(2) + "T10:00:00Z", 360))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder() + .add(courtScheduleId1.toString()) + .add(courtScheduleId2.toString()) + .add(courtScheduleId3.toString()) + .build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + // listHearingInCourtSessions IS now called even when some sessions are draft + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + // Expanded hearingDays should be returned with isDraft from sessions + assertThat(result.getHearingDays().size(), is(3)); + } + + // ─── CROWN update hearing enrichment tests ─────────────────────────── + + @Test + void shouldSkipCrownUpdateEnrichmentWhenNoCourtScheduleIds() { + final UUID hearingId = UUID.randomUUID(); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now()) + .withDurationMinutes(240) + .build())) + .build(); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + // No court scheduler calls + verify(hearingSlotsService, never()).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + assertThat(result.getHearingId(), is(hearingId)); + } + + @Test + void shouldSearchAndBookForCrownUpdateWhenAllocationCandidateWithoutCourtScheduleIds() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtCentreId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withCourtCentreId(courtCentreId) + .withCourtRoomId(courtRoomId) + .withStartDate(LocalDate.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(5)) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now().plusDays(5)) + .withDurationMinutes(240) + .withCourtRoomId(courtRoomId) + .withCourtCentreId(courtCentreId) + .build())) + .build(); + + final JsonObject searchBookResponse = givenPayload("/courtscheduler.search.book.hearing.slots.json"); + when(hearingSlotsService.searchBookSlots(anyMap())).thenReturn(response); + when(response.getStatus()).thenReturn(HttpStatus.SC_OK); + when(objectToJsonObjectConverter.convert(any())).thenReturn(searchBookResponse); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + verify(hearingSlotsService).searchBookSlots(anyMap()); + // listHearingInCourtSessions should NOT be called — update searchAndBook returns directly + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + assertThat(result.getHearingDays().size(), is(1)); + // courtScheduleId should be set from searchAndBook response + assertThat(result.getHearingDays().get(0).getCourtScheduleId().toString(), is("23681024-8eac-4890-8c44-4651ad48cb24")); + // courtRoomId should be PRESERVED from the original hearing day, not overwritten by searchAndBook + assertThat(result.getHearingDays().get(0).getCourtRoomId(), is(courtRoomId)); + } + + @Test + void shouldReturnUnchangedWhenCrownUpdateSearchAndBookReturnsEmpty() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtCentreId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withCourtCentreId(courtCentreId) + .withCourtRoomId(courtRoomId) + .withStartDate(LocalDate.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(5)) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now().plusDays(5)) + .withDurationMinutes(240) + .build())) + .build(); + + // searchAndBook returns empty response + when(hearingSlotsService.searchBookSlots(anyMap())).thenReturn(response); + when(response.getStatus()).thenReturn(HttpStatus.SC_OK); + // Return empty hearingSlots object + when(objectToJsonObjectConverter.convert(any())).thenReturn( + javax.json.Json.createObjectBuilder().add("hearingSlots", javax.json.Json.createObjectBuilder()).build()); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + verify(hearingSlotsService).searchBookSlots(anyMap()); + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + // Should return unchanged hearing when searchAndBook returns empty + assertThat(result.getHearingId(), is(hearingId)); + assertThat(result.getHearingDays().get(0).getCourtScheduleId(), is(org.hamcrest.CoreMatchers.nullValue())); + } + + @Test + void shouldEnrichCrownUpdateSingleDay() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(sessionDate) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds - non-draft + final CourtSchedule cs = new CourtSchedule(); + cs.setCourtScheduleId(courtScheduleId.toString()); + cs.setSessionDate(sessionDate); + cs.setCourtRoomId(courtRoomId.toString()); + cs.setCourtHouseId(courtHouseId.toString()); + cs.setDraft(false); + cs.setHearingStartTime("2026-03-16T10:00:00Z"); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId, "2026-03-16T10:00:00Z", 240))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + verify(hearingSlotsService).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + assertThat(result.getHearingDays().size(), is(1)); + } + + @Test + void shouldSetIsDraftOnCrownUpdateWhenDraftSessions() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(sessionDate) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds - isDraft=true + final CourtSchedule cs = new CourtSchedule(); + cs.setCourtScheduleId(courtScheduleId.toString()); + cs.setSessionDate(sessionDate); + cs.setCourtRoomId(courtRoomId.toString()); + cs.setCourtHouseId(courtHouseId.toString()); + cs.setDraft(true); + cs.setHearingStartTime("2026-03-16T10:00:00Z"); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions (now always called, even for draft) + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("hearingStartTime", "2026-03-16T10:00:00Z") + .add("duration", 240))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + assertThat(result.getHearingDays().size(), is(1)); + assertThat(result.getHearingDays().get(0).getIsDraft(), is(true)); + // courtRoomId should be null for draft sessions + assertThat(result.getHearingDays().get(0).getCourtRoomId(), is((UUID) null)); + } + + // ─── Sanity check tests ────────────────────────────────────────────── + + @Test + void shouldUseSchedulerDateWhenHearingDateMismatches() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate hearingDate = LocalDate.of(2026, 3, 16); + final LocalDate schedulerDate = LocalDate.of(2026, 3, 17); // different! + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(240) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(hearingDate) + .withDurationMinutes(240) + .build())) + .build(); + + // Session has different date than hearingDay + final CourtSchedule cs = new CourtSchedule(); + cs.setCourtScheduleId(courtScheduleId.toString()); + cs.setSessionDate(schedulerDate); // scheduler wins + cs.setCourtRoomId(courtRoomId.toString()); + cs.setCourtHouseId(courtHouseId.toString()); + cs.setDraft(true); // draft + cs.setHearingStartTime("2026-03-17T10:00:00Z"); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions (now always called, even for draft) + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("hearingStartTime", "2026-03-17T10:00:00Z") + .add("duration", 240))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + // Scheduler date should win + assertThat(result.getHearingDays().get(0).getHearingDate(), is(schedulerDate)); + } + + // ─── CROWN update multi-day enrichment tests ─────────────────────── + + @Test + void shouldEnrichCrownUpdateMultiDay() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId1 = UUID.randomUUID(); + final UUID courtScheduleId2 = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate day1 = LocalDate.now().plusDays(5); + + // Multi-day: totalDuration > 360 + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId1) + .withHearingDate(day1) + .withDurationMinutes(360) + .build(), + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId2) + .withHearingDate(day1.plusDays(1)) + .withDurationMinutes(360) + .build())) + .build(); + + // Mock multiDaySearchAndBook + final CourtSchedule cs1 = buildCourtSchedule(courtScheduleId1, courtRoomId, courtHouseId, day1, false); + final CourtSchedule cs2 = buildCourtSchedule(courtScheduleId2, courtRoomId, courtHouseId, day1.plusDays(1), false); + + final JsonObject multiDayResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(buildCsJson(cs1)) + .add(buildCsJson(cs2))) + .build(); + + final Response multiDayResponse = mock(Response.class); + when(multiDayResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(multiDayResponse); + when(objectToJsonObjectConverter.convert(multiDayResponse.getEntity())).thenReturn(multiDayResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))) + .thenReturn(cs1, cs2); + + // Mock listHearingInCourtSessions + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId1, "2026-03-16T10:00:00Z", 360)) + .add(buildListHearingJson(courtScheduleId2, "2026-03-17T10:00:00Z", 360))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder() + .add(courtScheduleId1.toString()) + .add(courtScheduleId2.toString()) + .build()); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + verify(hearingSlotsService).multiDaySearchAndBook(anyMap()); + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + assertThat(result.getHearingDays().size(), is(2)); + } + + @Test + void shouldReturnUnchangedWhenCrownUpdateMultiDaySearchReturnsEmpty() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(LocalDate.now().plusDays(5)) + .withDurationMinutes(720) + .build())) + .build(); + + // Mock multiDaySearchAndBook returning empty + final JsonObject emptyResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder()) + .build(); + + final Response multiDayResponse = mock(Response.class); + when(multiDayResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(multiDayResponse); + when(objectToJsonObjectConverter.convert(multiDayResponse.getEntity())).thenReturn(emptyResponseJson); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + assertThat(result.getHearingId(), is(hearingId)); + } + + @Test + void shouldNotCallListHearingWhenCrownUpdateMultiDaySessionsHaveDraft() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId1 = UUID.randomUUID(); + final UUID courtScheduleId2 = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate day1 = LocalDate.now().plusDays(5); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId1) + .withHearingDate(day1) + .withDurationMinutes(720) + .build())) + .build(); + + // Mock multiDaySearchAndBook - one session is draft + final CourtSchedule cs1 = buildCourtSchedule(courtScheduleId1, courtRoomId, courtHouseId, day1, false); + final CourtSchedule cs2 = buildCourtSchedule(courtScheduleId2, courtRoomId, courtHouseId, day1.plusDays(1), true); + + final JsonObject multiDayResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(buildCsJson(cs1)) + .add(buildCsJson(cs2))) + .build(); + + final Response multiDayResponse = mock(Response.class); + when(multiDayResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(multiDayResponse); + when(objectToJsonObjectConverter.convert(multiDayResponse.getEntity())).thenReturn(multiDayResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))) + .thenReturn(cs1, cs2); + + // Mock listHearingInCourtSessions (now always called, even when some sessions are draft) + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId1, day1 + "T10:00:00Z", 360)) + .add(buildListHearingJson(courtScheduleId2, day1.plusDays(1) + "T10:00:00Z", 360))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder() + .add(courtScheduleId1.toString()) + .add(courtScheduleId2.toString()) + .build()); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + assertThat(result.getHearingDays().size(), is(2)); + } + + @Test + void shouldSkipCrownUpdateWhenHearingDaysEmpty() { + final UUID hearingId = UUID.randomUUID(); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.emptyList()) + .build(); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + verify(hearingSlotsService, never()).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService, never()).multiDaySearchAndBook(anyMap()); + assertThat(result.getHearingId(), is(hearingId)); + } + + @Test + void shouldKeepExistingJudiciaryOnCrownUpdate() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final UUID judicialId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final uk.gov.justice.core.courts.JudicialRole existingJudiciary = uk.gov.justice.core.courts.JudicialRole.judicialRole() + .withJudicialId(judicialId) + .build(); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withJudiciary(Collections.singletonList(existingJudiciary)) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(sessionDate) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds - non-draft + final CourtSchedule cs = buildCourtSchedule(courtScheduleId, courtRoomId, courtHouseId, sessionDate, false); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(buildCsJson(cs))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId, "2026-03-16T10:00:00Z", 240))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + // Existing judiciary should be preserved + assertThat(result.getJudiciary().size(), is(1)); + assertThat(result.getJudiciary().get(0).getJudicialId(), is(judicialId)); + } + + // ─── fetchCourtSchedulesByIds error paths ──────────────────────────── + + @Test + void shouldReturnEmptyWhenFetchCourtSchedulesByIdsFailsResponse() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(240) + .withCourtCentre(CourtCentre.courtCentre().withId(UUID.randomUUID()).withRoomId(UUID.randomUUID()).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(LocalDate.now().plusDays(5)) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds returning error status + final Response errorResponse = mock(Response.class); + when(errorResponse.getStatus()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(errorResponse); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + // Should return unchanged since sessions is empty + assertThat(result.getHearingDays().size(), is(1)); + } + + @Test + void shouldReturnEmptyWhenFetchCourtSchedulesByIdsReturnsNullResponse() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(240) + .withCourtCentre(CourtCentre.courtCentre().withId(UUID.randomUUID()).withRoomId(UUID.randomUUID()).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(LocalDate.now().plusDays(5)) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds returning null JSON + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(null); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + assertThat(result.getHearingDays().size(), is(1)); + } + + // ─── multiDaySearchAndBook error paths ─────────────────────────────── + + @Test + void shouldReturnUnchangedWhenMultiDaySearchAndBookFailsResponse() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(1080) + .withCourtCentre(CourtCentre.courtCentre().withId(UUID.randomUUID()).withRoomId(UUID.randomUUID()).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(LocalDate.now().plusDays(5)) + .withDurationMinutes(1080) + .build())) + .withBookedSlots(Collections.singletonList( + RotaSlot.rotaSlot() + .withCourtScheduleId(courtScheduleId.toString()) + .build())) + .build(); + + // Mock multiDaySearchAndBook returning error status + final Response errorResponse = mock(Response.class); + when(errorResponse.getStatus()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(errorResponse); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + assertThat(result.getHearingDays().size(), is(1)); + } + + @Test + void shouldReturnUnchangedWhenMultiDaySearchAndBookReturnsNullJson() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(1080) + .withCourtCentre(CourtCentre.courtCentre().withId(UUID.randomUUID()).withRoomId(UUID.randomUUID()).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(LocalDate.now().plusDays(5)) + .withDurationMinutes(1080) + .build())) + .withBookedSlots(Collections.singletonList( + RotaSlot.rotaSlot() + .withCourtScheduleId(courtScheduleId.toString()) + .build())) + .build(); + + // Mock multiDaySearchAndBook returning null json + final Response multiDayResponse = mock(Response.class); + when(multiDayResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(multiDayResponse); + when(objectToJsonObjectConverter.convert(multiDayResponse.getEntity())).thenReturn(null); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + assertThat(result.getHearingDays().size(), is(1)); + } + + // ─── needsCourtScheduleEnrichment static tests ─────────────────────── + + @Test + void shouldNeedEnrichmentForMagistratesWithBookingReference() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withId(UUID.randomUUID()) + .withBookingReference(UUID.randomUUID()) + .build(); + + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(true)); + } + + @Test + void shouldNotNeedEnrichmentForCrownWithoutCourtScheduleIds() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(UUID.randomUUID()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now()) + .withDurationMinutes(240) + .build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(false)); + } + + @Test + void shouldNotNeedEnrichmentForUnknownJurisdiction() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withId(UUID.randomUUID()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(UUID.randomUUID()) + .withHearingDate(LocalDate.now()) + .build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(false)); + } + + @Test + void shouldNeedEnrichmentForCrownWithCourtScheduleIds() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(UUID.randomUUID()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(UUID.randomUUID()) + .withHearingDate(LocalDate.now()) + .withDurationMinutes(240) + .build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(true)); + } + + // ─── isCandidateForAllocation tests ────────────────────────────────── + + @Test + void shouldBeAllocationCandidateWithStartDateTimeAndCourtRoom() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withId(UUID.randomUUID()) + .withListedStartDateTime(ZonedDateTime.now()) + .withCourtCentre(CourtCentre.courtCentre().withId(UUID.randomUUID()).withRoomId(UUID.randomUUID()).build()) + .build(); + + assertThat(CourtScheduleEnrichmentService.isCandidateForAllocation(hearing), is(true)); + } + + @Test + void shouldNotBeAllocationCandidateWithoutStartDateTime() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withId(UUID.randomUUID()) + .withCourtCentre(CourtCentre.courtCentre().withId(UUID.randomUUID()).withRoomId(UUID.randomUUID()).build()) + .build(); + + assertThat(CourtScheduleEnrichmentService.isCandidateForAllocation(hearing), is(false)); + } + + @Test + void shouldBeUpdateAllocationCandidateWithStartDateAndCourtRoom() { + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(UUID.randomUUID()) + .withJurisdictionType(JurisdictionType.CROWN) + .withStartDate(LocalDate.now()) + .withCourtRoomId(UUID.randomUUID()) + .build(); + + assertThat(CourtScheduleEnrichmentService.isCandidateForAllocation(update), is(true)); + } + + @Test + void shouldNotBeUpdateAllocationCandidateWithoutCourtRoom() { + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(UUID.randomUUID()) + .withJurisdictionType(JurisdictionType.CROWN) + .withStartDate(LocalDate.now()) + .build(); + + assertThat(CourtScheduleEnrichmentService.isCandidateForAllocation(update), is(false)); + } + + // ─── sanityCheckAndEnrichCrown edge cases ──────────────────────────── + + @Test + void shouldPassThroughHearingDayWithNoCourtScheduleId() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtScheduleId2 = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + // One hearingDay with courtScheduleId matching a fetched session, one with a different courtScheduleId (no matching session) + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(240) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(sessionDate) + .withDurationMinutes(120) + .build(), + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId2) + .withHearingDate(sessionDate.plusDays(1)) + .withDurationMinutes(120) + .build())) + .build(); + + final CourtSchedule cs = buildCourtSchedule(courtScheduleId, courtRoomId, courtHouseId, sessionDate, true); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(buildCsJson(cs))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions (now always called, even for draft) + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("hearingStartTime", sessionDate + "T10:00:00Z") + .add("duration", 120)) + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId2.toString()) + .add("hearingStartTime", sessionDate.plusDays(1) + "T10:00:00Z") + .add("duration", 120))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder() + .add(courtScheduleId.toString()) + .add(courtScheduleId2.toString()) + .build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + // Both days should be present; one enriched from session, one passed through (no matching session in fetched list) + assertThat(result.getHearingDays().size(), is(2)); + } + + @Test + void searchAndBookShouldReturnBookedHearingSlotsWithJudiciaries() { + final String hearingId = "5416c10a-0cf1-49d5-a7c9-5761ff3bdf2c"; + final String ouCode = "OU12345"; + final String hearingSessionDate = LocalDate.now().toString(); + final String courtRoomId = UUID.randomUUID().toString(); + final String hearingSessionDateSearchCutOff = LocalDate.now().plusDays(7).toString(); + final String sessionStartTime = LocalDate.now().toString(); + final Integer durationInMinutes = 20; + + final JsonObject searchBookResponse = givenPayload("/courtscheduler.search.book.hearing.slots.with.judiciaries.json"); + + when(hearingSlotsService.searchBookSlots(anyMap())).thenReturn(response); + when(response.getStatus()).thenReturn(HttpStatus.SC_OK); + when(response.getEntity()).thenReturn(searchBookResponse); + when(objectToJsonObjectConverter.convert(any())).thenReturn(searchBookResponse); + + final HearingSlotSearchResponse hearingSlotSearchResponse = courtScheduleEnrichmentService + .searchAndBookSlots(hearingId, ouCode, hearingSessionDate, courtRoomId, hearingSessionDateSearchCutOff, sessionStartTime, durationInMinutes, true); + + assertThat(hearingSlotSearchResponse.courtScheduleId(), is("23681024-8eac-4890-8c44-4651ad48cb24")); + assertThat(hearingSlotSearchResponse.judiciaries().size(), is(2)); + assertThat(hearingSlotSearchResponse.judiciaries().get(0).getJudicialId().toString(), is("a1b2c3d4-e5f6-7890-abcd-ef1234567890")); + assertThat(hearingSlotSearchResponse.judiciaries().get(0).getJudicialRoleType().getJudiciaryType(), is("CIRCUIT_JUDGE")); + assertThat(hearingSlotSearchResponse.judiciaries().get(0).getIsBenchChairman().orElse(false), is(true)); + assertThat(hearingSlotSearchResponse.judiciaries().get(0).getIsDeputy().orElse(true), is(false)); + assertThat(hearingSlotSearchResponse.judiciaries().get(1).getJudicialId().toString(), is("b2c3d4e5-f6a7-8901-bcde-f12345678901")); + assertThat(hearingSlotSearchResponse.judiciaries().get(1).getJudicialRoleType().getJudiciaryType(), is("RECORDER")); + assertThat(hearingSlotSearchResponse.judiciaries().get(1).getIsDeputy().orElse(false), is(true)); + } + + // ─── MAGISTRATES update hearing judiciary enrichment tests ───────── + + @Test + void shouldPreserveMagistratesExistingJudiciaryOnUpdateHearing() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID judicialId = UUID.randomUUID(); + + final uk.gov.justice.core.courts.JudicialRole existingJudiciary = uk.gov.justice.core.courts.JudicialRole.judicialRole() + .withJudicialId(judicialId) + .build(); + + final HearingDay hearingDay = HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withCourtRoomId(courtRoomId) + .withHearingDate(LocalDate.now().plusDays(3)) + .withStartTime(ZonedDateTime.now(ZoneOffset.UTC)) + .withDurationMinutes(30) + .build(); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withJudiciary(Collections.singletonList(existingJudiciary)) + .withSelectedCourtCentre(SelectedCourtCentre.selectedCourtCentre().withOuCode("OU123").build()) + .withCourtRoomId(courtRoomId) + .withStartDate(LocalDate.now().plusDays(3)) + .withHearingDays(Collections.singletonList(hearingDay)) + .build(); + + // Mock slotsToJsonStringConverter + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + // Mock listHearingInCourtSessions — the response has NO judiciaries + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("hearingStartTime", "2026-03-16T10:00:00Z") + .add("duration", 30))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + // Existing judiciary should be preserved even though response has none + assertThat(result.getJudiciary().size(), is(1)); + assertThat(result.getJudiciary().get(0).getJudicialId(), is(judicialId)); + } + + @Test + void shouldEnrichMagistratesUpdateHearingWithJudiciaryFromListResponse() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID judicialId = UUID.fromString("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + + final HearingDay hearingDay = HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withCourtRoomId(courtRoomId) + .withHearingDate(LocalDate.now().plusDays(3)) + .withStartTime(ZonedDateTime.now(ZoneOffset.UTC)) + .withDurationMinutes(30) + .build(); + + // No existing judiciary on the update + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withSelectedCourtCentre(SelectedCourtCentre.selectedCourtCentre().withOuCode("OU123").build()) + .withCourtRoomId(courtRoomId) + .withStartDate(LocalDate.now().plusDays(3)) + .withHearingDays(Collections.singletonList(hearingDay)) + .build(); + + // Mock slotsToJsonStringConverter + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + // Mock listHearingInCourtSessions — response includes judiciaries + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("hearingStartTime", "2026-03-16T10:00:00Z") + .add("duration", 30) + .add("judiciaries", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("id", judicialId.toString()) + .add("judiciaryType", "MAGISTRATE") + .add("isBenchChairman", true) + .add("isDeputy", false))))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + // Judiciary should be populated from the list response, converted from domain to core model + assertThat(result.getJudiciary().size(), is(1)); + assertThat(result.getJudiciary().get(0).getJudicialId(), is(judicialId)); + assertThat(result.getJudiciary().get(0).getJudicialRoleType().getJudiciaryType(), is("MAGISTRATE")); + assertThat(result.getJudiciary().get(0).getIsBenchChairman(), is(true)); + assertThat(result.getJudiciary().get(0).getIsDeputy(), is(false)); + } + + @Test + void shouldNotSetJudiciaryWhenNeitherExistingNorEnrichedPresent() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + + final HearingDay hearingDay = HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withCourtRoomId(courtRoomId) + .withHearingDate(LocalDate.now().plusDays(3)) + .withStartTime(ZonedDateTime.now(ZoneOffset.UTC)) + .withDurationMinutes(30) + .build(); + + // No existing judiciary on the update + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withSelectedCourtCentre(SelectedCourtCentre.selectedCourtCentre().withOuCode("OU123").build()) + .withCourtRoomId(courtRoomId) + .withStartDate(LocalDate.now().plusDays(3)) + .withHearingDays(Collections.singletonList(hearingDay)) + .build(); + + // Mock slotsToJsonStringConverter + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + // Mock listHearingInCourtSessions — no judiciaries in response + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("hearingStartTime", "2026-03-16T10:00:00Z") + .add("duration", 30))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + // No judiciary should be set — neither existing nor enriched + assertThat(result.getJudiciary() == null || result.getJudiciary().isEmpty(), is(true)); + } + + @Test + void shouldReturnUnchangedWhenCrownMultiDayHasNoCourtScheduleIdOnAnyDay() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(720) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withHearingDate(sessionDate) + .withDurationMinutes(360) + .build(), + HearingDay.hearingDay() + .withHearingDate(sessionDate.plusDays(1)) + .withDurationMinutes(360) + .build())) + .build(); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + assertThat(result.getHearingDays().size(), is(2)); + verify(hearingSlotsService, never()).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + verify(hearingSlotsService, never()).multiDaySearchAndBook(any()); + } + + // ─── getFirstAvailableSlot error paths ──────────────────────────────── + + @Test + void shouldThrowWhenGetFirstAvailableSlotSearchFails() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final LocalDate hearingDate = LocalDate.now().plusDays(5); + + // HearingDay with NULL courtScheduleId triggers getFirstAvailableSlot + final HearingDay hearingDay = HearingDay.hearingDay() + .withHearingDate(hearingDate) + .withDurationMinutes(120) + .withStartTime(hearingDate.atTime(10, 0).atZone(ZoneOffset.UTC)) + .withCourtRoomId(courtRoomId) + .build(); + + final UpdateHearingForListing updateHearing = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withSelectedCourtCentre(SelectedCourtCentre.selectedCourtCentre().withOuCode("OU123").build()) + .withHearingDays(Collections.singletonList(hearingDay)) + .build(); + + final Response failedResponse = mock(Response.class); + when(failedResponse.getStatus()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); + when(failedResponse.hasEntity()).thenReturn(true); + when(failedResponse.getEntity()).thenReturn("Server Error"); + when(hearingSlotsService.search(anyMap())).thenReturn(failedResponse); + + org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class, () -> + courtScheduleEnrichmentService.enrichWithCourtSchedules(updateHearing, mock(JsonEnvelope.class))); + } + + @Test + void shouldThrowWhenGetFirstAvailableSlotReturnsEmptySlots() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final LocalDate hearingDate = LocalDate.now().plusDays(5); + + final HearingDay hearingDay = HearingDay.hearingDay() + .withHearingDate(hearingDate) + .withDurationMinutes(120) + .withStartTime(hearingDate.atTime(10, 0).atZone(ZoneOffset.UTC)) + .withCourtRoomId(courtRoomId) + .build(); + + final UpdateHearingForListing updateHearing = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withSelectedCourtCentre(SelectedCourtCentre.selectedCourtCentre().withOuCode("OU123").build()) + .withHearingDays(Collections.singletonList(hearingDay)) + .build(); + + final JsonObject emptyResponseJson = JsonObjects.createObjectBuilder() + .add("hearingSlots", JsonObjects.createArrayBuilder()) + .build(); + + final Response emptyResponse = mock(Response.class); + when(emptyResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(emptyResponse.getEntity()).thenReturn(emptyResponseJson); + when(hearingSlotsService.search(anyMap())).thenReturn(emptyResponse); + when(objectToJsonObjectConverter.convert(emptyResponseJson)).thenReturn(emptyResponseJson); + + org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class, () -> + courtScheduleEnrichmentService.enrichWithCourtSchedules(updateHearing, mock(JsonEnvelope.class))); + } + + // ─── populateJudiciaryInfoFromSlots edge case tests ───────────────── + + @Test + void shouldReturnNoJudiciaryWhenListResponseFails() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + + // HearingDay with courtScheduleId — bypasses getFirstAvailableSlot + final HearingDay hearingDay = HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withCourtRoomId(courtRoomId) + .withHearingDate(LocalDate.now().plusDays(3)) + .withStartTime(ZonedDateTime.now(ZoneOffset.UTC)) + .withDurationMinutes(30) + .build(); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withSelectedCourtCentre(SelectedCourtCentre.selectedCourtCentre().withOuCode("OU123").build()) + .withCourtRoomId(courtRoomId) + .withStartDate(LocalDate.now().plusDays(3)) + .withHearingDays(Collections.singletonList(hearingDay)) + .build(); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + // Mock listHearingInCourtSessions returning 500 + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + + // combineSearchAndBookResponseAndListResponse throws RuntimeException when response is not success + // so populateJudiciaryInfoFromSlots is never reached — the failed list response causes an exception + org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class, () -> + courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class))); + } + + @Test + void shouldReturnNoJudiciaryWhenResponseHasNoJudiciariesKey() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + + // HearingDay with courtScheduleId — bypasses getFirstAvailableSlot + final HearingDay hearingDay = HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withCourtRoomId(courtRoomId) + .withHearingDate(LocalDate.now().plusDays(3)) + .withStartTime(ZonedDateTime.now(ZoneOffset.UTC)) + .withDurationMinutes(30) + .build(); + + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withSelectedCourtCentre(SelectedCourtCentre.selectedCourtCentre().withOuCode("OU123").build()) + .withCourtRoomId(courtRoomId) + .withStartDate(LocalDate.now().plusDays(3)) + .withHearingDays(Collections.singletonList(hearingDay)) + .build(); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + // Mock listHearingInCourtSessions — response is 200 but hearing has NO "judiciaries" key + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("hearingStartTime", "2026-03-16T10:00:00Z") + .add("duration", 30))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(update, mock(JsonEnvelope.class)); + + // populateJudiciaryInfoFromSlots finds no "judiciaries" key in the hearing object, returns empty list + // No judiciary should be set on the result + assertThat(result.getJudiciary() == null || result.getJudiciary().isEmpty(), is(true)); + } + + // ─── Helper methods ────────────────────────────────────────────────── + + private CourtSchedule buildCourtSchedule(UUID courtScheduleId, UUID courtRoomId, UUID courtHouseId, LocalDate sessionDate, boolean isDraft) { + final CourtSchedule cs = new CourtSchedule(); + cs.setCourtScheduleId(courtScheduleId.toString()); + cs.setCourtRoomId(courtRoomId.toString()); + cs.setCourtHouseId(courtHouseId.toString()); + cs.setSessionDate(sessionDate); + cs.setDraft(isDraft); + cs.setHearingStartTime(sessionDate + "T10:00:00Z"); + return cs; + } + + private JsonObject buildCsJson(CourtSchedule cs) { + return JsonObjects.createObjectBuilder() + .add("courtScheduleId", cs.getCourtScheduleId()) + .add("courtRoomId", cs.getCourtRoomId()) + .add("courtHouseId", cs.getCourtHouseId()) + .add("sessionDate", cs.getSessionDate().toString()) + .add("isDraft", cs.isDraft()) + .build(); + } + + private JsonObject buildListHearingJson(UUID courtScheduleId, String hearingStartTime, int duration) { + return JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("hearingStartTime", hearingStartTime) + .add("duration", duration) + .build(); + } + + // ─── calculateAggregatedDuration tests ─────────────────────────────── + + @Test + void shouldCalculateDurationFromHearingDays() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withEstimatedMinutes(100) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay().withDurationMinutes(360).build(), + HearingDay.hearingDay().withDurationMinutes(360).build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing), is(720)); + } + + @Test + void shouldCalculateDurationFromNonDefaultDaysWhenNoHearingDays() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withEstimatedMinutes(100) + .withNonDefaultDays(Arrays.asList( + new uk.gov.justice.core.courts.NonDefaultDay(null, null, null, 180, null, null, null, null), + new uk.gov.justice.core.courts.NonDefaultDay(null, null, null, 180, null, null, null, null))) + .build(); + + assertThat(CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing), is(360)); + } + + @Test + void shouldFallbackToEstimatedMinutesWhenNoHearingDaysOrNonDefaultDays() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withEstimatedMinutes(240) + .build(); + + assertThat(CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing), is(240)); + } + + @Test + void shouldSumBookedSlotsDurationWhenNoHearingDaysOrNonDefaultDays() { + // Multi-day CROWN adjournment shape: bookedSlots carries the booked window; estimatedMinutes + // may be 0 or a per-offence value. bookedSlots aggregated total wins over estimatedMinutes. + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withEstimatedMinutes(0) + .withBookedSlots(Arrays.asList( + RotaSlot.rotaSlot().withCourtScheduleId(UUID.randomUUID().toString()).withDuration(360).build(), + RotaSlot.rotaSlot().withCourtScheduleId(UUID.randomUUID().toString()).withDuration(360).build(), + RotaSlot.rotaSlot().withCourtScheduleId(UUID.randomUUID().toString()).withDuration(360).build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing), is(1080)); + } + + @Test + void shouldFallBackToEstimatedMinutesWhenBookedSlotsHaveNoDuration() { + // Guard case: bookedSlots present but all durations null -> must not collapse to 0; keep estimatedMinutes. + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withEstimatedMinutes(240) + .withBookedSlots(Collections.singletonList( + RotaSlot.rotaSlot().withCourtScheduleId(UUID.randomUUID().toString()).build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing), is(240)); + } + + @Test + void shouldReturnZeroWhenNoDurationInfoAvailable() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .build(); + + assertThat(CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing), is(0)); + } + + @Test + void shouldCalculateAggregatedDurationForUpdateFromHearingDays() { + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withHearingDays(Arrays.asList( + HearingDay.hearingDay().withDurationMinutes(360).build(), + HearingDay.hearingDay().withDurationMinutes(360).build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.calculateAggregatedDuration(update), is(720)); + } + + @Test + void shouldCalculateAggregatedDurationForUpdateFromNonDefaultDays() { + final UpdateHearingForListing update = UpdateHearingForListing.updateHearingForListing() + .withNonDefaultDays(Collections.singletonList( + NonDefaultDay.nonDefaultDay().withDuration(360).build())) + .build(); + + assertThat(CourtScheduleEnrichmentService.calculateAggregatedDuration(update), is(360)); + } + + // ─── enrichCrownCourtScheduleFirst tests ──────────────────────────── + + @Test + void enrichCrownCourtScheduleFirst_shouldReturnUnchanged_whenNoCourtScheduleIdAnywhere() { + final UUID hearingId = UUID.randomUUID(); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.of(2026, 4, 10)) + .withDurationMinutes(120) + .build())) + .build(); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + assertThat(result, is(hearing)); + verify(hearingSlotsService, never()).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + verify(hearingSlotsService, never()).multiDaySearchAndBook(anyMap()); + } + + @Test + void enrichCrownCourtScheduleFirst_shouldReturnUnchanged_whenNoCourtScheduleIdAndHasWeekCommencing() { + final UUID hearingId = UUID.randomUUID(); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withWeekCommencingDate(WeekCommencingDate.weekCommencingDate() + .withStartDate(LocalDate.now().toString()) + .withDuration(1) + .build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.of(2026, 4, 10)) + .withDurationMinutes(120) + .build())) + .build(); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + assertThat(result, is(hearing)); + verify(hearingSlotsService, never()).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + verify(hearingSlotsService, never()).multiDaySearchAndBook(anyMap()); + } + + @Test + void enrichCrownCourtScheduleFirst_shouldCallSingleDay_whenCourtScheduleIdOnlyOnBookedSlots() { + // When courtScheduleId lives only on bookedSlots (MCC shape), single-day enrichment must still run. + // It fetches sessions by that id and materialises a HearingDay from the returned session. + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(240) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).build()) + .withBookedSlots(Collections.singletonList( + RotaSlot.rotaSlot() + .withCourtScheduleId(courtScheduleId.toString()) + .withCourtCentreId(courtHouseId.toString()) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds returning one non-draft session + final CourtSchedule cs = buildCourtSchedule(courtScheduleId, courtRoomId, courtHouseId, sessionDate, false); + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder().add("courtScheduleId", courtScheduleId.toString()))) + .build(); + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId, "2026-03-16T10:00:00Z", 240))) + .build(); + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + verify(hearingSlotsService).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + verify(hearingSlotsService, never()).multiDaySearchAndBook(anyMap()); + // One HearingDay materialised from the fetched session + assertThat(result.getHearingDays().size(), is(1)); + assertThat(result.getHearingDays().get(0).getCourtScheduleId().toString(), is(courtScheduleId.toString())); + } + + @Test + void enrichCrownCourtScheduleFirst_shouldCallSingleDay_whenCourtScheduleIdPresentAndDurationBelow360() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(240) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(sessionDate) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds + final CourtSchedule cs = buildCourtSchedule(courtScheduleId, courtRoomId, courtHouseId, sessionDate, false); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId, "2026-03-16T10:00:00Z", 240))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + verify(hearingSlotsService).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + verify(hearingSlotsService, never()).multiDaySearchAndBook(anyMap()); + assertThat(result.getHearingDays().size(), is(1)); + assertThat(result.getHearingDays().get(0).getCourtScheduleId().toString(), is(courtScheduleId.toString())); + } + + @Test + void enrichCrownCourtScheduleFirst_shouldCallMultiDay_whenCourtScheduleIdPresentAndDurationAbove360() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId1 = UUID.randomUUID(); + final UUID courtScheduleId2 = UUID.randomUUID(); + final UUID courtScheduleId3 = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate day1 = LocalDate.now().plusDays(5); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(1080) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId1) + .withHearingDate(day1) + .withDurationMinutes(1080) + .build())) + .withBookedSlots(Collections.singletonList( + RotaSlot.rotaSlot() + .withCourtScheduleId(courtScheduleId1.toString()) + .withCourtCentreId(courtHouseId.toString()) + .withRoomId(courtRoomId.toString()) + .build())) + .build(); + + // Mock multiDaySearchAndBook + final CourtSchedule cs1 = buildCourtSchedule(courtScheduleId1, courtRoomId, courtHouseId, day1, false); + final CourtSchedule cs2 = buildCourtSchedule(courtScheduleId2, courtRoomId, courtHouseId, day1.plusDays(1), false); + final CourtSchedule cs3 = buildCourtSchedule(courtScheduleId3, courtRoomId, courtHouseId, day1.plusDays(2), false); + + final JsonObject multiDayResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(buildCsJson(cs1)) + .add(buildCsJson(cs2)) + .add(buildCsJson(cs3))) + .build(); + + final Response multiDayResponse = mock(Response.class); + when(multiDayResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(multiDayResponse); + when(objectToJsonObjectConverter.convert(multiDayResponse.getEntity())).thenReturn(multiDayResponseJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))) + .thenReturn(cs1, cs2, cs3); + + // Mock listHearingInCourtSessions + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId1, "2026-03-16T10:00:00Z", 360)) + .add(buildListHearingJson(courtScheduleId2, "2026-03-17T10:00:00Z", 360)) + .add(buildListHearingJson(courtScheduleId3, "2026-03-18T10:00:00Z", 360))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder() + .add(courtScheduleId1.toString()) + .add(courtScheduleId2.toString()) + .add(courtScheduleId3.toString()) + .build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + verify(hearingSlotsService).multiDaySearchAndBook(anyMap()); + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + verify(hearingSlotsService, never()).getCourtSchedulesById(anyMap()); + assertThat(result.getHearingDays().size(), is(3)); + } + + @Test + void enrichCrownCourtScheduleFirst_shouldSkipListHearing_whenSingleDaySessionIsDraft() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(240) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(sessionDate) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds - isDraft=true + final CourtSchedule cs = buildCourtSchedule(courtScheduleId, courtRoomId, courtHouseId, sessionDate, true); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("isDraft", true))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions (now always called, even for draft) + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId, sessionDate + "T10:00:00Z", 240))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + // listHearingInCourtSessions IS now called for draft sessions (for slot deduction) + verify(hearingSlotsService).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + // HearingDays should carry isDraft=true from sanity check + assertThat(result.getHearingDays().size(), is(1)); + assertThat(result.getHearingDays().get(0).getIsDraft(), is(true)); + // courtRoomId should be null for draft sessions + assertThat(result.getHearingDays().get(0).getCourtRoomId(), is((UUID) null)); + } + + @Test + void enrichCrownCourtScheduleFirst_shouldSkipListHearing_whenMultiDaySessionsHaveDraft() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId1 = UUID.randomUUID(); + final UUID courtScheduleId2 = UUID.randomUUID(); + final UUID courtScheduleId3 = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate day1 = LocalDate.now().plusDays(5); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(720) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId1) + .withHearingDate(day1) + .withDurationMinutes(720) + .build())) + .withBookedSlots(Collections.singletonList( + RotaSlot.rotaSlot() + .withCourtScheduleId(courtScheduleId1.toString()) + .withCourtCentreId(courtHouseId.toString()) + .withRoomId(courtRoomId.toString()) + .build())) + .build(); + + // Mock multiDaySearchAndBook - one session is draft + final CourtSchedule cs1 = buildCourtSchedule(courtScheduleId1, courtRoomId, courtHouseId, day1, false); + final CourtSchedule cs2 = buildCourtSchedule(courtScheduleId2, courtRoomId, courtHouseId, day1.plusDays(1), true); // draft! + final CourtSchedule cs3 = buildCourtSchedule(courtScheduleId3, courtRoomId, courtHouseId, day1.plusDays(2), false); + + final JsonObject multiDayResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(buildCsJson(cs1)) + .add(buildCsJson(cs2)) + .add(buildCsJson(cs3))) + .build(); + + final Response multiDayResponse = mock(Response.class); + when(multiDayResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(multiDayResponse); + when(objectToJsonObjectConverter.convert(multiDayResponse.getEntity())).thenReturn(multiDayResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))) + .thenReturn(cs1, cs2, cs3); + + // Mock listHearingInCourtSessions (now always called, even when some sessions are draft) + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId1, day1 + "T10:00:00Z", 240)) + .add(buildListHearingJson(courtScheduleId2, day1.plusDays(1) + "T10:00:00Z", 240)) + .add(buildListHearingJson(courtScheduleId3, day1.plusDays(2) + "T10:00:00Z", 240))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder() + .add(courtScheduleId1.toString()) + .add(courtScheduleId2.toString()) + .add(courtScheduleId3.toString()) + .build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + // listHearingInCourtSessions IS now called even when some sessions are draft + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + // Expanded hearingDays should be returned with isDraft from sessions + assertThat(result.getHearingDays().size(), is(3)); + } + + // ─── enrichCrownCourtScheduleFirst (UpdateHearingForListing) tests ─── + + @Test + void enrichCrownCourtScheduleFirst_update_shouldReturnUnchanged_whenNoCourtScheduleId() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtCentreId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + + final UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withStartDate(LocalDate.of(2026, 4, 10)) + .withEndDate(LocalDate.of(2026, 4, 10)) + .withCourtCentreId(courtCentreId) + .withCourtRoomId(courtRoomId) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.of(2026, 4, 10)) + .withDurationMinutes(120) + .build() + )) + .build(); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + assertThat(result, is(hearing)); + verify(hearingSlotsService, never()).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + verify(hearingSlotsService, never()).multiDaySearchAndBook(anyMap()); + } + + @Test + void enrichCrownCourtScheduleFirst_update_shouldCallSingleDay_whenCourtScheduleIdPresentAndDurationBelow360() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(sessionDate) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds - non-draft + final CourtSchedule cs = buildCourtSchedule(courtScheduleId, courtRoomId, courtHouseId, sessionDate, false); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId, "2026-03-16T10:00:00Z", 240))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + verify(hearingSlotsService).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + verify(hearingSlotsService, never()).multiDaySearchAndBook(anyMap()); + assertThat(result.getHearingDays().size(), is(1)); + } + + @Test + void enrichCrownCourtScheduleFirst_update_shouldCallMultiDay_whenCourtScheduleIdPresentAndDurationAbove360() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId1 = UUID.randomUUID(); + final UUID courtScheduleId2 = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate day1 = LocalDate.now().plusDays(5); + + // Multi-day: totalDuration > 360 + final UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withHearingId(hearingId) + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId1) + .withHearingDate(day1) + .withDurationMinutes(360) + .build(), + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId2) + .withHearingDate(day1.plusDays(1)) + .withDurationMinutes(360) + .build())) + .build(); + + // Mock multiDaySearchAndBook + final CourtSchedule cs1 = buildCourtSchedule(courtScheduleId1, courtRoomId, courtHouseId, day1, false); + final CourtSchedule cs2 = buildCourtSchedule(courtScheduleId2, courtRoomId, courtHouseId, day1.plusDays(1), false); + + final JsonObject multiDayResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(buildCsJson(cs1)) + .add(buildCsJson(cs2))) + .build(); + + final Response multiDayResponse = mock(Response.class); + when(multiDayResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.multiDaySearchAndBook(anyMap())).thenReturn(multiDayResponse); + when(objectToJsonObjectConverter.convert(multiDayResponse.getEntity())).thenReturn(multiDayResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))) + .thenReturn(cs1, cs2); + + // Mock listHearingInCourtSessions + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId1, "2026-03-16T10:00:00Z", 360)) + .add(buildListHearingJson(courtScheduleId2, "2026-03-17T10:00:00Z", 360))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder() + .add(courtScheduleId1.toString()) + .add(courtScheduleId2.toString()) + .build()); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + verify(hearingSlotsService).multiDaySearchAndBook(anyMap()); + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + verify(hearingSlotsService, never()).getCourtSchedulesById(anyMap()); + assertThat(result.getHearingDays().size(), is(2)); + } + + @Test + void enrichCrownCourtScheduleFirst_update_shouldSkipListHearing_whenSingleDaySessionIsDraft() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingId(hearingId) + .withStartDate(sessionDate) + .withEndDate(sessionDate) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withCourtCentreId(courtHouseId) + .withCourtRoomId(courtRoomId) + .withHearingDate(sessionDate) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds - isDraft=true + final CourtSchedule cs = buildCourtSchedule(courtScheduleId, courtRoomId, courtHouseId, sessionDate, true); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("isDraft", true))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions (now always called, even for draft) + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId, sessionDate + "T10:00:00Z", 240))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + // listHearingInCourtSessions IS now called for draft sessions (for slot deduction) + verify(hearingSlotsService).getCourtSchedulesById(anyMap()); + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + // HearingDays should carry isDraft=true + assertThat(result.getHearingDays().size(), is(1)); + assertThat(result.getHearingDays().get(0).getIsDraft(), is(true)); + // courtRoomId should be null for draft sessions + assertThat(result.getHearingDays().get(0).getCourtRoomId(), is((UUID) null)); + } + + // ─── Additional coverage tests ────────────────────────────────────── + + @Test + void calculateAggregatedDuration_update_shouldReturnZeroWhenNoDurationInfo() { + final UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing().build(); + assertThat(CourtScheduleEnrichmentService.calculateAggregatedDuration(hearing), is(0)); + } + + @Test + void enrichCrownUpdateHearing_shouldReturnUnchanged_whenMultiDayAndNoCourtScheduleIdOnHearingDays() { + final UUID hearingId = UUID.randomUUID(); + + final UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingId(hearingId) + .withStartDate(LocalDate.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(7)) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now().plusDays(5)) + .withDurationMinutes(720) + .build())) // no courtScheduleId + .build(); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + assertThat(result, is(hearing)); + verify(hearingSlotsService, never()).multiDaySearchAndBook(anyMap()); + } + + @Test + void enrichCrownUpdateHearing_shouldReturnUnchanged_whenSingleDayAndFetchReturnsEmpty() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + + final UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingId(hearingId) + .withStartDate(LocalDate.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(5)) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withCourtCentreId(UUID.randomUUID()) + .withCourtRoomId(UUID.randomUUID()) + .withHearingDate(LocalDate.now().plusDays(5)) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds to return empty + final JsonObject emptyResponse = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder()) + .build(); + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(emptyResponse); + + final UpdateHearingForListing result = courtScheduleEnrichmentService.enrichWithCourtSchedules(hearing, mock(JsonEnvelope.class)); + + assertThat(result, is(hearing)); + verify(hearingSlotsService, never()).listHearingInCourtSessions(any()); + } + + @Test + void enrichCrownCourtScheduleFirst_shouldEnrichJudiciary_whenListHearingReturnsJudiciaries() { + final UUID hearingId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final UUID courtHouseId = UUID.randomUUID(); + final UUID judicialId = UUID.randomUUID(); + final LocalDate sessionDate = LocalDate.now().plusDays(5); + + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withId(hearingId) + .withEstimatedMinutes(240) + .withCourtCentre(CourtCentre.courtCentre().withId(courtHouseId).withRoomId(courtRoomId).build()) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId) + .withHearingDate(sessionDate) + .withDurationMinutes(240) + .build())) + .build(); + + // Mock fetchCourtSchedulesByIds - non-draft + final CourtSchedule cs = buildCourtSchedule(courtScheduleId, courtRoomId, courtHouseId, sessionDate, false); + + final JsonObject csResponseJson = JsonObjects.createObjectBuilder() + .add("courtSchedules", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("courtScheduleId", courtScheduleId.toString()) + .add("isDraft", false))) + .build(); + + final Response csResponse = mock(Response.class); + when(csResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(hearingSlotsService.getCourtSchedulesById(anyMap())).thenReturn(csResponse); + when(objectToJsonObjectConverter.convert(csResponse.getEntity())).thenReturn(csResponseJson); + when(jsonObjectConverter.convert(any(JsonObject.class), eq(CourtSchedule.class))).thenReturn(cs); + + // Mock listHearingInCourtSessions with judiciaries + final JsonObject listJson = JsonObjects.createObjectBuilder() + .add("hearings", JsonObjects.createArrayBuilder() + .add(buildListHearingJson(courtScheduleId, "2026-03-16T10:00:00Z", 240))) + .add("judiciaries", JsonObjects.createArrayBuilder() + .add(JsonObjects.createObjectBuilder() + .add("judicialId", judicialId.toString()) + .add("judicialRoleTypeId", UUID.randomUUID().toString()) + .add("isPrimary", true))) + .build(); + + final Response listResponse = mock(Response.class); + when(listResponse.getStatus()).thenReturn(HttpStatus.SC_OK); + when(listResponse.getEntity()).thenReturn(listJson); + when(hearingSlotsService.listHearingInCourtSessions(any(JsonObject.class))).thenReturn(listResponse); + when(objectToJsonObjectConverter.convert(listJson)).thenReturn(listJson); + + when(jsonObjectConverter.convert(any(JsonObject.class), eq(ListUpdateHearing.class))) + .thenAnswer(inv -> { + JsonObject jo = inv.getArgument(0); + ListUpdateHearing luh = new ListUpdateHearing(); + luh.setCourtScheduleId(jo.getString("courtScheduleId")); + luh.setHearingStartTime(jo.getString("hearingStartTime")); + luh.setDuration(jo.getInt("duration")); + return luh; + }); + + when(slotsToJsonStringConverter.convertHearingDaysToCourtScheduleIdsJson(anyList())) + .thenReturn(JsonObjects.createArrayBuilder().add(courtScheduleId.toString()).build()); + + final HearingListingNeeds result = courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(hearing); + + verify(hearingSlotsService).listHearingInCourtSessions(any(JsonObject.class)); + assertThat(result.getHearingDays().size(), is(1)); + } + + @Test + void needsCourtScheduleEnrichment_shouldReturnFalse_forUnknownJurisdiction() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(null) + .build(); + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(false)); + } + + @Test + void needsCourtScheduleEnrichment_shouldReturnTrue_forMagistratesWithBookingReference() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withBookingReference(UUID.randomUUID()) + .build(); + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(true)); + } + + @Test + void needsCourtScheduleEnrichment_shouldReturnTrue_forCrownFixedDateWithCourtScheduleId() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingDays(Collections.singletonList( + HearingDay.hearingDay() + .withCourtScheduleId(UUID.randomUUID()) + .withHearingDate(LocalDate.now().plusDays(5)) + .build())) + .build(); + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(true)); + } + + @Test + void needsCourtScheduleEnrichment_shouldReturnFalse_forCrownWithWeekCommencing() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withWeekCommencingDate(WeekCommencingDate.weekCommencingDate() + .withStartDate(LocalDate.now().plusDays(7).toString()) + .withDuration(1) + .build()) + .build(); + assertThat(CourtScheduleEnrichmentService.needsCourtScheduleEnrichment(hearing), is(false)); + } + + @Test + void isCandidateForAllocation_shouldReturnFalse_whenNoCourtCentre() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withListedStartDateTime(java.time.ZonedDateTime.now()) + .build(); + assertThat(CourtScheduleEnrichmentService.isCandidateForAllocation(hearing), is(false)); + } + + @Test + void isCandidateForAllocation_shouldReturnTrue_whenAllCriteriaMet() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withListedStartDateTime(java.time.ZonedDateTime.now()) + .withCourtCentre(CourtCentre.courtCentre().withRoomId(UUID.randomUUID()).build()) + .build(); + assertThat(CourtScheduleEnrichmentService.isCandidateForAllocation(hearing), is(true)); + } + + @Test + void isCandidateForAllocation_shouldReturnTrue_whenEarliestStartDateTimeUsed() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withJurisdictionType(JurisdictionType.CROWN) + .withEarliestStartDateTime(java.time.ZonedDateTime.now()) + .withCourtCentre(CourtCentre.courtCentre().withRoomId(UUID.randomUUID()).build()) + .build(); + assertThat(CourtScheduleEnrichmentService.isCandidateForAllocation(hearing), is(true)); + } + + @Test + void isCandidateForAllocation_update_shouldReturnFalse_whenNoStartDate() { + final UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withJurisdictionType(JurisdictionType.CROWN) + .withCourtRoomId(UUID.randomUUID()) + .build(); + assertThat(CourtScheduleEnrichmentService.isCandidateForAllocation(hearing), is(false)); + } + + @Test + void isCandidateForAllocation_update_shouldReturnTrue_whenAllCriteriaMet() { + final UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withJurisdictionType(JurisdictionType.CROWN) + .withStartDate(LocalDate.now()) + .withCourtRoomId(UUID.randomUUID()) + .build(); + assertThat(CourtScheduleEnrichmentService.isCandidateForAllocation(hearing), is(true)); + } + + @Test + void hasCourtScheduleIdOnInput_shouldReturnFalse_whenNoHearingDaysOrBookedSlots() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds().build(); + assertThat(CourtScheduleEnrichmentService.hasCourtScheduleIdOnInput(hearing), is(false)); + } + + @Test + void hasCourtScheduleIdOnInput_shouldReturnTrue_whenBookedSlotsHaveCourtScheduleId() { + final HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withBookedSlots(Collections.singletonList( + RotaSlot.rotaSlot() + .withCourtScheduleId(UUID.randomUUID().toString()) + .build())) + .build(); + assertThat(CourtScheduleEnrichmentService.hasCourtScheduleIdOnInput(hearing), is(true)); + } + + // --- populateBookedSlots tests --- + + @Test + void populateBookedSlots_shouldMatchAndPopulate_whenRoomIdIsNullOnBothSides() { + final UUID courtCentreId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final ZonedDateTime startTime = ZonedDateTime.of(2026, 5, 1, 9, 0, 0, 0, ZoneOffset.UTC); + + RotaSlot slot = RotaSlot.rotaSlot() + .withCourtCentreId(courtCentreId.toString()) + .withCourtScheduleId(courtScheduleId.toString()) + .withStartTime(startTime) + .withDuration(60) + .build(); + + HearingDay hearingDay = HearingDay.hearingDay() + .withCourtCentreId(courtCentreId) + .withCourtScheduleId(courtScheduleId) + .withStartTime(startTime) + .withDurationMinutes(60) + .build(); + + List result = CourtScheduleEnrichmentService.populateBookedSlots( + List.of(slot), List.of(hearingDay)); + + assertEquals(1, result.size()); + assertEquals(courtScheduleId.toString(), result.get(0).getCourtScheduleId()); + assertNull(result.get(0).getRoomId()); + } + + @Test + void populateBookedSlots_shouldMatchAndKeepOriginalRoom_whenSlotHasRoomButHearingDayDoesNot() { + final UUID courtCentreId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID roomId = UUID.randomUUID(); + final ZonedDateTime startTime = ZonedDateTime.of(2026, 5, 1, 9, 0, 0, 0, ZoneOffset.UTC); + + RotaSlot slot = RotaSlot.rotaSlot() + .withCourtCentreId(courtCentreId.toString()) + .withRoomId(roomId.toString()) + .withCourtScheduleId(courtScheduleId.toString()) + .withStartTime(startTime) + .withDuration(60) + .build(); + + HearingDay hearingDay = HearingDay.hearingDay() + .withCourtCentreId(courtCentreId) + .withCourtScheduleId(courtScheduleId) + .withStartTime(startTime) + .withDurationMinutes(60) + .build(); + + List result = CourtScheduleEnrichmentService.populateBookedSlots( + List.of(slot), List.of(hearingDay)); + + assertEquals(1, result.size()); + // roomId from original slot preserved via withValuesFrom; hearingDay has no courtRoomId to override + assertEquals(roomId.toString(), result.get(0).getRoomId()); + } + + @Test + void populateBookedSlots_shouldMatchAndPopulateRoomId_whenBothHaveRoomId() { + final UUID courtCentreId = UUID.randomUUID(); + final UUID courtScheduleId = UUID.randomUUID(); + final UUID roomId = UUID.randomUUID(); + final ZonedDateTime startTime = ZonedDateTime.of(2026, 5, 1, 9, 0, 0, 0, ZoneOffset.UTC); + + RotaSlot slot = RotaSlot.rotaSlot() + .withCourtCentreId(courtCentreId.toString()) + .withRoomId(roomId.toString()) + .withCourtScheduleId(courtScheduleId.toString()) + .withStartTime(startTime) + .withDuration(60) + .build(); + + HearingDay hearingDay = HearingDay.hearingDay() + .withCourtCentreId(courtCentreId) + .withCourtRoomId(roomId) + .withCourtScheduleId(courtScheduleId) + .withStartTime(startTime) + .withDurationMinutes(60) + .build(); + + List result = CourtScheduleEnrichmentService.populateBookedSlots( + List.of(slot), List.of(hearingDay)); + + assertEquals(1, result.size()); + assertEquals(roomId.toString(), result.get(0).getRoomId()); + } + + @Test + void populateBookedSlots_shouldNotMatch_whenCentreIdsDiffer() { + final ZonedDateTime startTime = ZonedDateTime.of(2026, 5, 1, 9, 0, 0, 0, ZoneOffset.UTC); + + RotaSlot slot = RotaSlot.rotaSlot() + .withCourtCentreId(UUID.randomUUID().toString()) + .withCourtScheduleId(UUID.randomUUID().toString()) + .withStartTime(startTime) + .withDuration(60) + .build(); + + HearingDay hearingDay = HearingDay.hearingDay() + .withCourtCentreId(UUID.randomUUID()) + .withCourtScheduleId(UUID.randomUUID()) + .withStartTime(startTime) + .withDurationMinutes(60) + .build(); + + List result = CourtScheduleEnrichmentService.populateBookedSlots( + List.of(slot), List.of(hearingDay)); + + assertTrue(result.isEmpty()); + } +} diff --git a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingDaysEnrichmentServiceTest.java b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingDaysEnrichmentServiceTest.java index 0804b168d..18f8edc79 100644 --- a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingDaysEnrichmentServiceTest.java +++ b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingDaysEnrichmentServiceTest.java @@ -6,7 +6,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; import static uk.gov.justice.services.test.utils.core.random.RandomGenerator.FUTURE_LOCAL_DATE; import static uk.gov.justice.services.test.utils.core.random.RandomGenerator.FUTURE_UTC_DATE_TIME; import static uk.gov.justice.services.test.utils.core.random.RandomGenerator.FUTURE_ZONED_DATE_TIME; @@ -29,6 +32,9 @@ import java.util.Arrays; import java.util.List; +import javax.json.Json; +import javax.json.JsonObject; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -49,6 +55,8 @@ class HearingDaysEnrichmentServiceTest { @Mock private CourtScheduleEnrichmentService courtScheduleEnrichmentService; @Mock + private uk.gov.moj.cpp.listing.common.xhibit.ReferenceDataCache referenceDataCache; + @Mock private JsonEnvelope jsonEnvelope; private RotaSlot defaultSlot; private HearingDay defaultHearingDay; @@ -208,6 +216,44 @@ public void shouldEnrichSingleHearingForCrownJurisdiction() { assertEquals(JurisdictionType.CROWN, enrichedHearing.getJurisdictionType()); } + @Test + public void shouldSetEndTimeOnHearingDaysForUpdateWithNonDefaultDays() { + // Given — MAGISTRATES update hearing with non-default days triggers convertNonDefaultDaysToHearingDays + // which should compute endTime = startTime + duration + final LocalDate today = LocalDate.now(); + + UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withJurisdictionType(JurisdictionType.MAGISTRATES) + .withHearingId(randomUUID()) + .withNonDefaultDays(Arrays.asList( + createCommandNonDefaultDay(today, "AM"), + createCommandNonDefaultDay(today.plusDays(1), "PM"))) + .withStartDate(today) + .withEndDate(today.plusDays(1)) + .withNonSittingDays(emptyList()) + .build(); + + // When + UpdateHearingForListing enrichedHearing = hearingDaysEnrichmentService.enrichHearing(hearing, jsonEnvelope); + + // Then — each hearingDay should have endTime = startTime + durationMinutes + assertNotNull(enrichedHearing); + assertNotNull(enrichedHearing.getHearingDays()); + assertFalse(enrichedHearing.getHearingDays().isEmpty()); + enrichedHearing.getHearingDays().forEach(hearingDay -> { + assertNotNull(hearingDay.getStartTime(), "startTime should not be null"); + assertNotNull(hearingDay.getDurationMinutes(), "durationMinutes should not be null"); + assertNotNull(hearingDay.getEndTime(), "endTime should not be null"); + assertEquals(hearingDay.getStartTime().plusMinutes(hearingDay.getDurationMinutes()), hearingDay.getEndTime(), + "endTime should equal startTime + durationMinutes"); + }); + } + + // Removed shouldSetEndTimeOnEnrichCandidateHearingDay: on list-court-hearing, CROWN hearingDays + // are now produced exclusively by CourtScheduleEnrichmentService.enrichCrownCourtScheduleFirst + // (single-day) or by multiDaySearchAndBook (multi-day). HearingDaysEnrichmentService.enrichHearings + // no longer creates hearingDays for CROWN and only calculates start/end dates. + @Test public void shouldEnrichHearingDaysFromBookedSlots() { // Given @@ -261,6 +307,100 @@ public void shouldEnrichHearingDaysFromNonDefaultDays() { enrichedHearing.getHearingDays().forEach(hearingDay -> assertNotNull(hearingDay.getCourtScheduleId())); } + @Test + public void shouldPreserveCourtScheduleIdFromNonDefaultDaysForCrownUpdate() { + // Given — Crown update hearing with nonDefaultDays carrying courtScheduleId + final LocalDate today = LocalDate.now(); + final java.util.UUID expectedCourtScheduleId = randomUUID(); + final java.util.UUID courtCentreId = randomUUID(); + final java.util.UUID roomId = randomUUID(); + + JsonObject courtRoomJson = Json.createObjectBuilder() + .add("id", roomId.toString()) + .add("courtroomId", 772) + .build(); + when(referenceDataCache.getCpCourtRoomCache(courtCentreId)).thenReturn(List.of(courtRoomJson)); + + uk.gov.justice.listing.commands.NonDefaultDay nonDefaultDay = uk.gov.justice.listing.commands.NonDefaultDay.nonDefaultDay() + .withCourtCentreId(courtCentreId.toString()) + .withRoomId(roomId.toString()) + .withCourtScheduleId(expectedCourtScheduleId.toString()) + .withStartTime(today.atTime(9, 0).atZone(ZoneId.of("UTC"))) + .withDuration(60) + .withSession("AM") + .withOucode("C01BL00") + .build(); + + UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingId(randomUUID()) + .withCourtCentreId(courtCentreId) + .withCourtRoomId(roomId) + .withStartDate(today) + .withEndDate(today) + .withNonSittingDays(emptyList()) + .withNonDefaultDays(List.of(nonDefaultDay)) + .build(); + + // When + UpdateHearingForListing enrichedHearing = hearingDaysEnrichmentService.enrichHearing(hearing, jsonEnvelope); + + // Then — hearingDays should have the courtScheduleId from nonDefaultDays + assertNotNull(enrichedHearing.getHearingDays()); + assertEquals(1, enrichedHearing.getHearingDays().size()); + HearingDay resultDay = enrichedHearing.getHearingDays().get(0); + assertEquals(expectedCourtScheduleId, resultDay.getCourtScheduleId(), + "courtScheduleId from nonDefaultDay must be preserved on the generated HearingDay"); + assertEquals(courtCentreId, resultDay.getCourtCentreId()); + assertEquals(roomId, resultDay.getCourtRoomId()); + assertEquals(today, resultDay.getHearingDate()); + assertEquals(60, resultDay.getDurationMinutes()); + } + + @Test + public void shouldGenerateHearingDayWithoutCourtScheduleIdWhenNonDefaultDayHasNone() { + // Given — Crown update hearing with nonDefaultDay that has NO courtScheduleId + final LocalDate today = LocalDate.now(); + final java.util.UUID courtCentreId = randomUUID(); + final java.util.UUID roomId = randomUUID(); + + JsonObject courtRoomJson = Json.createObjectBuilder() + .add("id", roomId.toString()) + .add("courtroomId", 772) + .build(); + when(referenceDataCache.getCpCourtRoomCache(courtCentreId)).thenReturn(List.of(courtRoomJson)); + + uk.gov.justice.listing.commands.NonDefaultDay nonDefaultDay = uk.gov.justice.listing.commands.NonDefaultDay.nonDefaultDay() + .withCourtCentreId(courtCentreId.toString()) + .withRoomId(roomId.toString()) + .withStartTime(today.atTime(9, 0).atZone(ZoneId.of("UTC"))) + .withDuration(60) + .withSession("AM") + .withOucode("C01BL00") + .build(); + + UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withJurisdictionType(JurisdictionType.CROWN) + .withHearingId(randomUUID()) + .withCourtCentreId(courtCentreId) + .withCourtRoomId(roomId) + .withStartDate(today) + .withEndDate(today) + .withNonSittingDays(emptyList()) + .withNonDefaultDays(List.of(nonDefaultDay)) + .build(); + + // When + UpdateHearingForListing enrichedHearing = hearingDaysEnrichmentService.enrichHearing(hearing, jsonEnvelope); + + // Then — hearingDay should have no courtScheduleId (unallocated) + assertNotNull(enrichedHearing.getHearingDays()); + assertEquals(1, enrichedHearing.getHearingDays().size()); + HearingDay resultDay = enrichedHearing.getHearingDays().get(0); + assertNull(resultDay.getCourtScheduleId(), + "courtScheduleId should be null when nonDefaultDay has no courtScheduleId"); + } + @Test public void shouldHandleNullJurisdictionType() { // Given diff --git a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorCoverageTest.java b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorCoverageTest.java deleted file mode 100644 index d729ec06e..000000000 --- a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorCoverageTest.java +++ /dev/null @@ -1,439 +0,0 @@ -package uk.gov.moj.cpp.listing.command.api.service; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsNull.notNullValue; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static uk.gov.justice.listing.commands.HearingDay.hearingDay; -import static uk.gov.justice.listing.commands.HearingListingNeeds.hearingListingNeeds; -import static uk.gov.justice.listing.commands.UpdateHearingForListing.updateHearingForListing; - -import uk.gov.justice.core.courts.JurisdictionType; -import uk.gov.justice.core.courts.WeekCommencingDate; -import uk.gov.justice.listing.commands.CourtCentreDetails; -import uk.gov.justice.listing.commands.HearingDay; -import uk.gov.justice.listing.commands.HearingListingNeeds; -import uk.gov.justice.listing.commands.UpdateHearingForListing; -import uk.gov.justice.services.messaging.JsonEnvelope; - -import java.lang.reflect.Field; -import java.time.LocalDate; -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Tests the static helper methods and all jurisdiction branches of - * HearingEnrichmentOrchestrator using anonymous service stubs injected via - * reflection — no Mockito inline mocking required. - */ -public class HearingEnrichmentOrchestratorCoverageTest { - - private final HearingDaysEnrichmentService hearingDaysStub = new HearingDaysEnrichmentService() { - @Override - public HearingListingNeeds enrichHearings(HearingListingNeeds hearing, JsonEnvelope envelope) { - return hearing; - } - @Override - public UpdateHearingForListing enrichHearing(UpdateHearingForListing hearing, JsonEnvelope envelope) { - return hearing; - } - @Override - public UpdateHearingForListing enrichHearing(UpdateHearingForListing hearing, JsonEnvelope envelope, CourtCentreDetails courtCentreDetails) { - return hearing; - } - }; - - private final HearingDurationEnrichmentService durationStub = new HearingDurationEnrichmentService() { - @Override - public HearingListingNeeds enrichWithDurations(HearingListingNeeds hearing, JsonEnvelope envelope) { - return hearing; - } - @Override - public UpdateHearingForListing enrichWithDurationForUpdate(UpdateHearingForListing hearing, JsonEnvelope envelope) { - return hearing; - } - }; - - private final CourtScheduleEnrichmentService scheduleStub = new CourtScheduleEnrichmentService() { - @Override - public HearingListingNeeds enrichWithCourtSchedules(HearingListingNeeds hearing, JsonEnvelope envelope) { - return hearing; - } - @Override - public UpdateHearingForListing enrichWithCourtSchedules(UpdateHearingForListing hearing, JsonEnvelope envelope) { - return hearing; - } - }; - - private HearingEnrichmentOrchestrator orchestrator; - - @BeforeEach - void setUp() throws Exception { - orchestrator = new HearingEnrichmentOrchestrator(); - injectField("hearingDaysEnrichmentService", hearingDaysStub); - injectField("hearingDurationEnrichmentService", durationStub); - injectField("courtScheduleEnrichmentService", scheduleStub); - } - - private void injectField(String fieldName, Object value) throws Exception { - Field field = HearingEnrichmentOrchestrator.class.getDeclaredField(fieldName); - field.setAccessible(true); - field.set(orchestrator, value); - } - - // ── enrichListCourtHearing — all jurisdiction branches ──────────────────── - - @Test - void shouldEnrichListCourtHearingForMagistrates() { - HearingListingNeeds hearing = hearingListingNeeds() - .withJurisdictionType(JurisdictionType.MAGISTRATES) - .build(); - - List result = orchestrator.enrichListCourtHearing(List.of(hearing), null); - - assertThat(result, hasSize(1)); - assertThat(result.get(0).getJurisdictionType(), is(JurisdictionType.MAGISTRATES)); - } - - @Test - void shouldEnrichListCourtHearingForCrown() { - HearingListingNeeds hearing = hearingListingNeeds() - .withJurisdictionType(JurisdictionType.CROWN) - .build(); - - List result = orchestrator.enrichListCourtHearing(List.of(hearing), null); - - assertThat(result, hasSize(1)); - assertThat(result.get(0).getJurisdictionType(), is(JurisdictionType.CROWN)); - } - - @Test - void shouldThrowForUnsupportedJurisdictionInEnrichListCourtHearing() { - HearingListingNeeds hearing = hearingListingNeeds().build(); // jurisdictionType = null - - assertThrows(IllegalArgumentException.class, - () -> orchestrator.enrichListCourtHearing(List.of(hearing), null)); - } - - // ── enrichUpdateHearingForListing(hearing, envelope) ───────────────────── - - @Test - void shouldEnrichUpdateHearingForListingForMagistrates() { - UpdateHearingForListing hearing = updateHearingForListing() - .withJurisdictionType(JurisdictionType.MAGISTRATES) - .build(); - - UpdateHearingForListing result = orchestrator.enrichUpdateHearingForListing(hearing, null); - - assertThat(result, notNullValue()); - } - - @Test - void shouldEnrichUpdateHearingForListingForCrown() { - UpdateHearingForListing hearing = updateHearingForListing() - .withJurisdictionType(JurisdictionType.CROWN) - .build(); - - UpdateHearingForListing result = orchestrator.enrichUpdateHearingForListing(hearing, null); - - assertThat(result, notNullValue()); - } - - @Test - void shouldThrowForUnsupportedJurisdictionInUpdateHearing() { - UpdateHearingForListing hearing = updateHearingForListing().build(); // jurisdictionType = null - - assertThrows(IllegalArgumentException.class, - () -> orchestrator.enrichUpdateHearingForListing(hearing, null)); - } - - // ── enrichUpdateHearingForListing(hearing, envelope, courtCentreDetails) ── - - @Test - void shouldEnrichUpdateHearingWithCourtCentreForMagistrates() { - UpdateHearingForListing hearing = updateHearingForListing() - .withJurisdictionType(JurisdictionType.MAGISTRATES) - .build(); - - UpdateHearingForListing result = orchestrator.enrichUpdateHearingForListing(hearing, null, null); - - assertThat(result, notNullValue()); - } - - @Test - void shouldEnrichUpdateHearingWithCourtCentreForCrown() { - UpdateHearingForListing hearing = updateHearingForListing() - .withJurisdictionType(JurisdictionType.CROWN) - .build(); - - UpdateHearingForListing result = orchestrator.enrichUpdateHearingForListing(hearing, null, null); - - assertThat(result, notNullValue()); - } - - @Test - void shouldThrowForUnsupportedJurisdictionInUpdateHearingWithCourtCentre() { - UpdateHearingForListing hearing = updateHearingForListing().build(); // jurisdictionType = null - - assertThrows(IllegalArgumentException.class, - () -> orchestrator.enrichUpdateHearingForListing(hearing, null, null)); - } - - // ── Static: sequenceValidHearingDays ───────────────────────────────────── - - @Test - void shouldSequenceHearingDaysStartingFromOne() { - HearingDay day1 = hearingDay().withHearingDate(LocalDate.of(2020, 8, 18)).withSequence(99).withDurationMinutes(30).build(); - HearingDay day2 = hearingDay().withHearingDate(LocalDate.of(2020, 8, 19)).withSequence(99).withDurationMinutes(10).build(); - - List result = HearingEnrichmentOrchestrator.sequenceValidHearingDays(List.of(day1, day2)); - - assertThat(result, hasSize(2)); - assertThat(result.get(0).getSequence(), is(1)); - assertThat(result.get(1).getSequence(), is(2)); - } - - // ── Static: orderAndFilterOutNonSittingDays ─────────────────────────────── - - @Test - void shouldFilterNonSittingDaysAndSortByDate() { - LocalDate nonSitting = LocalDate.of(2020, 8, 19); - HearingDay day1 = hearingDay().withHearingDate(LocalDate.of(2020, 8, 18)).withSequence(1).withDurationMinutes(30).build(); - HearingDay day2 = hearingDay().withHearingDate(nonSitting).withSequence(2).withDurationMinutes(10).build(); - HearingDay day3 = hearingDay().withHearingDate(LocalDate.of(2020, 8, 17)).withSequence(3).withDurationMinutes(20).build(); - - List result = HearingEnrichmentOrchestrator.orderAndFilterOutNonSittingDays( - List.of(day1, day2, day3), List.of(nonSitting)); - - assertThat(result, hasSize(2)); - assertThat(result.get(0).getHearingDate(), is(LocalDate.of(2020, 8, 17))); - assertThat(result.get(1).getHearingDate(), is(LocalDate.of(2020, 8, 18))); - assertThat(result.get(0).getSequence(), is(0)); - } - - @Test - void shouldReturnAllDaysOrderedWhenNonSittingListIsEmpty() { - HearingDay day1 = hearingDay().withHearingDate(LocalDate.of(2020, 8, 18)).withSequence(2).withDurationMinutes(30).build(); - HearingDay day2 = hearingDay().withHearingDate(LocalDate.of(2020, 8, 17)).withSequence(1).withDurationMinutes(20).build(); - - List result = HearingEnrichmentOrchestrator.orderAndFilterOutNonSittingDays( - List.of(day1, day2), List.of()); - - assertThat(result, hasSize(2)); - assertThat(result.get(0).getHearingDate(), is(LocalDate.of(2020, 8, 17))); - assertThat(result.get(0).getSequence(), is(0)); - } - - // ── Static: getTotalDuration ────────────────────────────────────────────── - - @Test - void shouldSumAllHearingDayDurations() { - HearingDay day1 = hearingDay().withDurationMinutes(30).withHearingDate(LocalDate.of(2020, 8, 18)).build(); - HearingDay day2 = hearingDay().withDurationMinutes(20).withHearingDate(LocalDate.of(2020, 8, 19)).build(); - - assertThat(HearingEnrichmentOrchestrator.getTotalDuration(List.of(day1, day2)), is(50)); - } - - @Test - void shouldUseDefaultMinWhenHearingDayDurationIsNull() { - // DEFAULT_MIN = 20; null duration contributes 20 - HearingDay dayWithNull = hearingDay().withHearingDate(LocalDate.of(2020, 8, 18)).build(); - HearingDay dayWithValue = hearingDay().withDurationMinutes(30).withHearingDate(LocalDate.of(2020, 8, 19)).build(); - - assertThat(HearingEnrichmentOrchestrator.getTotalDuration(List.of(dayWithNull, dayWithValue)), is(50)); - } - - // ── Static: recalculateDurationSequenceAndEndDatesForHearingDays(UpdateHearingForListing) ── - - @Test - void shouldSetWeekCommencingEndDateUsingExplicitDuration() { - LocalDate startDate = LocalDate.of(2020, 8, 17); - UpdateHearingForListing hearing = updateHearingForListing() - .withWeekCommencingStartDate(startDate) - .withWeekCommencingDurationInWeeks(2) - .build(); - - UpdateHearingForListing result = HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(hearing); - - assertThat(result.getWeekCommencingDurationInWeeks(), is(2)); - assertThat(result.getWeekCommencingEndDate(), is(startDate.plusWeeks(2).minusDays(1))); - } - - @Test - void shouldUseDefaultWeekDurationWhenWeekCommencingDurationIsNull() { - LocalDate startDate = LocalDate.of(2020, 8, 17); - UpdateHearingForListing hearing = updateHearingForListing() - .withWeekCommencingStartDate(startDate) - .build(); // weekCommencingDurationInWeeks = null → uses DEFAULT = 1 - - UpdateHearingForListing result = HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(hearing); - - assertThat(result.getWeekCommencingDurationInWeeks(), is(1)); - assertThat(result.getWeekCommencingEndDate(), is(startDate.plusWeeks(1).minusDays(1))); - } - - @Test - void shouldReturnUpdateHearingAsIsWhenHearingDaysIsEmpty() { - UpdateHearingForListing hearing = updateHearingForListing() - .withHearingDays(List.of()) - .build(); - - assertThat(HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(hearing), is(hearing)); - } - - @Test - void shouldSetEndDateToLastHearingDayAfterFilteringNonSittingDays() { - LocalDate date1 = LocalDate.of(2020, 8, 18); - LocalDate date2 = LocalDate.of(2020, 8, 19); - LocalDate date3 = LocalDate.of(2020, 8, 20); - HearingDay day1 = hearingDay().withHearingDate(date1).withDurationMinutes(30).withSequence(1).build(); - HearingDay day2 = hearingDay().withHearingDate(date2).withDurationMinutes(20).withSequence(2).build(); - HearingDay day3 = hearingDay().withHearingDate(date3).withDurationMinutes(10).withSequence(3).build(); - - UpdateHearingForListing hearing = updateHearingForListing() - .withHearingDays(List.of(day1, day2, day3)) - .withNonSittingDays(List.of(date2)) - .build(); - - UpdateHearingForListing result = HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(hearing); - - assertThat(result.getHearingDays(), hasSize(2)); - assertThat(result.getEndDate(), is(date3)); - } - - @Test - void shouldReturnOriginalUpdateHearingWhenAllHearingDaysAreNonSitting() { - LocalDate nonSittingDay = LocalDate.of(2020, 8, 18); - HearingDay day1 = hearingDay().withHearingDate(nonSittingDay).withDurationMinutes(30).withSequence(1).build(); - - UpdateHearingForListing hearing = updateHearingForListing() - .withHearingDays(List.of(day1)) - .withNonSittingDays(List.of(nonSittingDay)) - .build(); - - assertThat(HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(hearing), is(hearing)); - } - - // ── Static: recalculateDurationSequenceAndEndDatesForHearingDays(List) ─── - - @Test - void shouldHandleWeekCommencingHearingListingNeedsWithExplicitDuration() { - WeekCommencingDate weekCommencingDate = WeekCommencingDate.weekCommencingDate() - .withStartDate("2020-08-17").withDuration(3).build(); - - HearingListingNeeds hearing = hearingListingNeeds().withWeekCommencingDate(weekCommencingDate).build(); - - List result = - HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(List.of(hearing)); - - assertThat(result, hasSize(1)); - assertThat(result.get(0).getWeekCommencingDate().getDuration(), is(3)); - } - - @Test - void shouldUseDefaultDurationForWeekCommencingWhenNull() { - WeekCommencingDate weekCommencingDate = WeekCommencingDate.weekCommencingDate() - .withStartDate("2020-08-17").build(); // duration = null → DEFAULT = 1 - - HearingListingNeeds hearing = hearingListingNeeds().withWeekCommencingDate(weekCommencingDate).build(); - - List result = - HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(List.of(hearing)); - - assertThat(result, hasSize(1)); - assertThat(result.get(0).getWeekCommencingDate().getDuration(), is(1)); - } - - @Test - void shouldReturnHearingListingNeedsAsIsWhenNoHearingDays() { - HearingListingNeeds hearing = hearingListingNeeds().withHearingDays(List.of()).build(); - - List result = - HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(List.of(hearing)); - - assertThat(result, hasSize(1)); - assertThat(result.get(0), is(hearing)); - } - - @Test - void shouldCalculateEndDateAndEstimatedMinutesAfterFilteringNonSittingDays() { - LocalDate date1 = LocalDate.of(2020, 8, 18); - LocalDate date2 = LocalDate.of(2020, 8, 20); - HearingDay day1 = hearingDay().withHearingDate(date1).withDurationMinutes(30).withSequence(1).build(); - HearingDay day2 = hearingDay().withHearingDate(date2).withDurationMinutes(20).withSequence(2).build(); - - HearingListingNeeds hearing = hearingListingNeeds() - .withHearingDays(List.of(day1, day2)) - .withNonSittingDays(List.of(date1.toString())) - .build(); - - List result = - HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(List.of(hearing)); - - assertThat(result, hasSize(1)); - assertThat(result.get(0).getHearingDays(), hasSize(1)); - assertThat(result.get(0).getEndDate(), is(date2.toString())); - assertThat(result.get(0).getEstimatedMinutes(), is(20)); - } - - @Test - void shouldReturnOriginalHearingListingNeedsWhenAllDaysMatchNonSittingDays() { - LocalDate date1 = LocalDate.of(2020, 8, 18); - HearingDay day1 = hearingDay().withHearingDate(date1).withDurationMinutes(30).withSequence(1).build(); - - HearingListingNeeds hearing = hearingListingNeeds() - .withHearingDays(List.of(day1)) - .withNonSittingDays(List.of(date1.toString())) - .build(); - - List result = - HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(List.of(hearing)); - - assertThat(result, hasSize(1)); - assertThat(result.get(0), is(hearing)); - } - - @Test - void shouldCalculateEndDateWhenHearingDaysExistAndNonSittingDaysIsEmpty() { - LocalDate date1 = LocalDate.of(2020, 8, 18); - LocalDate date2 = LocalDate.of(2020, 8, 19); - HearingDay day1 = hearingDay().withHearingDate(date1).withDurationMinutes(30).withSequence(1).build(); - HearingDay day2 = hearingDay().withHearingDate(date2).withDurationMinutes(20).withSequence(2).build(); - - // No nonSittingDays → isEmpty branch produces new ArrayList<>() - HearingListingNeeds hearing = hearingListingNeeds() - .withHearingDays(List.of(day1, day2)) - .build(); - - List result = - HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(List.of(hearing)); - - assertThat(result, hasSize(1)); - assertThat(result.get(0).getEndDate(), is(date2.toString())); - assertThat(result.get(0).getEstimatedMinutes(), is(50)); - } - - // ── Static: logEnrichedHearings ─────────────────────────────────────────── - - @Test - void shouldLogEnrichedHearingsWithNullAndNonNullFields() { - HearingListingNeeds hearingWithNullDays = hearingListingNeeds().build(); - HearingListingNeeds hearingWithDays = hearingListingNeeds() - .withHearingDays(List.of( - hearingDay().withHearingDate(LocalDate.of(2020, 8, 18)) - .withDurationMinutes(30).withSequence(1).build())) - .withNonSittingDays(List.of("2020-08-19")) - .build(); - - List input = List.of(hearingWithNullDays, hearingWithDays); - - HearingEnrichmentOrchestrator.logEnrichedHearings(input); - - assertThat(input.get(0).getHearingDays(), is(hearingWithNullDays.getHearingDays())); - assertThat(input.get(1).getHearingDays(), hasSize(1)); - assertThat(input.get(1).getNonSittingDays(), is(List.of("2020-08-19"))); - } -} diff --git a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorTest.java b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorTest.java index 3d4c3260e..429f27df1 100644 --- a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorTest.java +++ b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/service/HearingEnrichmentOrchestratorTest.java @@ -1,6 +1,9 @@ package uk.gov.moj.cpp.listing.command.api.service; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -8,9 +11,15 @@ import static org.mockito.Mockito.when; import uk.gov.justice.core.courts.JurisdictionType; +import uk.gov.justice.core.courts.RotaSlot; +import uk.gov.justice.listing.commands.CourtCentreDetails; +import uk.gov.justice.listing.commands.HearingDay; import uk.gov.justice.listing.commands.HearingListingNeeds; +import uk.gov.justice.listing.commands.UpdateHearingForListing; import uk.gov.justice.services.messaging.JsonEnvelope; +import java.time.LocalDate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -53,6 +62,10 @@ public void setUp() { enrichedCrownHearing = mock(HearingListingNeeds.class); lenient().when(magistratesHearing.getJurisdictionType()).thenReturn(JurisdictionType.MAGISTRATES); lenient().when(crownHearing.getJurisdictionType()).thenReturn(JurisdictionType.CROWN); + // CROWN with courtScheduleId on bookedSlot -> triggers the CourtSchedule-first flow in enrichListCourtHearing. + // Tests that need the allocation-candidate flow should override getBookedSlots() to return null/empty. + lenient().when(crownHearing.getBookedSlots()).thenReturn(Collections.singletonList( + RotaSlot.rotaSlot().withCourtScheduleId(java.util.UUID.randomUUID().toString()).build())); } @Test @@ -67,7 +80,7 @@ public void shouldEnrichListCourtHearings() { // Mock objects for crown hearing chain HearingListingNeeds crownWithHearingDays = mock(HearingListingNeeds.class); - // Mock the enrichment chain for magistrates + // Mock the enrichment chain for magistrates (3 steps: days -> duration -> courtSchedule) when(hearingDaysEnrichmentService.enrichHearings(magistratesHearing, envelope)) .thenReturn(magsWithHearingDays); when(hearingDurationEnrichmentService.enrichWithDurations(magsWithHearingDays, envelope)) @@ -75,8 +88,11 @@ public void shouldEnrichListCourtHearings() { when(courtScheduleEnrichmentService.enrichWithCourtSchedules(magsWithDurations, envelope)) .thenReturn(enrichedMagistratesHearing); - // Mock the enrichment chain for crown - when(hearingDaysEnrichmentService.enrichHearings(crownHearing, envelope)) + // Mock the enrichment chain for crown (3 steps: crownCourtScheduleFirst -> days -> duration) + HearingListingNeeds crownWithCourtSchedules = mock(HearingListingNeeds.class); + when(courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(crownHearing)) + .thenReturn(crownWithCourtSchedules); + when(hearingDaysEnrichmentService.enrichHearings(crownWithCourtSchedules, envelope)) .thenReturn(crownWithHearingDays); when(hearingDurationEnrichmentService.enrichWithDurations(crownWithHearingDays, envelope)) .thenReturn(enrichedCrownHearing); @@ -93,7 +109,7 @@ public void shouldEnrichListCourtHearings() { @Test public void shouldEnrichListMagsHearing() { // Given - List hearings = Collections.singletonList(magistratesHearing); + List hearings = Arrays.asList(magistratesHearing); HearingListingNeeds withDurations = mock(HearingListingNeeds.class); HearingListingNeeds withHearingDays = mock(HearingListingNeeds.class); @@ -120,11 +136,15 @@ public void shouldEnrichListMagsHearing() { @Test public void shouldEnrichListCrownHearing() { // Given - List hearings = Collections.singletonList(crownHearing); + List hearings = Arrays.asList(crownHearing); + HearingListingNeeds withCourtSchedules = mock(HearingListingNeeds.class); HearingListingNeeds withHearingDays = mock(HearingListingNeeds.class); - when(hearingDaysEnrichmentService.enrichHearings(crownHearing, envelope)) + // CROWN order: crownCourtScheduleFirst -> days -> duration + when(courtScheduleEnrichmentService.enrichCrownCourtScheduleFirst(crownHearing)) + .thenReturn(withCourtSchedules); + when(hearingDaysEnrichmentService.enrichHearings(withCourtSchedules, envelope)) .thenReturn(withHearingDays); when(hearingDurationEnrichmentService.enrichWithDurations(withHearingDays, envelope)) .thenReturn(enrichedCrownHearing); @@ -133,10 +153,341 @@ public void shouldEnrichListCrownHearing() { List result = orchestrator.enrichListCourtHearing(hearings, envelope); // Then - verify(hearingDaysEnrichmentService).enrichHearings(crownHearing, envelope); + verify(courtScheduleEnrichmentService).enrichCrownCourtScheduleFirst(crownHearing); + verify(hearingDaysEnrichmentService).enrichHearings(withCourtSchedules, envelope); verify(hearingDurationEnrichmentService).enrichWithDurations(withHearingDays, envelope); assertEquals(1, result.size()); assertEquals(enrichedCrownHearing, result.get(0)); } -} + + @Test + public void shouldEnrichUpdateHearingForListingForCrown() { + // Given + UpdateHearingForListing crownUpdateHearing = mock(UpdateHearingForListing.class); + lenient().when(crownUpdateHearing.getJurisdictionType()).thenReturn(JurisdictionType.CROWN); + + UpdateHearingForListing withHearingDays = mock(UpdateHearingForListing.class); + UpdateHearingForListing withDuration = mock(UpdateHearingForListing.class); + UpdateHearingForListing enrichedUpdate = mock(UpdateHearingForListing.class); + + // Crown update has 3 enrichment steps: days -> duration -> courtSchedule + when(hearingDaysEnrichmentService.enrichHearing(crownUpdateHearing, envelope)) + .thenReturn(withHearingDays); + when(hearingDurationEnrichmentService.enrichWithDurationForUpdate(withHearingDays, envelope)) + .thenReturn(withDuration); + when(courtScheduleEnrichmentService.enrichWithCourtSchedules(withDuration, envelope)) + .thenReturn(enrichedUpdate); + + // When + UpdateHearingForListing result = orchestrator.enrichUpdateHearingForListing(crownUpdateHearing, envelope); + + // Then + verify(hearingDaysEnrichmentService).enrichHearing(crownUpdateHearing, envelope); + verify(hearingDurationEnrichmentService).enrichWithDurationForUpdate(withHearingDays, envelope); + verify(courtScheduleEnrichmentService).enrichWithCourtSchedules(withDuration, envelope); + assertEquals(enrichedUpdate, result); + } + + @Test + public void shouldEnrichUpdateHearingForListingWithCourtCentreDetailsForCrown() { + // Given + UpdateHearingForListing crownUpdateHearing = mock(UpdateHearingForListing.class); + lenient().when(crownUpdateHearing.getJurisdictionType()).thenReturn(JurisdictionType.CROWN); + CourtCentreDetails courtCentreDetails = mock(CourtCentreDetails.class); + + UpdateHearingForListing withHearingDays = mock(UpdateHearingForListing.class); + UpdateHearingForListing withDuration = mock(UpdateHearingForListing.class); + UpdateHearingForListing enrichedUpdate = mock(UpdateHearingForListing.class); + + // Crown update with courtCentreDetails has 3 enrichment steps: days -> duration -> courtSchedule + when(hearingDaysEnrichmentService.enrichHearing(crownUpdateHearing, envelope, courtCentreDetails)) + .thenReturn(withHearingDays); + when(hearingDurationEnrichmentService.enrichWithDurationForUpdate(withHearingDays, envelope)) + .thenReturn(withDuration); + when(courtScheduleEnrichmentService.enrichWithCourtSchedules(withDuration, envelope)) + .thenReturn(enrichedUpdate); + + // When + UpdateHearingForListing result = orchestrator.enrichUpdateHearingForListing(crownUpdateHearing, envelope, courtCentreDetails); + + // Then + verify(hearingDaysEnrichmentService).enrichHearing(crownUpdateHearing, envelope, courtCentreDetails); + verify(hearingDurationEnrichmentService).enrichWithDurationForUpdate(withHearingDays, envelope); + verify(courtScheduleEnrichmentService).enrichWithCourtSchedules(withDuration, envelope); + assertEquals(enrichedUpdate, result); + } + + // ─── MAGS update enrichment tests ──────────────────────────────────── + + @Test + public void shouldEnrichUpdateHearingForListingForMagistrates() { + // Given + UpdateHearingForListing magsUpdateHearing = mock(UpdateHearingForListing.class); + lenient().when(magsUpdateHearing.getJurisdictionType()).thenReturn(JurisdictionType.MAGISTRATES); + + UpdateHearingForListing withHearingDays = mock(UpdateHearingForListing.class); + UpdateHearingForListing withDuration = mock(UpdateHearingForListing.class); + UpdateHearingForListing enrichedUpdate = mock(UpdateHearingForListing.class); + + when(hearingDaysEnrichmentService.enrichHearing(magsUpdateHearing, envelope)) + .thenReturn(withHearingDays); + when(hearingDurationEnrichmentService.enrichWithDurationForUpdate(withHearingDays, envelope)) + .thenReturn(withDuration); + when(courtScheduleEnrichmentService.enrichWithCourtSchedules(withDuration, envelope)) + .thenReturn(enrichedUpdate); + + // When + UpdateHearingForListing result = orchestrator.enrichUpdateHearingForListing(magsUpdateHearing, envelope); + + // Then + verify(hearingDaysEnrichmentService).enrichHearing(magsUpdateHearing, envelope); + verify(hearingDurationEnrichmentService).enrichWithDurationForUpdate(withHearingDays, envelope); + verify(courtScheduleEnrichmentService).enrichWithCourtSchedules(withDuration, envelope); + assertEquals(enrichedUpdate, result); + } + + @Test + public void shouldEnrichUpdateHearingForListingWithCourtCentreDetailsForMagistrates() { + // Given + UpdateHearingForListing magsUpdateHearing = mock(UpdateHearingForListing.class); + lenient().when(magsUpdateHearing.getJurisdictionType()).thenReturn(JurisdictionType.MAGISTRATES); + CourtCentreDetails courtCentreDetails = mock(CourtCentreDetails.class); + + UpdateHearingForListing withHearingDays = mock(UpdateHearingForListing.class); + UpdateHearingForListing withDuration = mock(UpdateHearingForListing.class); + UpdateHearingForListing enrichedUpdate = mock(UpdateHearingForListing.class); + + when(hearingDaysEnrichmentService.enrichHearing(magsUpdateHearing, envelope, courtCentreDetails)) + .thenReturn(withHearingDays); + when(hearingDurationEnrichmentService.enrichWithDurationForUpdate(withHearingDays, envelope)) + .thenReturn(withDuration); + when(courtScheduleEnrichmentService.enrichWithCourtSchedules(withDuration, envelope)) + .thenReturn(enrichedUpdate); + + // When + UpdateHearingForListing result = orchestrator.enrichUpdateHearingForListing(magsUpdateHearing, envelope, courtCentreDetails); + + // Then + verify(hearingDaysEnrichmentService).enrichHearing(magsUpdateHearing, envelope, courtCentreDetails); + verify(hearingDurationEnrichmentService).enrichWithDurationForUpdate(withHearingDays, envelope); + verify(courtScheduleEnrichmentService).enrichWithCourtSchedules(withDuration, envelope); + assertEquals(enrichedUpdate, result); + } + + // ─── Unsupported jurisdiction type tests ───────────────────────────── + + @Test + public void shouldThrowExceptionForUnsupportedJurisdictionInEnrichListCourtHearing() { + HearingListingNeeds unsupportedHearing = mock(HearingListingNeeds.class); + when(unsupportedHearing.getJurisdictionType()).thenReturn(null); + + List hearings = Arrays.asList(unsupportedHearing); + + assertThrows(IllegalArgumentException.class, + () -> orchestrator.enrichListCourtHearing(hearings, envelope)); + } + + @Test + public void shouldThrowExceptionForUnsupportedJurisdictionInEnrichUpdateHearingForListing() { + UpdateHearingForListing unsupportedHearing = mock(UpdateHearingForListing.class); + when(unsupportedHearing.getJurisdictionType()).thenReturn(null); + + assertThrows(IllegalArgumentException.class, + () -> orchestrator.enrichUpdateHearingForListing(unsupportedHearing, envelope)); + } + + @Test + public void shouldThrowExceptionForUnsupportedJurisdictionInEnrichUpdateHearingForListingWithCourtCentre() { + UpdateHearingForListing unsupportedHearing = mock(UpdateHearingForListing.class); + when(unsupportedHearing.getJurisdictionType()).thenReturn(null); + CourtCentreDetails courtCentreDetails = mock(CourtCentreDetails.class); + + assertThrows(IllegalArgumentException.class, + () -> orchestrator.enrichUpdateHearingForListing(unsupportedHearing, envelope, courtCentreDetails)); + } + + // ─── Static utility method tests ───────────────────────────────────── + + @Test + public void shouldSequenceValidHearingDays() { + HearingDay day1 = HearingDay.hearingDay() + .withHearingDate(LocalDate.of(2026, 3, 2)) + .withDurationMinutes(240) + .build(); + HearingDay day2 = HearingDay.hearingDay() + .withHearingDate(LocalDate.of(2026, 3, 3)) + .withDurationMinutes(240) + .build(); + + List result = HearingEnrichmentOrchestrator.sequenceValidHearingDays(Arrays.asList(day1, day2)); + + assertThat(result.size(), is(2)); + assertThat(result.get(0).getSequence(), is(1)); + assertThat(result.get(1).getSequence(), is(2)); + } + + @Test + public void shouldOrderAndFilterOutNonSittingDays() { + LocalDate mar2 = LocalDate.of(2026, 3, 2); + LocalDate mar3 = LocalDate.of(2026, 3, 3); + LocalDate mar4 = LocalDate.of(2026, 3, 4); + + HearingDay day1 = HearingDay.hearingDay().withHearingDate(mar4).withDurationMinutes(240).build(); + HearingDay day2 = HearingDay.hearingDay().withHearingDate(mar2).withDurationMinutes(240).build(); + HearingDay day3 = HearingDay.hearingDay().withHearingDate(mar3).withDurationMinutes(240).build(); + + // mar3 is a non-sitting day + List result = HearingEnrichmentOrchestrator.orderAndFilterOutNonSittingDays( + Arrays.asList(day1, day2, day3), Arrays.asList(mar3)); + + assertThat(result.size(), is(2)); + assertThat(result.get(0).getHearingDate(), is(mar2)); + assertThat(result.get(1).getHearingDate(), is(mar4)); + // Sequence should be reset to 0 + assertThat(result.get(0).getSequence(), is(0)); + } + + @Test + public void shouldOrderAndFilterWithEmptyNonSittingDays() { + LocalDate mar2 = LocalDate.of(2026, 3, 2); + HearingDay day1 = HearingDay.hearingDay().withHearingDate(mar2).withDurationMinutes(360).build(); + + List result = HearingEnrichmentOrchestrator.orderAndFilterOutNonSittingDays( + Arrays.asList(day1), new ArrayList<>()); + + assertThat(result.size(), is(1)); + assertThat(result.get(0).getHearingDate(), is(mar2)); + } + + @Test + public void shouldGetTotalDuration() { + HearingDay day1 = HearingDay.hearingDay().withHearingDate(LocalDate.now()).withDurationMinutes(240).build(); + HearingDay day2 = HearingDay.hearingDay().withHearingDate(LocalDate.now().plusDays(1)).withDurationMinutes(360).build(); + + int total = HearingEnrichmentOrchestrator.getTotalDuration(Arrays.asList(day1, day2)); + + assertThat(total, is(600)); + } + + @Test + public void shouldGetTotalDurationWithNullMinutes() { + HearingDay day1 = HearingDay.hearingDay().withHearingDate(LocalDate.now()).withDurationMinutes(240).build(); + HearingDay day2 = HearingDay.hearingDay().withHearingDate(LocalDate.now().plusDays(1)).build(); // null duration + + int total = HearingEnrichmentOrchestrator.getTotalDuration(Arrays.asList(day1, day2)); + + // null durationMinutes defaults to DEFAULT_MIN (20) + assertThat(total, is(260)); + } + + // ─── recalculateDurationSequenceAndEndDatesForHearingDays (UpdateHearingForListing) tests ───── + + @Test + public void shouldReturnUnchangedUpdateHearingWhenHearingDaysEmpty() { + UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withHearingDays(Collections.emptyList()) + .build(); + + UpdateHearingForListing result = HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(hearing); + + assertThat(result.getHearingDays(), is(Collections.emptyList())); + } + + @Test + public void shouldRecalculateEndDateForUpdateHearing() { + LocalDate mar2 = LocalDate.of(2026, 3, 2); + LocalDate mar3 = LocalDate.of(2026, 3, 3); + + HearingDay day1 = HearingDay.hearingDay().withHearingDate(mar2).withDurationMinutes(360).build(); + HearingDay day2 = HearingDay.hearingDay().withHearingDate(mar3).withDurationMinutes(360).build(); + + UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withHearingDays(Arrays.asList(day1, day2)) + .build(); + + UpdateHearingForListing result = HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(hearing); + + assertThat(result.getEndDate(), is(mar3)); + assertThat(result.getHearingDays().size(), is(2)); + } + + @Test + public void shouldFilterNonSittingDaysFromUpdateHearing() { + LocalDate mar2 = LocalDate.of(2026, 3, 2); + LocalDate mar3 = LocalDate.of(2026, 3, 3); + LocalDate mar4 = LocalDate.of(2026, 3, 4); + + HearingDay day1 = HearingDay.hearingDay().withHearingDate(mar2).withDurationMinutes(360).build(); + HearingDay day2 = HearingDay.hearingDay().withHearingDate(mar3).withDurationMinutes(360).build(); + HearingDay day3 = HearingDay.hearingDay().withHearingDate(mar4).withDurationMinutes(360).build(); + + UpdateHearingForListing hearing = UpdateHearingForListing.updateHearingForListing() + .withHearingDays(Arrays.asList(day1, day2, day3)) + .withNonSittingDays(Collections.singletonList(mar3)) + .build(); + + UpdateHearingForListing result = HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays(hearing); + + assertThat(result.getHearingDays().size(), is(2)); + assertThat(result.getEndDate(), is(mar4)); + } + + // ─── recalculateDurationSequenceAndEndDatesForHearingDays (List) tests ── + + @Test + public void shouldRecalculateEndDateForHearingListingNeeds() { + LocalDate mar2 = LocalDate.of(2026, 3, 2); + LocalDate mar4 = LocalDate.of(2026, 3, 4); + + HearingDay day1 = HearingDay.hearingDay().withHearingDate(mar2).withDurationMinutes(360).build(); + HearingDay day2 = HearingDay.hearingDay().withHearingDate(mar4).withDurationMinutes(360).build(); + + HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withHearingDays(Arrays.asList(day1, day2)) + .withEstimatedMinutes(720) + .build(); + + List result = HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays( + Arrays.asList(hearing)); + + assertThat(result.size(), is(1)); + assertThat(result.get(0).getEndDate(), is(mar4.toString())); + assertThat(result.get(0).getEstimatedMinutes(), is(720)); + } + + @Test + public void shouldPreserveHearingWithEmptyHearingDays() { + HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withEstimatedMinutes(240) + .build(); + + List result = HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays( + Arrays.asList(hearing)); + + assertThat(result.size(), is(1)); + assertThat(result.get(0).getEstimatedMinutes(), is(240)); + } + + @Test + public void shouldFilterNonSittingDaysForHearingListingNeeds() { + LocalDate mar2 = LocalDate.of(2026, 3, 2); + LocalDate mar3 = LocalDate.of(2026, 3, 3); + + HearingDay day1 = HearingDay.hearingDay().withHearingDate(mar2).withDurationMinutes(360).build(); + HearingDay day2 = HearingDay.hearingDay().withHearingDate(mar3).withDurationMinutes(360).build(); + + HearingListingNeeds hearing = HearingListingNeeds.hearingListingNeeds() + .withHearingDays(Arrays.asList(day1, day2)) + .withNonSittingDays(Collections.singletonList(mar3.toString())) + .withEstimatedMinutes(720) + .build(); + + List result = HearingEnrichmentOrchestrator.recalculateDurationSequenceAndEndDatesForHearingDays( + Arrays.asList(hearing)); + + assertThat(result.size(), is(1)); + assertThat(result.get(0).getHearingDays().size(), is(1)); + assertThat(result.get(0).getEndDate(), is(mar2.toString())); + } +} \ No newline at end of file diff --git a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/util/NonDefaultDayConverterTest.java b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/util/NonDefaultDayConverterTest.java index 64df43623..95f92319e 100644 --- a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/util/NonDefaultDayConverterTest.java +++ b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/util/NonDefaultDayConverterTest.java @@ -99,6 +99,8 @@ public void shouldConvertCommandNonDefaultDaysToHearingDays() { assertThat(result.get(0).getCourtScheduleId(), is(UUID.fromString(courtScheduleId))); assertThat(result.get(0).getDurationMinutes(), is(30)); assertThat(result.get(0).getHearingDate(), is(startTime.toLocalDate())); + assertThat(result.get(0).getEndTime(), is(startTime.plusMinutes(30))); + } @Test @@ -142,6 +144,7 @@ public void shouldConvertBookedSlotsToHearingDays() { assertThat(result.get(0).getDurationMinutes(), is(30)); assertThat(result.get(0).getHearingDate(), is(startTime.toLocalDate())); assertThat(result.get(0).getStartTime(), is(startTime)); + assertThat(result.get(0).getEndTime(), is(startTime.plusMinutes(30))); } @Test @@ -155,4 +158,29 @@ public void shouldReturnEmptyListWhenNoBookedSlotsProvided() { // Then assertThat(result, is(empty())); } + + @Test + public void shouldConvertBookedSlotsToHearingDays_whenRoomIdIsNull() { + // Given — adhoc Crown payload without roomId in bookedSlots + String courtCentreId = UUID.randomUUID().toString(); + String courtScheduleId = UUID.randomUUID().toString(); + ZonedDateTime startTime = ZonedDateTime.of(LocalDateTime.of(2026, 4, 13, 9, 0), UTC); + + RotaSlot slot = RotaSlot.rotaSlot() + .withCourtCentreId(courtCentreId) + .withCourtScheduleId(courtScheduleId) + .withStartTime(startTime) + .withDuration(20) + .build(); + + // When + List result = NonDefaultDayConverter.convertBookedSlotsToHearingDays(List.of(slot)); + + // Then + assertThat(result, hasSize(1)); + assertThat(result.get(0).getCourtCentreId(), is(UUID.fromString(courtCentreId))); + assertThat(result.get(0).getCourtScheduleId(), is(UUID.fromString(courtScheduleId))); + assertThat(result.get(0).getCourtRoomId(), is((UUID) null)); + assertThat(result.get(0).getDurationMinutes(), is(20)); + } } \ No newline at end of file diff --git a/listing-command/listing-command-api/src/test/resources/courtscheduler.search.book.hearing.slots.draft.json b/listing-command/listing-command-api/src/test/resources/courtscheduler.search.book.hearing.slots.draft.json new file mode 100644 index 000000000..885436da4 --- /dev/null +++ b/listing-command/listing-command-api/src/test/resources/courtscheduler.search.book.hearing.slots.draft.json @@ -0,0 +1,10 @@ +{ + "hearingSlots": { + "hearingId": "5416c10a-0cf1-49d5-a7c9-5761ff3bdf2c", + "courtScheduleId": "23681024-8eac-4890-8c44-4651ad48cb24", + "courtRoomId": "573bd1e6-92fa-49c2-8fa9-a355c1a4cded", + "hearingStartTime": "2020-05-26T09:00:000Z", + "duration": 20, + "isDraft": true + } +} diff --git a/listing-command/listing-command-api/src/test/resources/courtscheduler.search.book.hearing.slots.json b/listing-command/listing-command-api/src/test/resources/courtscheduler.search.book.hearing.slots.json index 83c55978e..fe96d6abc 100644 --- a/listing-command/listing-command-api/src/test/resources/courtscheduler.search.book.hearing.slots.json +++ b/listing-command/listing-command-api/src/test/resources/courtscheduler.search.book.hearing.slots.json @@ -4,6 +4,7 @@ "courtScheduleId": "23681024-8eac-4890-8c44-4651ad48cb24", "courtRoomId": "573bd1e6-92fa-49c2-8fa9-a355c1a4cded", "hearingStartTime": "2020-05-26T09:00:000Z", - "duration": 20 + "duration": 20, + "isDraft": false } } diff --git a/listing-command/listing-command-api/src/test/resources/courtscheduler.search.book.hearing.slots.with.judiciaries.json b/listing-command/listing-command-api/src/test/resources/courtscheduler.search.book.hearing.slots.with.judiciaries.json new file mode 100644 index 000000000..c166f0b9f --- /dev/null +++ b/listing-command/listing-command-api/src/test/resources/courtscheduler.search.book.hearing.slots.with.judiciaries.json @@ -0,0 +1,24 @@ +{ + "hearingSlots": { + "hearingId": "5416c10a-0cf1-49d5-a7c9-5761ff3bdf2c", + "courtScheduleId": "23681024-8eac-4890-8c44-4651ad48cb24", + "courtRoomId": "573bd1e6-92fa-49c2-8fa9-a355c1a4cded", + "hearingStartTime": "2020-05-26T09:00:000Z", + "duration": 20, + "isDraft": false, + "judiciaries": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "judiciaryType": "CIRCUIT_JUDGE", + "isBenchChairman": true, + "isDeputy": false + }, + { + "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "judiciaryType": "RECORDER", + "isBenchChairman": false, + "isDeputy": true + } + ] + } +} diff --git a/listing-command/listing-command-api/src/test/resources/listing.command.hearingSlots.stub-data.json b/listing-command/listing-command-api/src/test/resources/listing.command.hearingSlots.stub-data.json index 286712f7b..2405666ac 100644 --- a/listing-command/listing-command-api/src/test/resources/listing.command.hearingSlots.stub-data.json +++ b/listing-command/listing-command-api/src/test/resources/listing.command.hearingSlots.stub-data.json @@ -23,28 +23,28 @@ "availableDuration": 0, "judiciaries": [ { - "judiciaryId": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", + "id": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false }, { - "judiciaryId": "d424f73e-4a19-396d-a00c-f4c38d1c864e", + "id": "d424f73e-4a19-396d-a00c-f4c38d1c864e", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": false, - "benchChairman": true + "isDeputy": false, + "isBenchChairman": true }, { - "judiciaryId": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", + "id": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false } ], "slotStartTimes": [] diff --git a/listing-command/listing-command-handler/pom.xml b/listing-command/listing-command-handler/pom.xml index 6382b2ea6..2da407778 100644 --- a/listing-command/listing-command-handler/pom.xml +++ b/listing-command/listing-command-handler/pom.xml @@ -3,7 +3,7 @@ listing-command uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/handler/ListingCommandHandler.java b/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/handler/ListingCommandHandler.java index a9ecb14dd..157216ccf 100644 --- a/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/handler/ListingCommandHandler.java +++ b/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/handler/ListingCommandHandler.java @@ -662,8 +662,7 @@ public void extendHearingForHearing(final JsonEnvelope command) throws EventStre final Stream allocationEvents = hearing.applyAllocationRulesForExtendedHearing(unallocatedHearingPersisted, fullExtension, extendHearingForHearingEnriched.getSendNotificationToParties()); final Stream addCaseEvent = hearing.addCasesToUnAllocatedHearing(casesToMove, unAllocatedHearingId); final Stream hearingMarkedForPartialUpdated = hearing.markUnallocatedHearingForPartialUpdate(unAllocatedHearingId, prosecutionCasesToBeRemovedFromHearing); - final Stream emitYouthCourtListRestrictionsEvents = hearing.emitYouthCourtListRestrictions(); - return Stream.of(addCaseEvent, updatedHearing, allocationEvents, hearingMarkedForPartialUpdated, emitYouthCourtListRestrictionsEvents).flatMap(i -> i); + return Stream.of(addCaseEvent, updatedHearing, allocationEvents, hearingMarkedForPartialUpdated).flatMap(i -> i); }); } else { @@ -1650,7 +1649,6 @@ private uk.gov.moj.cpp.listing.domain.RestrictCourtList convertRestrictCourtList .withCourtApplicationApplicantIds(restrictCourtList.getCourtApplicationApplicantIds()) .withCourtApplicatonIds(restrictCourtList.getCourtApplicationIds()) .withCourtApplicatonRespondentIds(restrictCourtList.getCourtApplicationRespondentIds()) - .withCourtApplicationSubjectIds(restrictCourtList.getCourtApplicationSubjectIds()) .withCourtApplicationType(restrictCourtList.getCourtApplicationType()) .withRestrictFromCourtList(restrictCourtList.getRestrictCourtList()) .build(); diff --git a/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/service/HearingService.java b/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/service/HearingService.java index ba89a6562..9be7f52ca 100644 --- a/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/service/HearingService.java +++ b/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/service/HearingService.java @@ -5,7 +5,6 @@ import org.slf4j.LoggerFactory; import uk.gov.justice.services.core.annotation.ServiceComponent; import uk.gov.justice.services.core.enveloper.Enveloper; -import uk.gov.justice.services.core.requester.Requester; import uk.gov.justice.services.messaging.JsonEnvelope; import uk.gov.justice.services.messaging.Metadata; import uk.gov.moj.cpp.listing.query.view.HearingQueryView; @@ -15,7 +14,6 @@ import java.util.UUID; import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; -import static uk.gov.justice.services.core.annotation.Component.COMMAND_API; import static uk.gov.justice.services.core.annotation.Component.QUERY_API; import static uk.gov.justice.services.messaging.JsonEnvelope.metadataFrom; diff --git a/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/CommandToDomainConverter.java b/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/CommandToDomainConverter.java index f361c9c69..1a87087a5 100644 --- a/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/CommandToDomainConverter.java +++ b/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/CommandToDomainConverter.java @@ -36,7 +36,6 @@ import uk.gov.moj.cpp.listing.domain.CaseMarker; import uk.gov.moj.cpp.listing.domain.CivilOffence; import uk.gov.moj.cpp.listing.domain.CommittingCourt; -import uk.gov.moj.cpp.listing.domain.CourtHouseType; import uk.gov.moj.cpp.listing.domain.CourtApplicationPartyListingNeeds; import uk.gov.moj.cpp.listing.domain.CourtCentreDefaults; import uk.gov.moj.cpp.listing.domain.Hearing; @@ -108,15 +107,11 @@ public static ZonedDateTime getStartDateTime(final HearingListingNeeds commandHe private static Optional buildCommittingCourt(final uk.gov.justice.core.courts.CommittingCourt committingCourt) { - final CommittingCourt.Builder builder = CommittingCourt.committingCourt() - .withCourtCentreId(committingCourt.getCourtCentreId()) + return of(CommittingCourt.committingCourt().withCourtCentreId(committingCourt.getCourtCentreId()) .withCourtHouseCode(committingCourt.getCourtHouseCode()) .withCourtHouseName(committingCourt.getCourtHouseName()) - .withCourtHouseShortName(committingCourt.getCourtHouseShortName()); - if (committingCourt.getCourtHouseType() != null) { - CourtHouseType.valueFor(committingCourt.getCourtHouseType().name()).ifPresent(builder::withCourtHouseType); - } - return of(builder.build()); + .withCourtHouseShortName(committingCourt.getCourtHouseShortName()) + .build()); } @SuppressWarnings({"squid:S3655"}) diff --git a/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/CourtApplicationToDomainConverter.java b/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/CourtApplicationToDomainConverter.java index 015775dc8..179a22307 100644 --- a/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/CourtApplicationToDomainConverter.java +++ b/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/CourtApplicationToDomainConverter.java @@ -114,11 +114,6 @@ private CourtApplicationParty buildCourtSubject(final uk.gov.justice.core.courts private CourtApplicationParty buildCourtApplicationParty(final uk.gov.justice.core.courts.CourtApplicationParty courtApplicationParty, final boolean isRespondent) { - final UUID masterDefendantId = ofNullable(courtApplicationParty.getMasterDefendant()) - .map(uk.gov.justice.core.courts.MasterDefendant::getMasterDefendantId) - .orElse(null); - final String dateOfBirth = extractDateOfBirth(courtApplicationParty); - CourtApplicationParty applicationParty = ofNullable(courtApplicationParty.getPersonDetails()) .map(person -> getCourtApplicationParty( courtApplicationParty.getId(), @@ -126,17 +121,14 @@ private CourtApplicationParty buildCourtApplicationParty(final uk.gov.justice.co ofNullable(person.getFirstName()), person.getLastName(), PERSON, - ofNullable(person.getAddress()), - masterDefendantId, - dateOfBirth)) + ofNullable(person.getAddress()))) .orElse(null); if (isNull(applicationParty)) { applicationParty = ofNullable(courtApplicationParty.getMasterDefendant()) .map(defendant -> getCourtApplicationPartyForLegalEntityDefendant( courtApplicationParty.getId(), isRespondent, - ofNullable(defendant.getLegalEntityDefendant()), - masterDefendantId)) + ofNullable(defendant.getLegalEntityDefendant()))) .orElse(null); } @@ -147,9 +139,7 @@ private CourtApplicationParty buildCourtApplicationParty(final uk.gov.justice.co isRespondent, empty(), organisation.getName(), ORGANISATION, - ofNullable(organisation.getAddress()), - masterDefendantId, - dateOfBirth)) + ofNullable(organisation.getAddress()))) .orElse(null); } if (isNull(applicationParty)) { @@ -160,9 +150,7 @@ isRespondent, empty(), empty(), prosecutingAuthority.getProsecutionAuthorityCode(), PROSECUTING_AUTHORITY, - ofNullable(prosecutingAuthority.getAddress()), - masterDefendantId, - dateOfBirth)) + ofNullable(prosecutingAuthority.getAddress()))) .orElse(null); } if (isNull(applicationParty)) { @@ -170,57 +158,34 @@ isRespondent, empty(), .map(defendant -> getCourtApplicationParty( courtApplicationParty.getId(), isRespondent, - ofNullable(defendant.getPersonDefendant()), - masterDefendantId, - dateOfBirth)) + ofNullable(defendant.getPersonDefendant()))) .orElse(null); } return applicationParty; } - private String extractDateOfBirth(final uk.gov.justice.core.courts.CourtApplicationParty courtApplicationParty) { - final String dateOfBirth = ofNullable(courtApplicationParty.getPersonDetails()) - .map(uk.gov.justice.core.courts.Person::getDateOfBirth) - .orElse(null); - if (nonNull(dateOfBirth)) { - return dateOfBirth; - } - return ofNullable(courtApplicationParty.getMasterDefendant()) - .map(uk.gov.justice.core.courts.MasterDefendant::getPersonDefendant) - .map(uk.gov.justice.core.courts.PersonDefendant::getPersonDetails) - .map(uk.gov.justice.core.courts.Person::getDateOfBirth) - .orElse(null); - } - - private CourtApplicationParty getCourtApplicationParty(final UUID id, final boolean isRespondent, final Optional personDefendant, - final UUID masterDefendantId, final String dateOfBirth) { + private CourtApplicationParty getCourtApplicationParty(final UUID id, final boolean isRespondent, final Optional personDefendant) { return personDefendant.map(defendant -> getCourtApplicationParty( id, isRespondent, ofNullable(defendant.getPersonDetails().getFirstName()), defendant.getPersonDetails().getLastName(), PERSON_DEFENDANT, - ofNullable(defendant.getPersonDetails().getAddress()), - masterDefendantId, - dateOfBirth)) + ofNullable(defendant.getPersonDetails().getAddress()))) .orElse(null); } - private CourtApplicationParty getCourtApplicationPartyForLegalEntityDefendant(final UUID id, final boolean isRespondent, final Optional legalEntityDefendant, - final UUID masterDefendantId) { + private CourtApplicationParty getCourtApplicationPartyForLegalEntityDefendant(final UUID id, final boolean isRespondent, final Optional legalEntityDefendant) { return legalEntityDefendant.map(entityDefendant -> getCourtApplicationParty( id, isRespondent, empty(), entityDefendant.getOrganisation().getName(), PERSON, - ofNullable(entityDefendant.getOrganisation().getAddress()), - masterDefendantId, - null)).orElse(null); + ofNullable(entityDefendant.getOrganisation().getAddress()))).orElse(null); } - private CourtApplicationParty getCourtApplicationParty(final UUID id, final boolean isRespondent, final Optional firstName, final String lastName, final CourtApplicationPartyType type, Optional address, - final UUID masterDefendantId, final String dateOfBirth) { + private CourtApplicationParty getCourtApplicationParty(final UUID id, final boolean isRespondent, final Optional firstName, final String lastName, final CourtApplicationPartyType type, Optional address) { return CourtApplicationParty.courtApplicationParty() .withId(id) .withFirstName(firstName.orElse(null)) @@ -228,8 +193,6 @@ private CourtApplicationParty getCourtApplicationParty(final UUID id, final bool .withIsRespondent(isRespondent) .withCourtApplicationPartyType(type) .withAddress(buildAddress(address)) - .withMasterDefendantId(masterDefendantId) - .withDateOfBirth(dateOfBirth) .build(); } @@ -258,8 +221,6 @@ private CourtApplicationParty getApplicant(final Applicant applicant) { .withIsRespondent(false) .withCourtApplicationPartyType(buildCourtApplicationPartyType(applicant.getCourtApplicationPartyType())) .withAddress(buildAddress(ofNullable(applicant.getAddress()))) - .withMasterDefendantId(applicant.getMasterDefendantId()) - .withDateOfBirth(applicant.getDateOfBirth()) .build(); } @@ -271,8 +232,6 @@ private CourtApplicationParty getRespondent(final Respondents respondents) { .withIsRespondent(true) .withCourtApplicationPartyType(buildCourtApplicationPartyType(respondents.getCourtApplicationPartyType())) .withAddress(buildAddress(ofNullable(respondents.getAddress()))) - .withMasterDefendantId(respondents.getMasterDefendantId()) - .withDateOfBirth(respondents.getDateOfBirth()) .build(); } @@ -284,8 +243,6 @@ private CourtApplicationParty getSubject(final Subject subject) { .withIsRespondent(false) .withCourtApplicationPartyType(buildCourtApplicationPartyType(subject.getCourtApplicationPartyType())) .withAddress(buildAddress(ofNullable(subject.getAddress()))) - .withMasterDefendantId(subject.getMasterDefendantId()) - .withDateOfBirth(subject.getDateOfBirth()) .build(); } diff --git a/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCommandToDomainConverter.java b/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCommandToDomainConverter.java index fbd34249b..4370dc443 100644 --- a/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCommandToDomainConverter.java +++ b/listing-command/listing-command-handler/src/main/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCommandToDomainConverter.java @@ -24,9 +24,13 @@ public List convert(final List convert(final List judicialRoles.add(JudicialRole.judicialRole() - .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("benchChairman"))) - .withIsDeputy(of(judiciaryJsonObject.getBoolean("deputy"))) - .withJudicialId(fromString(judiciaryJsonObject.getString("judiciaryId"))) + .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("isBenchChairman"))) + .withIsDeputy(of(judiciaryJsonObject.getBoolean("isDeputy"))) + .withJudicialId(fromString(judiciaryJsonObject.getString("id"))) .withJudicialRoleType( JudicialRoleType.judicialRoleType() .withJudiciaryType(judiciaryJsonObject.getString("judiciaryType")) @@ -1174,9 +1174,9 @@ public void listingCommandHandlerShouldUpdateHearingForListingWithoutJudiciaries .map(JsonObject.class::cast) .forEach(judiciaryJsonObject -> judicialRoles.add(JudicialRole.judicialRole() - .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("benchChairman"))) - .withIsDeputy(of(judiciaryJsonObject.getBoolean("deputy"))) - .withJudicialId(fromString(judiciaryJsonObject.getString("judiciaryId"))) + .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("isBenchChairman"))) + .withIsDeputy(of(judiciaryJsonObject.getBoolean("isDeputy"))) + .withJudicialId(fromString(judiciaryJsonObject.getString("id"))) .withJudicialRoleType( JudicialRoleType.judicialRoleType() .withJudiciaryType(judiciaryJsonObject.getString("judiciaryType")) diff --git a/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/CommandToDomainConverterTest.java b/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/CommandToDomainConverterTest.java index a8eaab6c8..ec257797b 100644 --- a/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/CommandToDomainConverterTest.java +++ b/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/CommandToDomainConverterTest.java @@ -8,14 +8,8 @@ import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNot.not; import static uk.gov.moj.cpp.listing.command.utils.CommandToDomainConverter.REFERRAL_REASON_FOR_DISQUALIFICATION; -import static uk.gov.moj.cpp.listing.command.utils.CommandToDomainConverter.extractStartDate; -import static uk.gov.moj.cpp.listing.command.utils.CommandToDomainConverter.getStartDateTime; -import static uk.gov.moj.cpp.listing.domain.CourtHouseType.CROWN; -import static uk.gov.moj.cpp.listing.domain.CourtHouseType.MAGISTRATES; -import uk.gov.justice.core.courts.CommittingCourt; import uk.gov.justice.core.courts.DefendantListingNeeds; -import uk.gov.justice.core.courts.JurisdictionType; import uk.gov.justice.core.courts.ProsecutionCase; import uk.gov.justice.core.courts.RotaSlot; import uk.gov.justice.listing.commands.HearingListingNeeds; @@ -459,112 +453,4 @@ public void shouldNotSetIsPossibleDisqualificationFlagWhenListingReasonNotForDis //then assertThat(actual.getIsPossibleDisqualification().isPresent(), is(false)); } - - @Test - void shouldMapCommittingCourtMagistratesJurisdictionToDomainCourtHouseType() { - final UUID offenceId = UUID.randomUUID(); - final UUID courtCentreId = UUID.randomUUID(); - final uk.gov.justice.core.courts.Offence commandOffence = uk.gov.justice.core.courts.Offence.offence() - .withId(offenceId) - .withOffenceCode("OC1") - .withWording("wording") - .withStartDate("2018-01-01") - .withOrderIndex(1) - .withCount(1) - .withOffenceTitle("title") - .withCommittingCourt(CommittingCourt.committingCourt() - .withCourtCentreId(courtCentreId) - .withCourtHouseName("Test Magistrates") - .withCourtHouseCode("430") - .withCourtHouseShortName("430") - .withCourtHouseType(JurisdictionType.MAGISTRATES) - .build()) - .build(); - - final Offence domainOffence = commandToDomainConverter.buildOffence(commandOffence, Collections.emptyList()); - - assertThat(domainOffence.getCommittingCourt().isPresent(), is(true)); - assertThat(domainOffence.getCommittingCourt().get().getCourtCentreId(), is(courtCentreId)); - assertThat(domainOffence.getCommittingCourt().get().getCourtHouseName(), is("Test Magistrates")); - assertThat(domainOffence.getCommittingCourt().get().getCourtHouseCode().get(), is("430")); - assertThat(domainOffence.getCommittingCourt().get().getCourtHouseType(), is(MAGISTRATES)); - } - - @Test - void shouldMapCommittingCourtCrownJurisdictionToDomainCourtHouseType() { - final UUID offenceId = UUID.randomUUID(); - final UUID courtCentreId = UUID.randomUUID(); - final uk.gov.justice.core.courts.Offence commandOffence = uk.gov.justice.core.courts.Offence.offence() - .withId(offenceId) - .withOffenceCode("OC1") - .withWording("wording") - .withStartDate("2018-01-01") - .withOrderIndex(1) - .withCount(1) - .withOffenceTitle("title") - .withCommittingCourt(CommittingCourt.committingCourt() - .withCourtCentreId(courtCentreId) - .withCourtHouseName("Test Crown") - .withCourtHouseCode("001") - .withCourtHouseShortName("001") - .withCourtHouseType(JurisdictionType.CROWN) - .build()) - .build(); - - final Offence domainOffence = commandToDomainConverter.buildOffence(commandOffence, Collections.emptyList()); - - assertThat(domainOffence.getCommittingCourt().get().getCourtHouseType(), is(CROWN)); - } - - @Test - void shouldMapCommittingCourtWithoutJurisdictionLeavingDomainCourtHouseTypeUnset() { - final UUID offenceId = UUID.randomUUID(); - final UUID courtCentreId = UUID.randomUUID(); - final uk.gov.justice.core.courts.Offence commandOffence = uk.gov.justice.core.courts.Offence.offence() - .withId(offenceId) - .withOffenceCode("OC1") - .withWording("wording") - .withStartDate("2018-01-01") - .withOrderIndex(1) - .withCount(1) - .withOffenceTitle("title") - .withCommittingCourt(CommittingCourt.committingCourt() - .withCourtCentreId(courtCentreId) - .withCourtHouseName("Unspecified type court") - .withCourtHouseCode("999") - .withCourtHouseShortName("999") - .build()) - .build(); - - final Offence domainOffence = commandToDomainConverter.buildOffence(commandOffence, Collections.emptyList()); - - assertThat(domainOffence.getCommittingCourt().isPresent(), is(true)); - assertThat(domainOffence.getCommittingCourt().get().getCourtHouseName(), is("Unspecified type court")); - assertThat(domainOffence.getCommittingCourt().get().getCourtHouseType(), is(nullValue())); - } - - @Test - void shouldMapOffenceWithoutCommittingCourtToDomainWithoutCommittingCourt() { - final UUID offenceId = UUID.randomUUID(); - final uk.gov.justice.core.courts.Offence commandOffence = uk.gov.justice.core.courts.Offence.offence() - .withId(offenceId) - .withOffenceCode("OC1") - .withWording("wording") - .withStartDate("2018-01-01") - .withOrderIndex(1) - .withCount(1) - .withOffenceTitle("title") - .build(); - - final Offence domainOffence = commandToDomainConverter.buildOffence(commandOffence, Collections.emptyList()); - - assertThat(domainOffence.getCommittingCourt().isPresent(), is(false)); - } - - @Test - void extractStartDateShouldMatchGetStartDateTimeWhenListedStartPresent() { - final HearingListingNeeds commandHearing = commandBuilder.buildHearingWithListedStartDateTime(); - - assertThat(extractStartDate(commandHearing), is(getStartDateTime(commandHearing))); - } } diff --git a/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCommandToDomainConverterTest.java b/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCommandToDomainConverterTest.java index 3e5eab7f6..bc2d20666 100644 --- a/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCommandToDomainConverterTest.java +++ b/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCommandToDomainConverterTest.java @@ -1,162 +1,154 @@ package uk.gov.moj.cpp.listing.command.utils; +import static java.util.Collections.emptyList; +import static java.util.UUID.randomUUID; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.core.Is.is; -import static uk.gov.justice.listing.commands.HearingDay.hearingDay; import uk.gov.justice.listing.commands.HearingDay; import java.time.LocalDate; +import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.Test; -public class HearingDaysCommandToDomainConverterTest { +class HearingDaysCommandToDomainConverterTest { private final HearingDaysCommandToDomainConverter converter = new HearingDaysCommandToDomainConverter(); - private static final UUID COURT_CENTRE_ID = UUID.fromString("11111111-1111-1111-1111-111111111111"); - private static final UUID COURT_ROOM_ID = UUID.fromString("22222222-2222-2222-2222-222222222222"); - private static final UUID COURT_SCHEDULE_ID = UUID.fromString("33333333-3333-3333-3333-333333333333"); - private static final LocalDate HEARING_DATE = LocalDate.of(2020, 8, 18); - private static final ZonedDateTime START_TIME = ZonedDateTime.parse("2020-08-18T01:22:12.381Z"); - private static final ZonedDateTime END_TIME = ZonedDateTime.parse("2020-08-18T02:22:12.381Z"); - @Test - public void shouldReturnEmptyListWhenSourceIsNull() { - final List result = converter.convert(null); - - assertThat(result, hasSize(0)); + void shouldReturnEmptyListWhenSourceIsNull() { + assertThat(converter.convert(null), is(empty())); } @Test - public void shouldReturnEmptyListWhenSourceIsEmpty() { - final List result = converter.convert(Collections.emptyList()); - - assertThat(result, hasSize(0)); + void shouldReturnEmptyListWhenSourceIsEmpty() { + assertThat(converter.convert(emptyList()), is(empty())); } @Test - public void shouldConvertAllFieldsFromCommandToDomain() { - final HearingDay command = hearingDay() - .withCourtCentreId(COURT_CENTRE_ID) - .withCourtRoomId(COURT_ROOM_ID) - .withCourtScheduleId(COURT_SCHEDULE_ID) - .withDurationMinutes(30) - .withSequence(1) - .withIsCancelled(false) - .withHearingDate(HEARING_DATE) - .withStartTime(START_TIME) - .withEndTime(END_TIME) + void shouldPreserveEndTimeWhenExplicitlySet() { + final UUID courtCentreId = randomUUID(); + final ZonedDateTime startTime = ZonedDateTime.of(2026, 4, 10, 10, 0, 0, 0, ZoneOffset.UTC); + final ZonedDateTime endTime = ZonedDateTime.of(2026, 4, 10, 14, 0, 0, 0, ZoneOffset.UTC); + + final HearingDay commandDay = HearingDay.hearingDay() + .withCourtCentreId(courtCentreId) + .withHearingDate(startTime.toLocalDate()) + .withStartTime(startTime) + .withEndTime(endTime) + .withDurationMinutes(240) + .withSequence(0) .build(); - final List result = converter.convert(List.of(command)); + final List result = converter.convert(List.of(commandDay)); assertThat(result, hasSize(1)); - final uk.gov.moj.cpp.listing.domain.HearingDay domain = result.get(0); - assertThat(domain.getCourtCentreId(), is(Optional.of(COURT_CENTRE_ID))); - assertThat(domain.getCourtRoomId(), is(Optional.of(COURT_ROOM_ID))); - assertThat(domain.getCourtScheduleId(), is(Optional.of(COURT_SCHEDULE_ID))); - assertThat(domain.getDurationMinutes(), is(30)); - assertThat(domain.getSequence(), is(1)); - assertThat(domain.getIsCancelled(), is(Optional.of(false))); - assertThat(domain.getHearingDate(), is(HEARING_DATE)); - assertThat(domain.getStartTime(), is(START_TIME)); - assertThat(domain.getEndTime(), is(END_TIME)); + assertThat(result.get(0).getEndTime(), is(endTime)); } @Test - public void shouldFallBackToStartTimeWhenEndTimeIsNull() { - final HearingDay command = hearingDay() - .withCourtCentreId(COURT_CENTRE_ID) + void shouldComputeEndTimeFromStartTimeAndDurationWhenEndTimeIsNull() { + final UUID courtCentreId = randomUUID(); + final ZonedDateTime startTime = ZonedDateTime.of(2026, 4, 10, 10, 0, 0, 0, ZoneOffset.UTC); + + final HearingDay commandDay = HearingDay.hearingDay() + .withCourtCentreId(courtCentreId) + .withHearingDate(startTime.toLocalDate()) + .withStartTime(startTime) .withDurationMinutes(30) .withSequence(0) - .withHearingDate(HEARING_DATE) - .withStartTime(START_TIME) - .withEndTime(null) .build(); - final List result = converter.convert(List.of(command)); + final List result = converter.convert(List.of(commandDay)); - assertThat(result.get(0).getEndTime(), is(START_TIME)); + assertThat(result, hasSize(1)); + assertThat(result.get(0).getEndTime(), is(startTime.plusMinutes(30))); } @Test - public void shouldSetOptionalEmptyForNullableFieldsWhenNull() { - final HearingDay command = hearingDay() - .withCourtCentreId(COURT_CENTRE_ID) - .withCourtRoomId(null) - .withCourtScheduleId(null) - .withIsCancelled(null) - .withDurationMinutes(15) + void shouldFallBackToStartTimeWhenBothEndTimeAndDurationAreNull() { + final UUID courtCentreId = randomUUID(); + final ZonedDateTime startTime = ZonedDateTime.of(2026, 4, 10, 10, 0, 0, 0, ZoneOffset.UTC); + + final HearingDay commandDay = HearingDay.hearingDay() + .withCourtCentreId(courtCentreId) + .withHearingDate(startTime.toLocalDate()) + .withStartTime(startTime) .withSequence(0) - .withHearingDate(HEARING_DATE) - .withStartTime(START_TIME) - .withEndTime(END_TIME) .build(); - final List result = converter.convert(List.of(command)); + final List result = converter.convert(List.of(commandDay)); - final uk.gov.moj.cpp.listing.domain.HearingDay domain = result.get(0); - assertThat(domain.getCourtRoomId(), is(Optional.empty())); - assertThat(domain.getCourtScheduleId(), is(Optional.empty())); - assertThat(domain.getIsCancelled(), is(Optional.empty())); + assertThat(result, hasSize(1)); + assertThat(result.get(0).getEndTime(), is(startTime)); } @Test - public void shouldConvertMultipleHearingDaysPreservingOrder() { - final HearingDay first = hearingDay() - .withCourtCentreId(COURT_CENTRE_ID) + void shouldMapIsDraftFromCommandToDomain() { + final UUID courtCentreId = randomUUID(); + final UUID courtScheduleId = randomUUID(); + final ZonedDateTime startTime = ZonedDateTime.of(2026, 4, 10, 10, 0, 0, 0, ZoneOffset.UTC); + + final HearingDay commandDay = HearingDay.hearingDay() + .withCourtCentreId(courtCentreId) + .withCourtScheduleId(courtScheduleId) + .withHearingDate(startTime.toLocalDate()) + .withStartTime(startTime) + .withEndTime(startTime.plusMinutes(30)) .withDurationMinutes(30) .withSequence(0) - .withIsCancelled(null) - .withHearingDate(LocalDate.of(2020, 8, 18)) - .withStartTime(ZonedDateTime.parse("2020-08-18T01:22:12.381Z")) - .withEndTime(ZonedDateTime.parse("2020-08-18T02:22:12.381Z")) + .withIsDraft(true) .build(); - final HearingDay second = hearingDay() - .withCourtCentreId(COURT_CENTRE_ID) - .withDurationMinutes(10) + final List result = converter.convert(List.of(commandDay)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getIsDraft().isPresent(), is(true)); + assertThat(result.get(0).getIsDraft().get(), is(true)); + assertThat(result.get(0).getCourtScheduleId().get(), is(courtScheduleId)); + } + + @Test + void shouldMapAllFieldsCorrectly() { + final UUID courtCentreId = randomUUID(); + final UUID courtRoomId = randomUUID(); + final UUID courtScheduleId = randomUUID(); + final ZonedDateTime startTime = ZonedDateTime.of(2026, 4, 10, 10, 0, 0, 0, ZoneOffset.UTC); + final ZonedDateTime endTime = startTime.plusMinutes(60); + final LocalDate hearingDate = startTime.toLocalDate(); + + final HearingDay commandDay = HearingDay.hearingDay() + .withCourtCentreId(courtCentreId) + .withCourtRoomId(courtRoomId) + .withCourtScheduleId(courtScheduleId) + .withHearingDate(hearingDate) + .withStartTime(startTime) + .withEndTime(endTime) + .withDurationMinutes(60) .withSequence(1) .withIsCancelled(false) - .withHearingDate(LocalDate.of(2020, 8, 19)) - .withStartTime(ZonedDateTime.parse("2020-08-19T01:22:12.381Z")) - .withEndTime(ZonedDateTime.parse("2020-08-19T02:22:12.381Z")) - .build(); - - final HearingDay third = hearingDay() - .withCourtCentreId(COURT_CENTRE_ID) - .withDurationMinutes(20) - .withSequence(2) - .withIsCancelled(true) - .withHearingDate(LocalDate.of(2020, 8, 20)) - .withStartTime(ZonedDateTime.parse("2020-08-20T02:22:12.381Z")) - .withEndTime(ZonedDateTime.parse("2020-08-20T03:22:12.381Z")) + .withIsDraft(false) .build(); - final List result = converter.convert(List.of(first, second, third)); + final List result = converter.convert(List.of(commandDay)); - assertThat(result, hasSize(3)); - - assertThat(result.get(0).getHearingDate(), is(LocalDate.of(2020, 8, 18))); - assertThat(result.get(0).getSequence(), is(0)); - assertThat(result.get(0).getDurationMinutes(), is(30)); - assertThat(result.get(0).getIsCancelled(), is(Optional.empty())); - - assertThat(result.get(1).getHearingDate(), is(LocalDate.of(2020, 8, 19))); - assertThat(result.get(1).getSequence(), is(1)); - assertThat(result.get(1).getDurationMinutes(), is(10)); - assertThat(result.get(1).getIsCancelled(), is(Optional.of(false))); - - assertThat(result.get(2).getHearingDate(), is(LocalDate.of(2020, 8, 20))); - assertThat(result.get(2).getSequence(), is(2)); - assertThat(result.get(2).getDurationMinutes(), is(20)); - assertThat(result.get(2).getIsCancelled(), is(Optional.of(true))); + assertThat(result, hasSize(1)); + final uk.gov.moj.cpp.listing.domain.HearingDay domain = result.get(0); + assertThat(domain.getCourtCentreId().get(), is(courtCentreId)); + assertThat(domain.getCourtRoomId().get(), is(courtRoomId)); + assertThat(domain.getCourtScheduleId().get(), is(courtScheduleId)); + assertThat(domain.getHearingDate(), is(hearingDate)); + assertThat(domain.getStartTime(), is(startTime)); + assertThat(domain.getEndTime(), is(endTime)); + assertThat(domain.getDurationMinutes(), is(60)); + assertThat(domain.getSequence(), is(1)); + assertThat(domain.getIsCancelled().get(), is(false)); + assertThat(domain.getIsDraft().get(), is(false)); } } diff --git a/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCoreToDomainConverterTest.java b/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCoreToDomainConverterTest.java index e193deb4d..d1d119c88 100644 --- a/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCoreToDomainConverterTest.java +++ b/listing-command/listing-command-handler/src/test/java/uk/gov/moj/cpp/listing/command/utils/HearingDaysCoreToDomainConverterTest.java @@ -47,18 +47,21 @@ public void shouldConvertHearingDayFromCoreDomainToListingDomain() { assertThat(hearingDaysInListingDomain.get(0).getSequence(), is(0)); assertThat(hearingDaysInListingDomain.get(0).getDurationMinutes(), is(30)); assertThat(ZonedDateTimes.toString(hearingDaysInListingDomain.get(0).getStartTime()), is("2020-08-18T01:22:12.381Z")); + assertThat(ZonedDateTimes.toString(hearingDaysInListingDomain.get(0).getEndTime()), is("2020-08-18T01:52:12.381Z")); assertThat(hearingDaysInListingDomain.get(0).getIsCancelled(), nullValue()); assertThat(LocalDates.to(hearingDaysInListingDomain.get(1).getHearingDate()), is("2020-08-19")); assertThat(hearingDaysInListingDomain.get(1).getSequence(), is(1)); assertThat(hearingDaysInListingDomain.get(1).getDurationMinutes(), is(10)); assertThat(ZonedDateTimes.toString(hearingDaysInListingDomain.get(1).getStartTime()), is("2020-08-19T01:22:12.381Z")); + assertThat(ZonedDateTimes.toString(hearingDaysInListingDomain.get(1).getEndTime()), is("2020-08-19T01:32:12.381Z")); assertThat(hearingDaysInListingDomain.get(1).getIsCancelled(), is(false)); assertThat(LocalDates.to(hearingDaysInListingDomain.get(2).getHearingDate()), is("2020-08-20")); assertThat(hearingDaysInListingDomain.get(2).getSequence(), is(1)); assertThat(hearingDaysInListingDomain.get(2).getDurationMinutes(), is(20)); assertThat(ZonedDateTimes.toString(hearingDaysInListingDomain.get(2).getStartTime()), is("2020-08-20T02:22:12.381Z")); + assertThat(ZonedDateTimes.toString(hearingDaysInListingDomain.get(2).getEndTime()), is("2020-08-20T02:42:12.381Z")); assertThat(hearingDaysInListingDomain.get(2).getIsCancelled(), is(true)); } } \ No newline at end of file diff --git a/listing-command/listing-command-handler/src/test/resources/stub-data/azure.rotasl.getHearingSlots.stub-data.json b/listing-command/listing-command-handler/src/test/resources/stub-data/azure.rotasl.getHearingSlots.stub-data.json index 286712f7b..2405666ac 100644 --- a/listing-command/listing-command-handler/src/test/resources/stub-data/azure.rotasl.getHearingSlots.stub-data.json +++ b/listing-command/listing-command-handler/src/test/resources/stub-data/azure.rotasl.getHearingSlots.stub-data.json @@ -23,28 +23,28 @@ "availableDuration": 0, "judiciaries": [ { - "judiciaryId": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", + "id": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false }, { - "judiciaryId": "d424f73e-4a19-396d-a00c-f4c38d1c864e", + "id": "d424f73e-4a19-396d-a00c-f4c38d1c864e", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": false, - "benchChairman": true + "isDeputy": false, + "isBenchChairman": true }, { - "judiciaryId": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", + "id": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false } ], "slotStartTimes": [] diff --git a/listing-command/pom.xml b/listing-command/pom.xml index d278138a7..2fd617c7a 100644 --- a/listing-command/pom.xml +++ b/listing-command/pom.xml @@ -3,7 +3,7 @@ listing-parent uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-common/pom.xml b/listing-common/pom.xml index df017b4bb..8ee2671ec 100644 --- a/listing-common/pom.xml +++ b/listing-common/pom.xml @@ -3,7 +3,7 @@ listing-parent uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerJsonGetQuerySupport.java b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerJsonGetQuerySupport.java new file mode 100644 index 000000000..ee7cb9b42 --- /dev/null +++ b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerJsonGetQuerySupport.java @@ -0,0 +1,103 @@ +package uk.gov.moj.cpp.listing.common.service; + +import static javax.ws.rs.core.HttpHeaders.ACCEPT; + +import uk.gov.justice.services.common.converter.StringToJsonObjectConverter; +import uk.gov.justice.services.core.dispatcher.SystemUserProvider; +import uk.gov.moj.cpp.listing.domain.exception.DataValidationException; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import javax.ws.rs.core.Response; + +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@SuppressWarnings({"squid:S1118", "squid:S1312", "squid:S2629"}) +final class CourtSchedulerJsonGetQuerySupport { + + private static final Logger LOGGER = LoggerFactory.getLogger(CourtSchedulerJsonGetQuerySupport.class); + + private static final String CJS_CPP_UID = "CJSCPPUID"; + + private CourtSchedulerJsonGetQuerySupport() { + } + + static CloseableHttpResponse execute(final HttpRequestBase httpRequest) throws IOException { + return HttpClientBuilder + .create() + .build() + .execute(httpRequest); + } + + static boolean isOk(final HttpResponse httpResponse) { + return httpResponse.getStatusLine().getStatusCode() == Response.Status.OK.getStatusCode(); + } + + static Response executeQuery( + final String baseUri, + final SystemUserProvider systemUserProvider, + final StringToJsonObjectConverter stringToJsonObjectConverter, + final String urlPath, + final String acceptHeader, + final Map params, + final String logContextLabel) { + if (LOGGER.isInfoEnabled() && Objects.nonNull(params)) { + params.forEach((key, value) -> + LOGGER.info("{} in {} with params '{}-{}'", acceptHeader, logContextLabel, key, value)); + } + + if (params == null) { + throw new DataValidationException("Params for search %s is null ....".formatted(acceptHeader)); + } + + try { + final URIBuilder uriBuilder = new URIBuilder(baseUri + urlPath); + params.forEach(uriBuilder::addParameter); + final HttpGet httpGet = new HttpGet(uriBuilder.build()); + httpGet.addHeader(ACCEPT, acceptHeader); + httpGet.addHeader(CJS_CPP_UID, getUserId(systemUserProvider).toString()); + + final HttpResponse httpResponse = execute(httpGet); + + if (isOk(httpResponse)) { + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Retrieve {} successfully", acceptHeader); + } + return Response + .status(Response.Status.fromStatusCode(httpResponse.getStatusLine().getStatusCode())) + .entity(stringToJsonObjectConverter.convert(EntityUtils.toString(httpResponse.getEntity()))) + .build(); + } + LOGGER.error("Retrieve {} failed with status code:{}", acceptHeader, + httpResponse.getStatusLine().getStatusCode()); + return Response + .status(Response.Status.fromStatusCode(httpResponse.getStatusLine().getStatusCode())) + .entity(EntityUtils.toString(httpResponse.getEntity())) + .build(); + } catch (URISyntaxException | IOException ex) { + LOGGER.error("Exception thrown on trying to Retrieving %s".formatted(acceptHeader), ex); + return Response + .status(HttpStatus.SC_INTERNAL_SERVER_ERROR) + .entity(ex.getMessage()) + .build(); + } + } + + private static UUID getUserId(final SystemUserProvider systemUserProvider) { + return systemUserProvider.getContextSystemUserId() + .orElseThrow(() -> new IllegalStateException("contextSystemUserId missing!!!")); + } +} diff --git a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerSearchService.java b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerSearchService.java new file mode 100644 index 000000000..fa771df2c --- /dev/null +++ b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerSearchService.java @@ -0,0 +1,41 @@ +package uk.gov.moj.cpp.listing.common.service; + +import uk.gov.justice.services.common.configuration.Value; +import uk.gov.justice.services.common.converter.StringToJsonObjectConverter; +import uk.gov.justice.services.core.dispatcher.SystemUserProvider; + +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.core.Response; + +@SuppressWarnings("squid:S6813") +@ApplicationScoped +public class CourtSchedulerSearchService { + + private static final String SEARCH_AVAILABLE_JUDICIARIES_RESOURCE = "/judiciaries/search-available"; + private static final String COURTSCHEDULER_SEARCH_AVAILABLE_JUDICIARIES = + "application/vnd.courtscheduler.search.available.judiciaries+json"; + + @Inject + @Value(key = "courtscheduler.base.url", defaultValue = "http://localhost:8080/listingcourtscheduler-api/rest/courtscheduler") + protected String baseUri; + + @Inject + SystemUserProvider systemUserProvider; + + @Inject + StringToJsonObjectConverter stringToJsonObjectConverter; + + public Response searchAvailableJudiciaries(final Map params) { + return CourtSchedulerJsonGetQuerySupport.executeQuery( + baseUri, + systemUserProvider, + stringToJsonObjectConverter, + SEARCH_AVAILABLE_JUDICIARIES_RESOURCE, + COURTSCHEDULER_SEARCH_AVAILABLE_JUDICIARIES, + params, + "CourtScheduler"); + } +} diff --git a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java index b493a0dab..ad9d61998 100644 --- a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java +++ b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapter.java @@ -97,9 +97,9 @@ private List getJudiciariesFromRota(final Response response) { .withJudicialRoleType(JudicialRoleType.judicialRoleType() .withJudiciaryType("MAGISTRATE") .build()) - .withJudicialId(UUID.fromString(rotaSlJudiciaryJsonObject.getString("judiciaryId"))) - .withIsDeputy(Optional.of(rotaSlJudiciaryJsonObject.getBoolean("deputy"))) - .withIsBenchChairman(Optional.of(rotaSlJudiciaryJsonObject.getBoolean("benchChairman"))) + .withJudicialId(UUID.fromString(rotaSlJudiciaryJsonObject.getString("id"))) + .withIsDeputy(Optional.of(rotaSlJudiciaryJsonObject.getBoolean("isDeputy"))) + .withIsBenchChairman(Optional.of(rotaSlJudiciaryJsonObject.getBoolean("isBenchChairman"))) .build()) ); }); @@ -167,6 +167,21 @@ public Response hearingSlotsSearch(final Map queryParams) { return hearingSlotResponse; } + public Response validateSessionAvailability(final JsonObject requestPayload) { + final Response response = hearingSlotsService.validateSessionAvailability(requestPayload); + + if (HttpStatus.SC_OK == response.getStatus()) { + return response; + } + + String responsePayload = ""; + if (response.hasEntity()) { + responsePayload = response.getEntity().toString(); + } + LOGGER.error("validateSessionAvailability from courtscheduler returned an error : {} with status {}", responsePayload, response.getStatus()); + return response; + } + public HearingIdsResponse getCourtSchedulerHearings(final String ouCode, final Optional courtSessionOptional, final String courtRoomId, final String startDate, diff --git a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/HearingSlotsService.java b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/HearingSlotsService.java index 137cf7c49..7e34bacfd 100644 --- a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/HearingSlotsService.java +++ b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/service/HearingSlotsService.java @@ -11,26 +11,24 @@ import java.io.IOException; import java.net.URISyntaxException; -import java.net.URL; import java.util.Map; import java.util.Objects; import java.util.UUID; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonObject; import javax.ws.rs.core.Response; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; -import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,15 +41,20 @@ public class HearingSlotsService { public static final String HEARING_DATE = "hearingDate"; private static final String HEARING_RESOURCE = "/hearingslots"; + private static final String VALIDATE_SESSION_AVAILABILITY_RESOURCE = "/validate-session-availability"; private static final String COURTSCHEDULER_LIST_HEARING_IN_COURT_SESSIONS_RESOURCE = "/list/hearingslots"; private static final String HEARING_SEARCH_BOOK_RESOURCE = "/searchlist/hearingslots"; private static final String COURTSCHEDULES_RESOURCE = "/courtschedule/search.court-schedules-by-id"; private static final String COURTSCHEDULER_LIST_HEARING_IN_COURT_SESSIONS = "application/vnd.courtscheduler.list.hearings-in-court-sessions+json"; private static final String COURTSCHEDULER_GET_HEARING_SLOTS_TYPE = "application/vnd.courtscheduler.get.hearing.slots+json"; - private static final String COURTSCHEDULER_SEARCH_COURTSCHEDULES_BY_ID = "application/vnd.courtscheduler.search.courtschedules.by.id+json"; + private static final String COURTSCHEDULER_SEARCH_COURTSCHEDULES_BY_ID = "application/vnd.courtscheduler.search.court-schedules-by-id+json"; private static final String COURTSCHEDULER_DELETE_HEARING_SLOTS_TYPE = "application/vnd.courtscheduler.remove.hearing.slots+json"; private static final String COUTRT_SCHEDULER_HEARING_IDS = "application/vnd.courtscheduler.get.hearing.ids+json"; private static final String COURTSCHEDULER_SEARCH_BOOK_COURTSCHEDULES = "application/vnd.courtscheduler.search.book.hearing.slots+json"; + private static final String COURTSCHEDULER_VALIDATE_SESSION_AVAILABILITY_TYPE = "application/vnd.courtscheduler.validate.session.availability+json"; + + private static final String MULTIDAY_SEARCH_BOOK_RESOURCE = "/multidaysearchandbook/hearingslots"; + private static final String COURTSCHEDULER_MULTIDAY_SEARCH_BOOK = "application/vnd.courtscheduler.multiday.searchandbook.hearing.slots+json"; private static final String CJS_CPP_UID = "CJSCPPUID"; @Inject @@ -68,26 +71,30 @@ public Response search(final Map params) { return query(HEARING_RESOURCE, COURTSCHEDULER_GET_HEARING_SLOTS_TYPE, params); } + public Response validateSessionAvailability(final JsonObject payload) { + return post(VALIDATE_SESSION_AVAILABILITY_RESOURCE, COURTSCHEDULER_VALIDATE_SESSION_AVAILABILITY_TYPE, payload); + } + public Response searchBookSlots(final Map params) { return query(HEARING_SEARCH_BOOK_RESOURCE, COURTSCHEDULER_SEARCH_BOOK_COURTSCHEDULES, params); } - public Response listHearingInCourtSessions(final Object payload) { + public Response listHearingInCourtSessions(final JsonObject payload) { if (LOGGER.isInfoEnabled()) { LOGGER.info("HearingSlots slots list update in CourtScheduler S & L with slot details '{}'", payload); } try { - final HttpPut httpPut = new HttpPut(new URL(baseUri + COURTSCHEDULER_LIST_HEARING_IN_COURT_SESSIONS_RESOURCE).toString()); + final HttpPut httpPut = new HttpPut(new URIBuilder(baseUri + COURTSCHEDULER_LIST_HEARING_IN_COURT_SESSIONS_RESOURCE).build()); httpPut.addHeader(CONTENT_TYPE, COURTSCHEDULER_LIST_HEARING_IN_COURT_SESSIONS); httpPut.addHeader(CJS_CPP_UID, getUserId().toString()); - final StringEntity requestEntity = new StringEntity(this.objectMapper.writeValueAsString(payload)); + final StringEntity requestEntity = new StringEntity(payload.toString()); httpPut.setEntity(requestEntity); - final HttpResponse httpResponse = execute(httpPut); + final HttpResponse httpResponse = CourtSchedulerJsonGetQuerySupport.execute(httpPut); - if (isOk(httpResponse)) { + if (CourtSchedulerJsonGetQuerySupport.isOk(httpResponse)) { if (LOGGER.isInfoEnabled()) { LOGGER.info("HearingSlots list updated successfully"); } @@ -104,7 +111,7 @@ public Response listHearingInCourtSessions(final Object payload) { .entity(entityBodyAsString) .build(); } - } catch (IOException ex) { + } catch (URISyntaxException | IOException ex) { LOGGER.error("Exception thrown on trying to Update Hearing Slots", ex); return Response .status(HttpStatus.SC_INTERNAL_SERVER_ERROR) @@ -117,17 +124,21 @@ public Response getCourtSchedulesById(final Map params) { return query(COURTSCHEDULES_RESOURCE, COURTSCHEDULER_SEARCH_COURTSCHEDULES_BY_ID, params); } + public Response multiDaySearchAndBook(final Map params) { + return query(MULTIDAY_SEARCH_BOOK_RESOURCE, COURTSCHEDULER_MULTIDAY_SEARCH_BOOK, params); + } + public void delete(final UUID hearingId) { if (LOGGER.isInfoEnabled()) { LOGGER.info("Delete HearingSlots in CourtScheduler S & L with hearing id '{}'", hearingId); } try { - final HttpDelete httpDelete = new HttpDelete(new URL(baseUri + HEARING_RESOURCE + "/" + hearingId.toString()).toString()); + final HttpDelete httpDelete = new HttpDelete(new URIBuilder(baseUri + HEARING_RESOURCE + "/" + hearingId).build()); httpDelete.addHeader(CONTENT_TYPE, COURTSCHEDULER_DELETE_HEARING_SLOTS_TYPE); httpDelete.addHeader(CJS_CPP_UID, getUserId().toString()); - final HttpResponse httpResponse = execute(httpDelete); + final HttpResponse httpResponse = CourtSchedulerJsonGetQuerySupport.execute(httpDelete); if (isAccepted(httpResponse)) { if (LOGGER.isInfoEnabled()) { @@ -138,7 +149,7 @@ public void delete(final UUID hearingId) { httpResponse.getStatusLine().getStatusCode()); } - } catch (IOException ex) { + } catch (URISyntaxException | IOException ex) { LOGGER.error("Exception thrown on trying to Delete Hearing Slots", ex); } } @@ -151,59 +162,53 @@ private UUID getUserId() { return systemUserProvider.getContextSystemUserId().orElseThrow(() -> new IllegalStateException("contextSystemUserId missing!!!")); } - static boolean isOk(HttpResponse httpResponse) { - return httpResponse.getStatusLine().getStatusCode() == Response.Status.OK.getStatusCode(); - } - private boolean isAccepted(HttpResponse httpResponse) { return httpResponse.getStatusLine().getStatusCode() == Response.Status.ACCEPTED.getStatusCode(); } - private static CloseableHttpResponse execute(final HttpRequestBase httpRequest) throws IOException { - return HttpClientBuilder - .create() - .build() - .execute(httpRequest); + private Response query(final String urlPath, final String acceptHeader, final Map params) { + return CourtSchedulerJsonGetQuerySupport.executeQuery( + baseUri, systemUserProvider, stringToJsonObjectConverter, urlPath, acceptHeader, params, "CourtScheduler S & L"); } - private Response query(final String urlPath, final String acceptHeader, final Map params) { - if (LOGGER.isInfoEnabled() && Objects.nonNull(params)) { - params.forEach((key, value) -> LOGGER.info("{} in CourtScheduler S & L with params '{}-{}'", acceptHeader, key, value)); + private Response post(final String urlPath, final String contentTypeHeader, final JsonObject payload) { + if (LOGGER.isInfoEnabled() && Objects.nonNull(payload)) { + LOGGER.info("{} in CourtScheduler S & L with payload '{}'", contentTypeHeader, payload); } - - if (params == null) { - throw new DataValidationException("Params for search %s is null ....".formatted(acceptHeader)); + if (payload == null || payload.isEmpty()) { + throw new DataValidationException("Payload for %s is null or empty ....".formatted(contentTypeHeader)); } - try { - final HttpGet httpGet = new HttpGet(new URL(baseUri + urlPath).toString()); - httpGet.addHeader(ACCEPT, acceptHeader); - httpGet.addHeader(CJS_CPP_UID, getUserId().toString()); - - final URIBuilder uriBuilder = new URIBuilder(httpGet.getURI()); - params.forEach(uriBuilder::addParameter); - httpGet.setURI(uriBuilder.build()); - - final HttpResponse httpResponse = execute(httpGet); - - if (isOk(httpResponse)) { + final HttpPost httpPost = new HttpPost(new URIBuilder(baseUri + urlPath).build()); + httpPost.addHeader(CONTENT_TYPE, contentTypeHeader); + httpPost.addHeader(ACCEPT, "application/json"); + httpPost.addHeader(CJS_CPP_UID, getUserId().toString()); + httpPost.setEntity(new StringEntity(payload.toString())); + + final HttpResponse httpResponse = CourtSchedulerJsonGetQuerySupport.execute(httpPost); + final String responseBody = httpResponse.getEntity() == null ? "" : EntityUtils.toString(httpResponse.getEntity()); + final Object entity = responseBody == null || responseBody.isBlank() + ? Json.createObjectBuilder().build() + : stringToJsonObjectConverter.convert(responseBody); + + if (CourtSchedulerJsonGetQuerySupport.isOk(httpResponse)) { if (LOGGER.isInfoEnabled()) { - LOGGER.info("Retrieve {} successfully", acceptHeader); + LOGGER.info("Retrieve {} successfully", contentTypeHeader); } return Response .status(Response.Status.fromStatusCode(httpResponse.getStatusLine().getStatusCode())) - .entity(stringToJsonObjectConverter.convert(EntityUtils.toString(httpResponse.getEntity()))) + .entity(entity) .build(); } else { - LOGGER.error("Retrieve {} failed with status code:{}", acceptHeader, + LOGGER.error("Retrieve {} failed with status code:{}", contentTypeHeader, httpResponse.getStatusLine().getStatusCode()); return Response .status(Response.Status.fromStatusCode(httpResponse.getStatusLine().getStatusCode())) - .entity(EntityUtils.toString(httpResponse.getEntity())) + .entity(entity) .build(); } } catch (URISyntaxException | IOException ex) { - LOGGER.error("Exception thrown on trying to Retrieving %s".formatted(acceptHeader), ex); + LOGGER.error("Exception thrown on trying to Retrieving %s".formatted(contentTypeHeader), ex); return Response .status(HttpStatus.SC_INTERNAL_SERVER_ERROR) .entity(ex.getMessage()) diff --git a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/CommonXhibitReferenceDataService.java b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/CommonXhibitReferenceDataService.java index c57a58820..5d5e7148b 100644 --- a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/CommonXhibitReferenceDataService.java +++ b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/CommonXhibitReferenceDataService.java @@ -31,16 +31,14 @@ public class CommonXhibitReferenceDataService { private static final String UNMAPPED_COURT_ROOM_NAME = "Court -99"; private static final String CREST_COURT_SITE_CODE = "crestCourtSiteCode"; + private static final String DEFAULT_CREST_COURT_SITE_CODE = "A"; private static final String CREST_COURT_SITE_ID = "crestCourtSiteId"; - private static final String MAGISTRATES_COURT_TYPE = "MAGISTRATES_COURT"; private final ConcurrentMap> crestCourtSitesCache = new ConcurrentHashMap<>(); private final XhibitReferenceDataValidator xhibitReferenceDataValidator = new XhibitReferenceDataValidator(); private static final String COURT_DETAILS_NOT_FOUND = "Cannot find court details with courtCentre %s"; - private static final String COURT_MAPPING_NOT_FOUND_FOR_TYPE = - "Cannot find court mapping for courtCentre %s with court type %s"; private static final Logger LOGGER = LoggerFactory.getLogger(CommonXhibitReferenceDataService.class); @@ -92,48 +90,17 @@ public CourtLocation getCrownCourtDetails(final UUID courtCentreId) { return createCrownCourtLocation(court); } - /** - * Xhibit court location for a committing court (magistrates or crown). Resolution uses - * {@link ReferenceDataCache#getCpXhibitCourtMappingsMapCache(UUID)}: mags - * ({@code referencedata.query.cp-xhibit-mags-court-mapping}, including empty {@code {}}), else - * {@code referencedata.query.cp-xhibit-court-mappings}. - *

- * When {@code courtHouseType} is set (e.g. from offence committing court), the mapping with that - * {@link CourtMapping#getCourtType()} is used; otherwise the first mapping from the cache is used. - */ - public CourtLocation getCriminalCourtDetails(final UUID courtCentreId, final String courtHouseType) { - - final List mappings = referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId) - .orElseThrow(() -> new InvalidReferenceDataException(format(COURT_DETAILS_NOT_FOUND, courtCentreId))); - if (mappings.isEmpty()) { - throw new InvalidReferenceDataException(format(COURT_DETAILS_NOT_FOUND, courtCentreId)); - } - - final CourtMapping court; - if (courtHouseType != null && !courtHouseType.isBlank()) { - court = mappings.stream() - .filter(m -> courtHouseType.equals(m.getCourtType())) - .findFirst() - .orElseThrow(() -> new InvalidReferenceDataException( - format(COURT_MAPPING_NOT_FOUND_FOR_TYPE, courtCentreId, courtHouseType))); - } else { - court = mappings.stream() - .findFirst() - .orElseThrow(() -> new InvalidReferenceDataException(format(COURT_DETAILS_NOT_FOUND, courtCentreId))); - } + public CourtLocation getMagsCourtDetails(final UUID courtCentreId) { - return createCourtLocationFromCourtMapping(court); - } + final CourtMapping court = referenceDataCache.getMagsCourtMappingsMapCache(courtCentreId) + .orElseThrow(() -> new InvalidReferenceDataException(format(COURT_DETAILS_NOT_FOUND, courtCentreId))) + .stream() + .findFirst() + .orElseThrow(() -> new InvalidReferenceDataException(format(COURT_DETAILS_NOT_FOUND, courtCentreId))); - /** - * Same as {@link #getCriminalCourtDetails(UUID, String)} with no {@code courtHouseType} (first combined mapping). - */ - public CourtLocation getCriminalCourtDetails(final UUID courtCentreId) { - return getCriminalCourtDetails(courtCentreId, null); + return createMagsCourtLocation(court); } - - public List getCrestCourtSitesForCrownCourtCentre(UUID courtCentreId) { return crestCourtSitesCache.computeIfAbsent(courtCentreId.toString(), id -> fetchCrestCourtSitesFromDatabase(courtCentreId)); } @@ -247,11 +214,4 @@ private CourtLocation createMagsCourtLocation(final CourtMapping courtMapping) { courtMapping.getCrestCourtSiteCode(), courtMapping.getCourtType()); } - - private CourtLocation createCourtLocationFromCourtMapping(final CourtMapping courtMapping) { - if (MAGISTRATES_COURT_TYPE.equals(courtMapping.getCourtType())) { - return createMagsCourtLocation(courtMapping); - } - return createCrownCourtLocation(courtMapping); - } } diff --git a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataCache.java b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataCache.java index af16020b6..6bf506192 100644 --- a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataCache.java +++ b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataCache.java @@ -3,7 +3,6 @@ import uk.gov.moj.cpp.listing.common.xhibit.exception.InvalidReferenceDataException; import uk.gov.moj.cpp.listing.common.xhibit.model.CourtCentreRoomKey; import uk.gov.moj.cpp.listing.domain.referencedata.CourtMapping; -import uk.gov.moj.cpp.listing.domain.referencedata.CourtMappingsList; import uk.gov.moj.cpp.listing.domain.referencedata.CourtRoomMapping; import uk.gov.moj.cpp.listing.domain.referencedata.CourtRoomMappingsList; import uk.gov.moj.cpp.listing.domain.referencedata.HearingType; @@ -13,8 +12,6 @@ import uk.gov.moj.cpp.listing.domain.referencedata.OrganisationUnitList; import uk.gov.moj.cpp.listing.domain.xhibit.CourtLocation; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -22,7 +19,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.enterprise.context.ApplicationScoped; @@ -41,15 +37,6 @@ public class ReferenceDataCache { private final Map> magsCourtMappingListMapCache = new ConcurrentHashMap<>(); - private static final String MAGISTRATES_COURT_TYPE = "MAGISTRATES_COURT"; - - /** - * CP Xhibit court mappings for committing court (lazy per court centre): mags from - * {@code referencedata.query.cp-xhibit-mags-court-mapping}, else - * {@code referencedata.query.cp-xhibit-court-mappings} by court centre id. - */ - private final Map> lazyCpXhibitCourtMappingsByCourtCentreId = new ConcurrentHashMap<>(); - private final Map hearingTypesMapCache = new ConcurrentHashMap<>(); private final Map> cpCourtRoomCache = new ConcurrentHashMap<>(); @@ -80,23 +67,9 @@ public void initReferenceData() { public Optional> getCrownCourtMappingsMapCache(final UUID courtCentreId) { OrganisationUnit organisationUnit = organisationUnitMapByIdCache.get(courtCentreId); - if (organisationUnit == null) { - return Optional.empty(); - } - - final String oucode = organisationUnit.getOucode(); - final List cachedMappings = crownCourtMappingListMapCache.get(oucode); - if (cachedMappings != null && !cachedMappings.isEmpty()) { - return Optional.of(cachedMappings); - } - - return referenceDataLoader.getXhibitCrownCourtMappings(courtCentreId) - .map(CourtMappingsList::getCpXhibitCourtMappings) - .filter(mappings -> !mappings.isEmpty()) - .map(mappings -> { - cacheCrownCourtMappings(mappings); - return crownCourtMappingListMapCache.get(oucode); - }); + return organisationUnit == null + ? Optional.empty() + : Optional.ofNullable(crownCourtMappingListMapCache.get(organisationUnit.getOucode())); } public Optional> getMagsCourtMappingsMapCache(final UUID courtCentreId) { @@ -116,56 +89,6 @@ public Optional> getMagsCourtMappingsMapCache(final UUID cour ); } - /** - * Xhibit court mappings for committing court: magistrates via - * {@code referencedata.query.cp-xhibit-mags-court-mapping} (including HTTP 200 with empty {@code {}}), - * then if absent or empty — {@code referencedata.query.cp-xhibit-court-mappings} by court centre id. - */ - public Optional> getCpXhibitCourtMappingsMapCache(final UUID courtCentreId) { - if (organisationUnitMapByIdCache.get(courtCentreId) == null) { - return Optional.empty(); - } - return Optional.of( - lazyCpXhibitCourtMappingsByCourtCentreId.computeIfAbsent(courtCentreId, this::resolveCpXhibitCourtMappingsWithMagsFirst) - ); - } - - private List resolveCpXhibitCourtMappingsWithMagsFirst(final UUID courtCentreId) { - final OrganisationUnit organisationUnit = organisationUnitMapByIdCache.get(courtCentreId); - if (organisationUnit == null) { - return Collections.emptyList(); - } - final String oucode = organisationUnit.getOucode(); - - final List magsMappings = referenceDataLoader.getXhibitMagsCourtMappings(oucode) - .map(CourtMappingsList::getCpXhibitCourtMappings) - .filter(list -> list != null && !list.isEmpty()) - .orElse(Collections.emptyList()); - - if (!magsMappings.isEmpty()) { - List updatedMappings = magsMappings.stream() - .map(mapping -> new CourtMapping.Builder() - .withId(mapping.getId()) - .withOucode(mapping.getOucode()) - .withCrestCourtId(mapping.getCrestCourtId()) - .withCrestCourtSiteId(mapping.getCrestCourtSiteId()) - .withCrestCourtName(mapping.getCrestCourtName()) - .withCrestCourtShortName(mapping.getCrestCourtShortName()) - .withCrestCourtSiteName(mapping.getCrestCourtSiteName()) - .withCrestCourtSiteCode(mapping.getCrestCourtSiteCode()) - .withCourtType(MAGISTRATES_COURT_TYPE) - .build()) - .collect(Collectors.toList()); - return new ArrayList<>(updatedMappings); - } - - - return referenceDataLoader.getXhibitCrownCourtMappings(courtCentreId) - .map(CourtMappingsList::getCpXhibitCourtMappings) - .filter(list -> list != null && !list.isEmpty()) - .orElse(Collections.emptyList()); - } - public Optional getJudiciariesMapCache(final UUID judiciaryId) { return Optional.ofNullable( judiciariesMapCache.computeIfAbsent(judiciaryId, @@ -242,20 +165,16 @@ private synchronized void ensureCrownCourtMappingsInitialized() { } private void initCrownCourtMappingsList() { - referenceDataLoader.getXhibitCrownCourtMappings() - .map(CourtMappingsList::getCpXhibitCourtMappings) - .ifPresent(this::cacheCrownCourtMappings); - } - - private void cacheCrownCourtMappings(final List courtMappings) { - courtMappings.forEach(courtMapping -> { - crownCourtLocationListMapCache - .computeIfAbsent(courtMapping.getCrestCourtId(), k -> new java.util.concurrent.CopyOnWriteArrayList<>()) - .add(createCourtLocation(courtMapping)); - - crownCourtMappingListMapCache - .computeIfAbsent(courtMapping.getOucode(), k -> new java.util.concurrent.CopyOnWriteArrayList<>()) - .add(courtMapping); + referenceDataLoader.getXhibitCrownCourtMappings().ifPresent(courtMappingsList -> { + courtMappingsList.getCpXhibitCourtMappings().forEach(courtMapping -> { + crownCourtLocationListMapCache + .computeIfAbsent(courtMapping.getCrestCourtId(), k -> new java.util.concurrent.CopyOnWriteArrayList<>()) + .add(createCourtLocation(courtMapping)); + + crownCourtMappingListMapCache + .computeIfAbsent(courtMapping.getOucode(), k -> new java.util.concurrent.CopyOnWriteArrayList<>()) + .add(courtMapping); + }); }); } diff --git a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataLoader.java b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataLoader.java index ad28422a8..07c1021bf 100644 --- a/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataLoader.java +++ b/listing-common/src/main/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataLoader.java @@ -145,22 +145,7 @@ public Optional getXhibitMagsCourtMappings(final String oucod final Envelope response = requester.requestAsAdmin(requestEnvelope, CourtMapping.class); - if (Objects.isNull(response) || Objects.isNull(response.payload())) { - return empty(); - } - final CourtMapping payload = response.payload(); - if (isEmptyMagsCourtMappingPayload(payload)) { - return empty(); - } - return of(new CourtMappingsList(asList(payload))); - } - - /** - * Referencedata returns HTTP 200 with {@code {}} when no mags mapping exists; that deserialises to a - * {@link CourtMapping} with no id/oucode. - */ - private static boolean isEmptyMagsCourtMappingPayload(final CourtMapping payload) { - return payload.getId() == null && payload.getOucode() == null; + return Objects.isNull(response) ? empty() : of((new CourtMappingsList(asList(response.payload())))); } public Optional getCourtRoomMappingsList(UUID courtCentreId) { diff --git a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerSearchServiceTest.java b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerSearchServiceTest.java new file mode 100644 index 000000000..b3e37ba77 --- /dev/null +++ b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerSearchServiceTest.java @@ -0,0 +1,89 @@ +package uk.gov.moj.cpp.listing.common.service; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import uk.gov.justice.services.common.converter.StringToJsonObjectConverter; +import uk.gov.justice.services.core.dispatcher.SystemUserProvider; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import javax.ws.rs.core.Response; + +import org.apache.http.HttpEntity; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CourtSchedulerSearchServiceTest { + + private static final String BASE_URI = "http://test-uri"; + private static final UUID TEST_USER_ID = UUID.randomUUID(); + + @Mock + private SystemUserProvider systemUserProvider; + @Mock + private StringToJsonObjectConverter stringToJsonObjectConverter; + @Mock + private HttpClientBuilder httpClientBuilder; + @Mock + private CloseableHttpClient httpClient; + @Mock + private CloseableHttpResponse httpResponse; + @Mock + private StatusLine statusLine; + @Captor + private ArgumentCaptor httpGetCaptor; + + @InjectMocks + private CourtSchedulerSearchService courtSchedulerSearchService; + + @BeforeEach + void setUp() { + courtSchedulerSearchService.baseUri = BASE_URI; + } + + @Test + void searchAvailableJudiciaries_shouldCallCourtSchedulerEndpoint() throws Exception { + final Map params = new HashMap<>(); + params.put("search", "ai"); + when(systemUserProvider.getContextSystemUserId()).thenReturn(Optional.of(TEST_USER_ID)); + + try (MockedStatic mockedStatic = Mockito.mockStatic(HttpClientBuilder.class)) { + mockedStatic.when(HttpClientBuilder::create).thenReturn(httpClientBuilder); + when(httpClientBuilder.build()).thenReturn(httpClient); + when(httpClient.execute(any(HttpGet.class))).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(Response.Status.OK.getStatusCode()); + when(httpResponse.getEntity()).thenReturn(mock(HttpEntity.class)); + when(stringToJsonObjectConverter.convert(any())).thenReturn(mock(javax.json.JsonObject.class)); + + final Response response = courtSchedulerSearchService.searchAvailableJudiciaries(params); + + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + verify(httpClient).execute(httpGetCaptor.capture()); + assertThat(httpGetCaptor.getValue().getURI().toString(), + is(BASE_URI + "/judiciaries/search-available?search=ai")); + } + } +} diff --git a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java index 1d884edb2..525096a0c 100644 --- a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java +++ b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/CourtSchedulerServiceAdapterTest.java @@ -6,6 +6,7 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.Mockito.when; import static uk.gov.moj.cpp.listing.common.utils.FileUtil.givenPayload; @@ -36,7 +37,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -public class CourtSchedulerServiceAdapterTest { +class CourtSchedulerServiceAdapterTest { @InjectMocks private CourtSchedulerServiceAdapter courtSchedulerServiceAdapter; @@ -54,7 +55,7 @@ public class CourtSchedulerServiceAdapterTest { private JsonObjectToObjectConverter jsonObjectConverter; @Test - public void shouldGetJudicialRoles() { + void shouldGetJudicialRoles() { final String startDate = LocalDate.now().toString(); final String ouCode = "B01LY00"; final Optional courtSessionOptional = Optional.of("AM"); @@ -75,16 +76,16 @@ public void shouldGetJudicialRoles() { final JsonObject judiciaryJsonObject = (JsonObject) ((JsonObject) hearingSlotsResponse.getJsonArray("hearingSlots").get(0)).getJsonArray("judiciaries").get(index); - assertThat(judicialRole.getJudicialId().toString(), is(judiciaryJsonObject.getString("judiciaryId"))); - assertThat(judicialRole.getIsBenchChairman(), is(Optional.of(judiciaryJsonObject.getBoolean("benchChairman")))); - assertThat(judicialRole.getIsDeputy(), is(Optional.of(judiciaryJsonObject.getBoolean("deputy")))); + assertThat(judicialRole.getJudicialId().toString(), is(judiciaryJsonObject.getString("id"))); + assertThat(judicialRole.getIsBenchChairman(), is(Optional.of(judiciaryJsonObject.getBoolean("isBenchChairman")))); + assertThat(judicialRole.getIsDeputy(), is(Optional.of(judiciaryJsonObject.getBoolean("isDeputy")))); assertThat(judicialRole.getJudicialRoleType().getJudiciaryType(), is(judiciaryJsonObject.getString("judiciaryType"))); }); } @Test - public void shouldGetEmptyListIfThereIsNoMatchingJudicialRolesInRotaSL() { + void shouldGetEmptyListIfThereIsNoMatchingJudicialRolesInRotaSL() { final String startDate = LocalDate.now().toString(); final String ouCode = "B01LY00"; final Optional courtSessionOptional = Optional.of("AM"); @@ -101,7 +102,7 @@ public void shouldGetEmptyListIfThereIsNoMatchingJudicialRolesInRotaSL() { } @Test - public void shouldGetHearingSlotResponse() { + void shouldGetHearingSlotResponse() { final String startDate = LocalDate.now().toString(); final String ouCode = "B01LY00"; final String courtRoomId = "a91a93e6-d704-3cf1-9f20-e267b5a7eeeb"; @@ -112,9 +113,9 @@ public void shouldGetHearingSlotResponse() { when(response.getEntity()).thenReturn(hearingSlotsResponse); when(hearingSlotsService.search(anyMap())).thenReturn(response); - final Response response = courtSchedulerServiceAdapter.getHearingSlotResponse(startDate, startDate, ouCode, courtRoomId); + final Response slotResponse = courtSchedulerServiceAdapter.getHearingSlotResponse(startDate, startDate, ouCode, courtRoomId); - final JsonObject responseJson = objectToJsonObjectConverter.convert(response.getEntity()); + final JsonObject responseJson = objectToJsonObjectConverter.convert(slotResponse.getEntity()); final JsonObject object = responseJson.getJsonArray("hearingSlots").getValuesAs(JsonObject.class).get(0); assertThat(object.getString("panel"), is("YOUTH")); @@ -122,7 +123,7 @@ public void shouldGetHearingSlotResponse() { } @Test - public void shouldGetPanelInfoIfNotPresentInPayload() { + void shouldGetPanelInfoIfNotPresentInPayload() { final Optional panelInfoFromPayload = empty(); final LocalDate startDate = LocalDate.of(2021, 6, 21); @@ -144,7 +145,7 @@ public void shouldGetPanelInfoIfNotPresentInPayload() { } @Test - public void shouldGetPanelInfoIfPresentInPayload() { + void shouldGetPanelInfoIfPresentInPayload() { final Optional panelInfoFromPayload = of("ADULT"); final LocalDate startDate = LocalDate.of(2021, 6, 21); @@ -160,7 +161,7 @@ public void shouldGetPanelInfoIfPresentInPayload() { } @Test - public void shouldReturnEmptyPanelInfoIfNotPresentInPayloadAndGettingErrorFromRotaApi() { + void shouldReturnEmptyPanelInfoIfNotPresentInPayloadAndGettingErrorFromRotaApi() { final Optional panelInfoFromPayload = empty(); final LocalDate startDate = LocalDate.of(2021, 6, 21); @@ -178,7 +179,7 @@ public void shouldReturnEmptyPanelInfoIfNotPresentInPayloadAndGettingErrorFromRo } @Test - public void shouldReturnEmptyPanelInfoIfNotPresentInPayloadAndGettingEmptyPayloadFromRotaApi() { + void shouldReturnEmptyPanelInfoIfNotPresentInPayloadAndGettingEmptyPayloadFromRotaApi() { final Optional panelInfoFromPayload = empty(); final LocalDate startDate = LocalDate.of(2021, 6, 21); @@ -199,7 +200,7 @@ public void shouldReturnEmptyPanelInfoIfNotPresentInPayloadAndGettingEmptyPayloa } @Test - public void shouldGetHearingIds() { + void shouldGetHearingIds() { final String courtCentreId = UUID.randomUUID().toString(); final Optional courtSessionOptional = Optional.of("AD"); final String courtRoomId = UUID.randomUUID().toString(); @@ -221,4 +222,43 @@ public void shouldGetHearingIds() { assertThat(finalResp.getPageCount(), is(1L)); assertThat(finalResp.getResults(), is(4L)); } + + @Test + void shouldValidateSessionAvailability() { + final JsonObject validateResponse = givenPayload("/mock-data/azure.rotasl.getHearingSlots.stub-data.json"); + final JsonObject params = javax.json.Json.createObjectBuilder() + .add("courtScheduleIdList", javax.json.Json.createArrayBuilder() + .add(javax.json.Json.createObjectBuilder() + .add("courtScheduleId", "f8254db1-1683-483e-afb3-b87fde5a0a26"))) + .add("duration", 30) + .build(); + + when(hearingSlotsService.validateSessionAvailability(any(JsonObject.class))).thenReturn(response); + when(response.getStatus()).thenReturn(HttpStatus.SC_OK); + when(response.getEntity()).thenReturn(validateResponse); + + final Response result = courtSchedulerServiceAdapter.validateSessionAvailability(params); + + assertThat(result.getStatus(), is(HttpStatus.SC_OK)); + assertThat(result.getEntity(), is(validateResponse)); + } + + @Test + void shouldReturnErrorResponseWhenValidateSessionAvailabilityFails() { + final JsonObject params = javax.json.Json.createObjectBuilder() + .add("courtScheduleIdList", javax.json.Json.createArrayBuilder() + .add(javax.json.Json.createObjectBuilder() + .add("courtScheduleId", "f8254db1-1683-483e-afb3-b87fde5a0a26"))) + .add("duration", 30) + .build(); + + when(hearingSlotsService.validateSessionAvailability(any(JsonObject.class))).thenReturn(response); + when(response.getStatus()).thenReturn(HttpStatus.SC_BAD_REQUEST); + when(response.hasEntity()).thenReturn(true); + when(response.getEntity()).thenReturn("Validation failed"); + + final Response result = courtSchedulerServiceAdapter.validateSessionAvailability(params); + + assertThat(result.getStatus(), is(HttpStatus.SC_BAD_REQUEST)); + } } diff --git a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/HearingSlotsServiceTest.java b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/HearingSlotsServiceTest.java index b38cebb37..1b45c285c 100644 --- a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/HearingSlotsServiceTest.java +++ b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/service/HearingSlotsServiceTest.java @@ -23,6 +23,7 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; @@ -65,6 +66,8 @@ class HearingSlotsServiceTest { private ArgumentCaptor httpPutCaptor; @Captor private ArgumentCaptor httpDeleteCaptor; + @Captor + private ArgumentCaptor httpPostCaptor; @InjectMocks private HearingSlotsService hearingSlotsService; @@ -75,7 +78,7 @@ void setUp() { } @Test - public void shouldSearchSuccessfully() throws Exception { + void shouldSearchSuccessfully() throws Exception { // Given Map params = new HashMap<>(); params.put("key", "value"); @@ -102,7 +105,7 @@ public void shouldSearchSuccessfully() throws Exception { } @Test - public void shouldDeleteSuccessfully() throws Exception { + void shouldDeleteSuccessfully() throws Exception { // Given when(systemUserProvider.getContextSystemUserId()).thenReturn(java.util.Optional.of(TEST_USER_ID)); @@ -124,7 +127,7 @@ public void shouldDeleteSuccessfully() throws Exception { } @Test - public void shouldGetCourtSchedulerHearingIdsSuccessfully() throws Exception { + void shouldGetCourtSchedulerHearingIdsSuccessfully() throws Exception { // Given Map params = new HashMap<>(); params.put("key", "value"); @@ -151,7 +154,7 @@ public void shouldGetCourtSchedulerHearingIdsSuccessfully() throws Exception { } @Test - public void shouldThrowExceptionWhenSearchParamsAreNull() { + void shouldThrowExceptionWhenSearchParamsAreNull() { // Given Map params = null; @@ -164,7 +167,7 @@ public void shouldThrowExceptionWhenSearchParamsAreNull() { } @Test - public void shouldHandleHttpErrorResponse() throws Exception { + void shouldHandleHttpErrorResponse() throws Exception { // Given Map params = new HashMap<>(); params.put("key", "value"); @@ -190,7 +193,7 @@ public void shouldHandleHttpErrorResponse() throws Exception { } @Test - public void shouldHandleIOException() throws Exception { + void shouldHandleIOException() throws Exception { // Given Map params = new HashMap<>(); params.put("key", "value"); @@ -210,7 +213,7 @@ public void shouldHandleIOException() throws Exception { } @Test - public void shouldGetCourtSchedulesByIdSuccessfully() throws Exception { + void shouldGetCourtSchedulesByIdSuccessfully() throws Exception { // Given Map params = new HashMap<>(); params.put("key", "value"); @@ -237,7 +240,7 @@ public void shouldGetCourtSchedulesByIdSuccessfully() throws Exception { } @Test - public void shouldThrowExceptionWhenGetCourtSchedulesByIdParamsAreNull() { + void shouldThrowExceptionWhenGetCourtSchedulesByIdParamsAreNull() { // Given Map params = null; @@ -245,12 +248,12 @@ public void shouldThrowExceptionWhenGetCourtSchedulesByIdParamsAreNull() { try { hearingSlotsService.getCourtSchedulesById(params); } catch (DataValidationException e) { - assertThat(e.getMessage(), is("Params for search application/vnd.courtscheduler.search.courtschedules.by.id+json is null ....")); + assertThat(e.getMessage(), is("Params for search application/vnd.courtscheduler.search.court-schedules-by-id+json is null ....")); } } @Test - public void shouldHandleGetCourtSchedulesByIdError() throws Exception { + void shouldHandleGetCourtSchedulesByIdError() throws Exception { // Given Map params = new HashMap<>(); params.put("key", "value"); @@ -273,7 +276,7 @@ public void shouldHandleGetCourtSchedulesByIdError() throws Exception { } @Test - public void shouldHandleDeleteError() throws Exception { + void shouldHandleDeleteError() throws Exception { // Given when(systemUserProvider.getContextSystemUserId()).thenReturn(java.util.Optional.of(TEST_USER_ID)); @@ -295,7 +298,7 @@ public void shouldHandleDeleteError() throws Exception { } @Test - public void shouldHandleDeleteIOException() throws Exception { + void shouldHandleDeleteIOException() throws Exception { // Given when(systemUserProvider.getContextSystemUserId()).thenReturn(java.util.Optional.of(TEST_USER_ID)); @@ -315,7 +318,7 @@ public void shouldHandleDeleteIOException() throws Exception { } @Test - public void shouldHandleResponseEntityConversionError() throws Exception { + void shouldHandleResponseEntityConversionError() throws Exception { // Given Map params = new HashMap<>(); params.put("key", "value"); @@ -343,7 +346,7 @@ public void shouldHandleResponseEntityConversionError() throws Exception { } @Test - public void shouldHandleMultipleQueryParameters() throws Exception { + void shouldHandleMultipleQueryParameters() throws Exception { // Given Map params = new HashMap<>(); params.put("key1", "value1"); @@ -371,7 +374,7 @@ public void shouldHandleMultipleQueryParameters() throws Exception { } @Test - public void shouldHandleSystemUserProviderError() { + void shouldHandleSystemUserProviderError() { // Given Map params = new HashMap<>(); params.put("key", "value"); @@ -387,7 +390,7 @@ public void shouldHandleSystemUserProviderError() { @Test - public void shouldSearchAndBookSlotsSuccessfully() throws Exception { + void shouldSearchAndBookSlotsSuccessfully() throws Exception { // Given Map params = new HashMap<>(); params.put("key", "value"); @@ -414,7 +417,7 @@ public void shouldSearchAndBookSlotsSuccessfully() throws Exception { } @Test - public void shouldThrowExceptionWhenSearchAndBookParamsAreNull() { + void shouldThrowExceptionWhenSearchAndBookParamsAreNull() { // Given Map params = null; @@ -425,4 +428,256 @@ public void shouldThrowExceptionWhenSearchAndBookParamsAreNull() { assertThat(e.getMessage(), is("Params for search application/vnd.courtscheduler.search.book.hearing.slots+json is null ....")); } } + + // ─── validateSessionAvailability tests ────────────────────────────── + + @Test + void shouldValidateSessionAvailabilitySuccessfully() throws Exception { + // Given + javax.json.JsonObject params = javax.json.Json.createObjectBuilder() + .add("courtScheduleIdList", javax.json.Json.createArrayBuilder() + .add(javax.json.Json.createObjectBuilder() + .add("courtScheduleId", "f8254db1-1683-483e-afb3-b87fde5a0a26"))) + .add("duration", 30) + .build(); + when(systemUserProvider.getContextSystemUserId()).thenReturn(java.util.Optional.of(TEST_USER_ID)); + + try (MockedStatic mockedStatic = Mockito.mockStatic(HttpClientBuilder.class)) { + mockedStatic.when(HttpClientBuilder::create).thenReturn(httpClientBuilder); + when(httpClientBuilder.build()).thenReturn(httpClient); + when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(Response.Status.OK.getStatusCode()); + when(httpResponse.getEntity()).thenReturn(mock(org.apache.http.HttpEntity.class)); + + // When + Response response = hearingSlotsService.validateSessionAvailability(params); + + // Then + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + verify(httpClient).execute(httpPostCaptor.capture()); + HttpPost capturedPost = httpPostCaptor.getValue(); + assertThat(capturedPost.getURI().toString(), is(BASE_URI + "/validate-session-availability")); + } + } + + @Test + void shouldThrowExceptionWhenValidateSessionAvailabilityParamsAreNull() { + // Given + javax.json.JsonObject params = null; + + // When/Then + try { + hearingSlotsService.validateSessionAvailability(params); + } catch (DataValidationException e) { + assertThat(e.getMessage(), is("Payload for application/vnd.courtscheduler.validate.session.availability+json is null or empty ....")); + } + } + + @Test + void shouldThrowExceptionWhenValidateSessionAvailabilityPayloadIsEmpty() { + // Given + javax.json.JsonObject emptyPayload = javax.json.Json.createObjectBuilder().build(); + + // When/Then + try { + hearingSlotsService.validateSessionAvailability(emptyPayload); + } catch (DataValidationException e) { + assertThat(e.getMessage(), is("Payload for application/vnd.courtscheduler.validate.session.availability+json is null or empty ....")); + } + } + + @Test + void shouldHandlePostErrorResponse() throws Exception { + // Given + javax.json.JsonObject payload = javax.json.Json.createObjectBuilder() + .add("courtScheduleIdList", javax.json.Json.createArrayBuilder() + .add(javax.json.Json.createObjectBuilder() + .add("courtScheduleId", "f8254db1-1683-483e-afb3-b87fde5a0a26"))) + .add("duration", 30) + .build(); + when(systemUserProvider.getContextSystemUserId()).thenReturn(java.util.Optional.of(TEST_USER_ID)); + + try (MockedStatic mockedStatic = Mockito.mockStatic(HttpClientBuilder.class)) { + mockedStatic.when(HttpClientBuilder::create).thenReturn(httpClientBuilder); + when(httpClientBuilder.build()).thenReturn(httpClient); + when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(Response.Status.BAD_REQUEST.getStatusCode()); + when(httpResponse.getEntity()).thenReturn(mock(org.apache.http.HttpEntity.class)); + + // When + Response response = hearingSlotsService.validateSessionAvailability(payload); + + // Then + assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode())); + } + } + + @Test + void shouldHandleIOExceptionWhenValidatingSessionAvailability() throws Exception { + // Given + javax.json.JsonObject payload = javax.json.Json.createObjectBuilder() + .add("courtScheduleIdList", javax.json.Json.createArrayBuilder() + .add(javax.json.Json.createObjectBuilder() + .add("courtScheduleId", "f8254db1-1683-483e-afb3-b87fde5a0a26"))) + .add("duration", 30) + .build(); + when(systemUserProvider.getContextSystemUserId()).thenReturn(java.util.Optional.of(TEST_USER_ID)); + + try (MockedStatic mockedStatic = Mockito.mockStatic(HttpClientBuilder.class)) { + mockedStatic.when(HttpClientBuilder::create).thenReturn(httpClientBuilder); + when(httpClientBuilder.build()).thenReturn(httpClient); + when(httpClient.execute(any(HttpPost.class))).thenThrow(new IOException("Connection refused")); + + // When + Response response = hearingSlotsService.validateSessionAvailability(payload); + + // Then + assertThat(response.getStatus(), is(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())); + } + } + + @Test + void shouldHandleNullEntityInPostResponse() throws Exception { + // Given + javax.json.JsonObject payload = javax.json.Json.createObjectBuilder() + .add("courtScheduleIdList", javax.json.Json.createArrayBuilder() + .add(javax.json.Json.createObjectBuilder() + .add("courtScheduleId", "f8254db1-1683-483e-afb3-b87fde5a0a26"))) + .add("duration", 30) + .build(); + when(systemUserProvider.getContextSystemUserId()).thenReturn(java.util.Optional.of(TEST_USER_ID)); + + try (MockedStatic mockedStatic = Mockito.mockStatic(HttpClientBuilder.class)) { + mockedStatic.when(HttpClientBuilder::create).thenReturn(httpClientBuilder); + when(httpClientBuilder.build()).thenReturn(httpClient); + when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(Response.Status.OK.getStatusCode()); + when(httpResponse.getEntity()).thenReturn(null); + + // When + Response response = hearingSlotsService.validateSessionAvailability(payload); + + // Then + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + } + } + + @Test + void shouldConvertNonBlankPostResponseBody() throws Exception { + // Given + javax.json.JsonObject payload = javax.json.Json.createObjectBuilder() + .add("courtScheduleIdList", javax.json.Json.createArrayBuilder() + .add(javax.json.Json.createObjectBuilder() + .add("courtScheduleId", "f8254db1-1683-483e-afb3-b87fde5a0a26"))) + .add("duration", 30) + .build(); + when(systemUserProvider.getContextSystemUserId()).thenReturn(java.util.Optional.of(TEST_USER_ID)); + + try (MockedStatic mockedStatic = Mockito.mockStatic(HttpClientBuilder.class); + MockedStatic entityUtilsMockedStatic = Mockito.mockStatic(EntityUtils.class)) { + mockedStatic.when(HttpClientBuilder::create).thenReturn(httpClientBuilder); + when(httpClientBuilder.build()).thenReturn(httpClient); + when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(Response.Status.OK.getStatusCode()); + org.apache.http.HttpEntity entity = mock(org.apache.http.HttpEntity.class); + when(httpResponse.getEntity()).thenReturn(entity); + entityUtilsMockedStatic.when(() -> EntityUtils.toString(entity)).thenReturn("{\"available\":true}"); + when(stringToJsonObjectConverter.convert("{\"available\":true}")).thenReturn(mock(javax.json.JsonObject.class)); + + // When + Response response = hearingSlotsService.validateSessionAvailability(payload); + + // Then + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + verify(stringToJsonObjectConverter).convert("{\"available\":true}"); + } + } + + // ─── listHearingInCourtSessions tests ──────────────────────────────── + + @Test + void shouldListHearingInCourtSessionsSuccessfully() throws Exception { + // Given + javax.json.JsonObject payload = javax.json.Json.createObjectBuilder() + .add("hearingSlots", javax.json.Json.createArrayBuilder().build()) + .build(); + when(systemUserProvider.getContextSystemUserId()).thenReturn(java.util.Optional.of(TEST_USER_ID)); + + try (MockedStatic mockedStatic = Mockito.mockStatic(HttpClientBuilder.class); + MockedStatic entityUtilsMockedStatic = Mockito.mockStatic(EntityUtils.class)) { + mockedStatic.when(HttpClientBuilder::create).thenReturn(httpClientBuilder); + when(httpClientBuilder.build()).thenReturn(httpClient); + when(httpClient.execute(any(HttpPut.class))).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(Response.Status.OK.getStatusCode()); + org.apache.http.HttpEntity entity = mock(org.apache.http.HttpEntity.class); + when(httpResponse.getEntity()).thenReturn(entity); + entityUtilsMockedStatic.when(() -> EntityUtils.toString(entity)).thenReturn("{\"result\":\"success\"}"); + when(stringToJsonObjectConverter.convert("{\"result\":\"success\"}")).thenReturn(mock(javax.json.JsonObject.class)); + + // When + Response response = hearingSlotsService.listHearingInCourtSessions(payload); + + // Then + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + verify(httpClient).execute(httpPutCaptor.capture()); + HttpPut capturedPut = httpPutCaptor.getValue(); + assertThat(capturedPut.getURI().toString(), is(BASE_URI + "/list/hearingslots")); + // Verify the body is produced by JsonObject.toString(), not objectMapper + byte[] requestBody = capturedPut.getEntity().getContent().readAllBytes(); + assertThat(new String(requestBody, java.nio.charset.StandardCharsets.UTF_8), is(payload.toString())); + } + } + + @Test + void shouldHandleListHearingInCourtSessionsErrorResponse() throws Exception { + // Given + javax.json.JsonObject payload = javax.json.Json.createObjectBuilder() + .add("hearingSlots", javax.json.Json.createArrayBuilder().build()) + .build(); + when(systemUserProvider.getContextSystemUserId()).thenReturn(java.util.Optional.of(TEST_USER_ID)); + + try (MockedStatic mockedStatic = Mockito.mockStatic(HttpClientBuilder.class); + MockedStatic entityUtilsMockedStatic = Mockito.mockStatic(EntityUtils.class)) { + mockedStatic.when(HttpClientBuilder::create).thenReturn(httpClientBuilder); + when(httpClientBuilder.build()).thenReturn(httpClient); + when(httpClient.execute(any(HttpPut.class))).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(Response.Status.BAD_REQUEST.getStatusCode()); + org.apache.http.HttpEntity entity = mock(org.apache.http.HttpEntity.class); + when(httpResponse.getEntity()).thenReturn(entity); + entityUtilsMockedStatic.when(() -> EntityUtils.toString(entity)).thenReturn("error message"); + + // When + Response response = hearingSlotsService.listHearingInCourtSessions(payload); + + // Then + assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode())); + } + } + + @Test + void shouldHandleListHearingInCourtSessionsIOException() throws Exception { + // Given + javax.json.JsonObject payload = javax.json.Json.createObjectBuilder() + .add("hearingSlots", javax.json.Json.createArrayBuilder().build()) + .build(); + when(systemUserProvider.getContextSystemUserId()).thenReturn(java.util.Optional.of(TEST_USER_ID)); + + try (MockedStatic mockedStatic = Mockito.mockStatic(HttpClientBuilder.class)) { + mockedStatic.when(HttpClientBuilder::create).thenReturn(httpClientBuilder); + when(httpClientBuilder.build()).thenReturn(httpClient); + when(httpClient.execute(any(HttpPut.class))).thenThrow(new IOException("Connection refused")); + + // When + Response response = hearingSlotsService.listHearingInCourtSessions(payload); + + // Then + assertThat(response.getStatus(), is(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())); + } + } } \ No newline at end of file diff --git a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/CommonXhibitReferenceDataServiceTest.java b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/CommonXhibitReferenceDataServiceTest.java index 694e1538f..c5faaf5e3 100644 --- a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/CommonXhibitReferenceDataServiceTest.java +++ b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/CommonXhibitReferenceDataServiceTest.java @@ -111,7 +111,7 @@ public void shouldGetCrownCourtDetails() { } @Test - public void shouldGetCriminalCourtDetailsForMagistratesCommittingCourt() { + public void shouldGetMagsCourtDetails() { final UUID courtCentreId = randomUUID(); final String ouCode = "OUCODE"; @@ -134,9 +134,9 @@ public void shouldGetCriminalCourtDetailsForMagistratesCommittingCourt() { .withCourtType(courtType) .build(); - when(referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId)).thenReturn(Optional.of(Arrays.asList(courtMapping))); + when(referenceDataCache.getMagsCourtMappingsMapCache(courtCentreId)).thenReturn(Optional.of(Arrays.asList(courtMapping))); - final CourtLocation courtDetails = commonXhibitReferenceDataService.getCriminalCourtDetails(courtCentreId); + final CourtLocation courtDetails = commonXhibitReferenceDataService.getMagsCourtDetails(courtCentreId); assertEquals(courtDetails.getOuCode(), ouCode); assertEquals(courtDetails.getCrestCourtId(), courtId); @@ -147,40 +147,6 @@ public void shouldGetCriminalCourtDetailsForMagistratesCommittingCourt() { assertEquals(courtDetails.getCourtType(), courtType); } - @Test - public void shouldGetCriminalCourtDetailsByCourtHouseTypeWhenCrownAndMagsMappingsPresent() { - - final UUID courtCentreId = randomUUID(); - - final CourtMapping crownMapping = new CourtMapping.Builder() - .withOucode("OUCROWN") - .withCrestCourtId("100") - .withCrestCourtSiteId("101") - .withCrestCourtName("CROWN") - .withCrestCourtSiteName("CROWN") - .withCrestCourtShortName("CR") - .withCrestCourtSiteCode("C") - .withCourtType("CROWN_COURT") - .build(); - - final CourtMapping magsMapping = new CourtMapping.Builder() - .withOucode("OUMAGS") - .withCrestCourtId("200") - .withCrestCourtSiteId("201") - .withCrestCourtName("MAGS") - .withCrestCourtSiteName("MAGS") - .withCrestCourtShortName("MG") - .withCrestCourtSiteCode("M") - .withCourtType("MAGISTRATES_COURT") - .build(); - - when(referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId)) - .thenReturn(Optional.of(Arrays.asList(crownMapping, magsMapping))); - - assertEquals("C", commonXhibitReferenceDataService.getCriminalCourtDetails(courtCentreId, "CROWN_COURT").getCourtSiteCode()); - assertEquals("M", commonXhibitReferenceDataService.getCriminalCourtDetails(courtCentreId, "MAGISTRATES_COURT").getCourtSiteCode()); - } - @Test public void shouldGetJudiciary() { final String titleSuffix = "judge"; diff --git a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataCacheTest.java b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataCacheTest.java index d2d0848e5..eade46b7c 100644 --- a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataCacheTest.java +++ b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataCacheTest.java @@ -1,10 +1,8 @@ package uk.gov.moj.cpp.listing.common.xhibit; import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; import static java.util.UUID.fromString; import static java.util.UUID.randomUUID; -import static org.mockito.Mockito.*; import static uk.gov.justice.services.messaging.JsonObjects.createArrayBuilder; import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; import static org.hamcrest.MatcherAssert.assertThat; @@ -13,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; import static uk.gov.justice.services.test.utils.core.reflection.ReflectionUtil.getValueOfField; import static uk.gov.moj.cpp.listing.common.utils.FileUtil.givenPayload; @@ -44,7 +42,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -226,35 +223,6 @@ public void shouldPopulateCrownCourtMappingsMapCache() { assertThat(actualCourtMappingList.get().get(0).getOucode(), is(expectedCourtMappingList.get(0).getOucode())); } - @Test - public void shouldLazyLoadCrownCourtMappingsWhenNotInStartupCache() { - final UUID manchesterCourtCentreId = fromString("e3e762ed-8271-3454-b59b-8a13f7cc8870"); - final String manchesterOuCode = "C06MC00"; - final CourtMapping manchesterCourtMapping = getManchesterCourtMapping(); - - organisationUnitList = Optional.of(new OrganisationUnitList(asList( - new OrganisationUnit.Builder() - .withId(manchesterCourtCentreId) - .withOucode(manchesterOuCode) - .build()))); - - hearingTypesListTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(Optional.of(new CourtMappingsList(emptyList()))); - when(referenceDataLoader.getXhibitCrownCourtMappings(manchesterCourtCentreId)) - .thenReturn(Optional.of(new CourtMappingsList(asList(manchesterCourtMapping)))); - - referenceDataCache.initReferenceData(); - - final Optional> actualCourtMappingList = - referenceDataCache.getCrownCourtMappingsMapCache(manchesterCourtCentreId); - - assertThat(actualCourtMappingList.isPresent(), is(true)); - assertThat(actualCourtMappingList.get().get(0).getOucode(), is(manchesterOuCode)); - assertThat(actualCourtMappingList.get().get(0).getCrestCourtId(), is("435")); - } - @Test public void shouldPopulateMagsCourtMappingsMapCache() { @@ -277,401 +245,6 @@ public void shouldPopulateMagsCourtMappingsMapCache() { assertThat(actualCourtMappingList.get().get(0).getOucode(), is(expectedCourtMappingList.get(0).getOucode())); } - @Test - public void shouldPopulateCpXhibitCourtMappingsMapCacheFromMagsQueryWhenMagsSucceeds() { - - initializeTestData(); - - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(magsCourtMappingsList); - - referenceDataCache.initReferenceData(); - - final CourtMapping magsMapping = getCourtMapping("MAGISTRATES_COURT"); - - final Optional> actualCourtMappingList = referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - assertThat(actualCourtMappingList.isPresent(), is(true)); - assertThat(actualCourtMappingList.get().size(), is(1)); - assertThat(actualCourtMappingList.get().get(0).getCourtType(), is(magsMapping.getCourtType())); - assertThat(actualCourtMappingList.get().get(0).getOucode(), is(magsMapping.getOucode())); - } - - @Test - public void shouldFallbackToCrownCourtMappingsWhenMagsReturnsEmpty() { - - initializeTestData(); - - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(Optional.empty()); - when(referenceDataLoader.getXhibitCrownCourtMappings(eq(courtCentreId))).thenReturn(crownCourtMappingsList); - - referenceDataCache.initReferenceData(); - - final CourtMapping crownMapping = getCourtMapping("CROWN_COURT"); - - final Optional> actualCourtMappingList = referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - assertThat(actualCourtMappingList.isPresent(), is(true)); - assertThat(actualCourtMappingList.get().size(), is(1)); - assertThat(actualCourtMappingList.get().get(0).getCourtType(), is(crownMapping.getCourtType())); - } - - @Test - public void shouldReturnEmptyForGetCrownCourtMappingsWhenCourtCentreIdUnknown() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - referenceDataCache.initReferenceData(); - - assertThat(referenceDataCache.getCrownCourtMappingsMapCache(randomUUID()).isPresent(), is(false)); - } - - @Test - public void shouldReturnEmptyForGetCrownCourtMappingsWhenOucodeNotInCrownCache() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(Optional.of(new CourtMappingsList(emptyList()))); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - referenceDataCache.initReferenceData(); - - assertThat(referenceDataCache.getCrownCourtMappingsMapCache(courtCentreId).isPresent(), is(false)); - } - - @Test - public void shouldReturnEmptyForGetMagsCourtMappingsWhenCourtCentreIdUnknown() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - referenceDataCache.initReferenceData(); - - assertThat(referenceDataCache.getMagsCourtMappingsMapCache(randomUUID()).isPresent(), is(false)); - } - - @Test - public void shouldReturnEmptyForGetMagsCourtMappingsWhenLoaderReturnsEmptyOptional() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(Optional.empty()); - referenceDataCache.initReferenceData(); - - assertThat(referenceDataCache.getMagsCourtMappingsMapCache(courtCentreId).isPresent(), is(false)); - } - - @Test - public void shouldReturnEmptyForGetMagsCourtMappingsWhenCpMappingListIsNull() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(Optional.of(new CourtMappingsList(null))); - referenceDataCache.initReferenceData(); - - assertThat(referenceDataCache.getMagsCourtMappingsMapCache(courtCentreId).isPresent(), is(false)); - } - - @Test - public void shouldReturnPresentWithEmptyListForGetMagsWhenMagsReturnsEmptyCpList() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(Optional.of(new CourtMappingsList(emptyList()))); - referenceDataCache.initReferenceData(); - - final Optional> actual = referenceDataCache.getMagsCourtMappingsMapCache(courtCentreId); - assertThat(actual.isPresent(), is(true)); - assertThat(actual.get().isEmpty(), is(true)); - } - - @Test - public void shouldHitMagsLoaderOnlyOncePerOucodeWhenGetMagsCalledTwice() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(magsCourtMappingsList); - referenceDataCache.initReferenceData(); - - referenceDataCache.getMagsCourtMappingsMapCache(courtCentreId); - referenceDataCache.getMagsCourtMappingsMapCache(courtCentreId); - - verify(referenceDataLoader, times(1)).getXhibitMagsCourtMappings(eq(ouCode)); - } - - @Test - public void shouldReturnEmptyForCpXhibitCourtMappingsWhenCourtCentreIdUnknown() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - referenceDataCache.initReferenceData(); - - assertThat(referenceDataCache.getCpXhibitCourtMappingsMapCache(randomUUID()).isPresent(), is(false)); - } - - @Test - public void shouldFallbackCpXhibitToCrownWhenMagsReturnsEmptyCpMappingList() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(Optional.of(new CourtMappingsList(emptyList()))); - when(referenceDataLoader.getXhibitCrownCourtMappings(eq(courtCentreId))).thenReturn(crownCourtMappingsList); - referenceDataCache.initReferenceData(); - - final CourtMapping crownMapping = getCourtMapping("CROWN_COURT"); - final Optional> actual = referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - assertThat(actual.isPresent(), is(true)); - assertThat(actual.get().size(), is(1)); - assertThat(actual.get().get(0).getCourtType(), is(crownMapping.getCourtType())); - verify(referenceDataLoader, times(1)).getXhibitCrownCourtMappings(eq(courtCentreId)); - } - - @Test - public void shouldReturnPresentEmptyListForCpXhibitWhenMagsAndCrownBothYieldNoMappings() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(Optional.of(new CourtMappingsList(emptyList()))); - when(referenceDataLoader.getXhibitCrownCourtMappings(eq(courtCentreId))).thenReturn(Optional.of(new CourtMappingsList(emptyList()))); - referenceDataCache.initReferenceData(); - - final Optional> actual = referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - assertThat(actual.isPresent(), is(true)); - assertThat(actual.get().isEmpty(), is(true)); - } - - @Test - public void shouldLazyCacheCpXhibitPerCourtCentreId() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(magsCourtMappingsList); - referenceDataCache.initReferenceData(); - - referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - verify(referenceDataLoader, times(1)).getXhibitMagsCourtMappings(eq(ouCode)); - verify(referenceDataLoader, never()).getXhibitCrownCourtMappings(eq(courtCentreId)); - } - - - - /** - * When the mags endpoint returns a non-empty list, every CourtMapping field - * from the source must be carried over to the rebuilt mapping AND the - * courtType must be forced to "MAGISTRATES_COURT" regardless of whatever - * value the source had. - */ - @Test - public void shouldCopyAllFieldsFromMagsMappingAndOverrideCourtTypeToMagistratesCourt() { - final UUID mappingId = randomUUID(); - final String oucode = "C01BL00"; - final String courtIdVal = "432"; - final String courtSiteId = "433"; - final String courtName = "BLACKFRIARS CROWN"; - final String courtShortName = "BLF"; - final String courtSiteName = "BLACKFRIARS SITE"; - final String courtSiteCode = "B"; - - final CourtMapping sourceMapping = new CourtMapping.Builder() - .withId(mappingId) - .withOucode(oucode) - .withCrestCourtId(courtIdVal) - .withCrestCourtSiteId(courtSiteId) - .withCrestCourtName(courtName) - .withCrestCourtShortName(courtShortName) - .withCrestCourtSiteName(courtSiteName) - .withCrestCourtSiteCode(courtSiteCode) - .withCourtType("CROWN_COURT") // original type – must be overridden - .build(); - - final CourtMappingsList magsList = new CourtMappingsList(asList(sourceMapping)); - - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(Optional.of(magsList)); - referenceDataCache.initReferenceData(); - - final Optional> result = referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - assertThat(result.isPresent(), is(true)); - assertThat(result.get(), hasSize(1)); - - final CourtMapping rebuilt = result.get().get(0); - assertThat(rebuilt.getId(), is(mappingId)); - assertThat(rebuilt.getOucode(), is(oucode)); - assertThat(rebuilt.getCrestCourtId(), is(courtIdVal)); - assertThat(rebuilt.getCrestCourtSiteId(), is(courtSiteId)); - assertThat(rebuilt.getCrestCourtName(), is(courtName)); - assertThat(rebuilt.getCrestCourtShortName(), is(courtShortName)); - assertThat(rebuilt.getCrestCourtSiteName(), is(courtSiteName)); - assertThat(rebuilt.getCrestCourtSiteCode(), is(courtSiteCode)); - // *** The key assertion: courtType must ALWAYS be MAGISTRATES_COURT *** - assertThat(rebuilt.getCourtType(), is("MAGISTRATES_COURT")); - } - - /** - * When mags returns multiple mappings, every entry must be rebuilt with - * MAGISTRATES_COURT courtType and the list size must match. - */ - @Test - public void shouldRebuildAllMagsMappingsWithMagistratesCourtType() { - final CourtMapping mapping1 = new CourtMapping.Builder() - .withId(randomUUID()) - .withOucode(ouCode) - .withCrestCourtId("111") - .withCourtType("CROWN_COURT") - .build(); - final CourtMapping mapping2 = new CourtMapping.Builder() - .withId(randomUUID()) - .withOucode(ouCode) - .withCrestCourtId("222") - .withCourtType("CROWN_COURT") - .build(); - - final CourtMappingsList magsList = new CourtMappingsList(asList(mapping1, mapping2)); - - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(Optional.of(magsList)); - referenceDataCache.initReferenceData(); - - final Optional> result = referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - assertThat(result.isPresent(), is(true)); - assertThat(result.get(), hasSize(2)); - result.get().forEach(m -> assertThat(m.getCourtType(), is("MAGISTRATES_COURT"))); - // Crown endpoint must never have been consulted - verify(referenceDataLoader, never()).getXhibitCrownCourtMappings(eq(courtCentreId)); - } - - /** - * When mags returns a CourtMappingsList whose inner list is null - * the filter step treats it as empty → fallback to crown. - */ - @Test - public void shouldFallbackToCrownWhenMagsReturnsNullInnerList() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - // null inner list – getCpXhibitCourtMappings() returns null - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))) - .thenReturn(Optional.of(new CourtMappingsList(null))); - when(referenceDataLoader.getXhibitCrownCourtMappings(eq(courtCentreId))) - .thenReturn(crownCourtMappingsList); - referenceDataCache.initReferenceData(); - - final Optional> result = referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - assertThat(result.isPresent(), is(true)); - assertThat(result.get(), hasSize(1)); - assertThat(result.get().get(0).getCourtType(), is("CROWN_COURT")); - verify(referenceDataLoader, times(1)).getXhibitCrownCourtMappings(eq(courtCentreId)); - } - - /** - * When mags is absent (Optional.empty()) AND crown returns Optional.empty(), - * the result must be present with an empty list (not Optional.empty()). - */ - @Test - public void shouldReturnPresentEmptyListWhenMagsAbsentAndCrownReturnsEmptyOptional() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(Optional.empty()); - when(referenceDataLoader.getXhibitCrownCourtMappings(eq(courtCentreId))).thenReturn(Optional.empty()); - referenceDataCache.initReferenceData(); - - final Optional> result = referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - assertThat(result.isPresent(), is(true)); - assertThat(result.get().isEmpty(), is(true)); - } - - /** - * When mags is absent AND crown returns a CourtMappingsList with a null - * inner list, the filter drops it and the result must be an empty list. - */ - @Test - public void shouldReturnPresentEmptyListWhenMagsAbsentAndCrownReturnsNullInnerList() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(Optional.empty()); - when(referenceDataLoader.getXhibitCrownCourtMappings(eq(courtCentreId))) - .thenReturn(Optional.of(new CourtMappingsList(null))); - referenceDataCache.initReferenceData(); - - final Optional> result = referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - assertThat(result.isPresent(), is(true)); - assertThat(result.get().isEmpty(), is(true)); - } - - /** - * The resolved list is stored in the lazy cache: regardless of how many - * times getCpXhibitCourtMappingsMapCache is called, the mags loader must - * only be consulted once per court-centre. - */ - @Test - public void shouldCallMagsLoaderOnlyOncePerCourtCentreIdOnRepeatedLookups() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(magsCourtMappingsList); - referenceDataCache.initReferenceData(); - - // Call three times - referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - verify(referenceDataLoader, times(1)).getXhibitMagsCourtMappings(eq(ouCode)); - verify(referenceDataLoader, never()).getXhibitCrownCourtMappings(eq(courtCentreId)); - } - - /** - * When mags succeeds (non-empty list) the crown-by-courtCentreId endpoint - * must never be invoked at all. - */ - @Test - public void shouldNeverCallCrownLoaderWhenMagsSucceeds() { - initializeTestData(); - when(referenceDataLoader.getAllHearingTypesList()).thenReturn(hearingTypesList); - when(referenceDataLoader.getXhibitCrownCourtMappings()).thenReturn(crownCourtMappingsList); - when(referenceDataLoader.getOrganisationUnitList()).thenReturn(organisationUnitList); - when(referenceDataLoader.getXhibitMagsCourtMappings(eq(ouCode))).thenReturn(magsCourtMappingsList); - referenceDataCache.initReferenceData(); - - referenceDataCache.getCpXhibitCourtMappingsMapCache(courtCentreId); - - verify(referenceDataLoader, never()).getXhibitCrownCourtMappings(eq(courtCentreId)); - } - - @Test public void shouldPopulateJudiciariesCache() { final UUID judiciaryId = randomUUID(); @@ -788,19 +361,6 @@ private void courtMappingsListTestData() { magsCourtMappingsList = Optional.of(new CourtMappingsList(asList(getCourtMapping("MAGISTRATES_COURT")))); } - private CourtMapping getManchesterCourtMapping() { - return new CourtMapping.Builder() - .withOucode("C06MC00") - .withCrestCourtId("435") - .withCrestCourtSiteId("435") - .withCrestCourtName("MANCHESTER") - .withCrestCourtSiteName("MANCHESTER") - .withCrestCourtShortName("MANCH") - .withCrestCourtSiteCode("A") - .withCourtType("CROWN_COURT") - .build(); - } - private CourtMapping getCourtMapping(final String courtType) { final String courtSiteId = "433"; final String crestCourtName = "BLACKFRIARS"; diff --git a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataLoaderTest.java b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataLoaderTest.java index 6dbb4b1b8..09057ba03 100644 --- a/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataLoaderTest.java +++ b/listing-common/src/test/java/uk/gov/moj/cpp/listing/common/xhibit/ReferenceDataLoaderTest.java @@ -4,13 +4,13 @@ import static uk.gov.justice.services.messaging.JsonObjects.createArrayBuilder; import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Answers.RETURNS_DEEP_STUBS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static uk.gov.justice.services.test.utils.core.reflection.ReflectionUtil.setField; import static uk.gov.moj.cpp.listing.common.utils.FileUtil.givenPayload; @@ -18,6 +18,7 @@ import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter; import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer; import uk.gov.justice.services.core.requester.Requester; +import uk.gov.justice.services.messaging.Envelope; import uk.gov.moj.cpp.listing.common.xhibit.exception.InvalidReferenceDataException; import uk.gov.moj.cpp.listing.domain.referencedata.CourtMapping; import uk.gov.moj.cpp.listing.domain.referencedata.CourtMappingsList; @@ -236,24 +237,6 @@ public void shouldGetXhibitCourtMappingsWithCourtCenter() { assertThat(courtId, is(courtMappingsList.get().getCpXhibitCourtMappings().get(0).getCrestCourtId())); } - @Test - public void shouldReturnEmptyWhenGetXhibitCrownCourtMappingsNoParamsAndResponseEnvelopeIsNull() { - when(requester.requestAsAdmin(any(), eq(CourtMappingsList.class))).thenReturn(null); - - final Optional courtMappingsList = referenceDataLoader.getXhibitCrownCourtMappings(); - - assertThat(courtMappingsList.isPresent(), is(false)); - } - - @Test - public void shouldReturnEmptyWhenGetXhibitCrownCourtMappingsByCourtCentreIdAndResponseEnvelopeIsNull() { - when(requester.requestAsAdmin(any(), eq(CourtMappingsList.class))).thenReturn(null); - - final Optional courtMappingsList = referenceDataLoader.getXhibitCrownCourtMappings(UUID.randomUUID()); - - assertThat(courtMappingsList.isPresent(), is(false)); - } - @Test public void shouldGetXhibitMagsCourtMappings() { final String ouCode = "OUCODE"; @@ -286,67 +269,6 @@ public void shouldGetXhibitMagsCourtMappings() { assertThat(courtId, is(courtMappingsList.get().getCpXhibitCourtMappings().get(0).getCrestCourtId())); } - @Test - public void shouldReturnEmptyWhenMagsPayloadIsEmptyRecordFromReferencedata() { - when(requester.requestAsAdmin(any(), eq(CourtMapping.class)).payload()).thenReturn(new CourtMapping.Builder().build()); - - final Optional courtMappingsList = referenceDataLoader.getXhibitMagsCourtMappings("OUCODE"); - - assertThat(courtMappingsList.isPresent(), equalTo(false)); - } - - @Test - public void shouldReturnEmptyWhenGetXhibitMagsCourtMappingsAndResponseEnvelopeIsNull() { - when(requester.requestAsAdmin(any(), eq(CourtMapping.class))).thenReturn(null); - - final Optional courtMappingsList = referenceDataLoader.getXhibitMagsCourtMappings("OUCODE"); - - assertThat(courtMappingsList.isPresent(), is(false)); - } - - @Test - public void shouldReturnEmptyWhenGetXhibitMagsCourtMappingsAndPayloadIsNull() { - when(requester.requestAsAdmin(any(), eq(CourtMapping.class)).payload()).thenReturn(null); - - final Optional courtMappingsList = referenceDataLoader.getXhibitMagsCourtMappings("OUCODE"); - - assertThat(courtMappingsList.isPresent(), is(false)); - } - - @Test - public void shouldReturnMagsMappingsWhenPayloadHasOucodeOnlyAndIdNull() { - final String ouCode = "ONLY_OU"; - final CourtMapping courtMapping = new CourtMapping.Builder() - .withOucode(ouCode) - .withCrestCourtId("432") - .build(); - - when(requester.requestAsAdmin(any(), eq(CourtMapping.class)).payload()).thenReturn(courtMapping); - - final Optional courtMappingsList = referenceDataLoader.getXhibitMagsCourtMappings(ouCode); - - assertThat(courtMappingsList.isPresent(), is(true)); - assertThat(courtMappingsList.get().getCpXhibitCourtMappings().get(0).getOucode(), is(ouCode)); - assertThat(courtMappingsList.get().getCpXhibitCourtMappings().get(0).getId(), is(nullValue())); - } - - @Test - public void shouldReturnMagsMappingsWhenPayloadHasIdOnlyAndOucodeNull() { - final UUID mappingId = randomUUID(); - final CourtMapping courtMapping = new CourtMapping.Builder() - .withId(mappingId) - .withCrestCourtId("432") - .build(); - - when(requester.requestAsAdmin(any(), eq(CourtMapping.class)).payload()).thenReturn(courtMapping); - - final Optional courtMappingsList = referenceDataLoader.getXhibitMagsCourtMappings("ANY"); - - assertThat(courtMappingsList.isPresent(), is(true)); - assertThat(courtMappingsList.get().getCpXhibitCourtMappings().get(0).getId(), is(mappingId)); - assertThat(courtMappingsList.get().getCpXhibitCourtMappings().get(0).getOucode(), is(nullValue())); - } - @Test public void shouldGetCourtRoomMappingsList() { final UUID courtCentreId = randomUUID(); diff --git a/listing-common/src/test/resources/mock-data/azure.rotasl.getHearingSlots.stub-data.json b/listing-common/src/test/resources/mock-data/azure.rotasl.getHearingSlots.stub-data.json index 286712f7b..2405666ac 100644 --- a/listing-common/src/test/resources/mock-data/azure.rotasl.getHearingSlots.stub-data.json +++ b/listing-common/src/test/resources/mock-data/azure.rotasl.getHearingSlots.stub-data.json @@ -23,28 +23,28 @@ "availableDuration": 0, "judiciaries": [ { - "judiciaryId": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", + "id": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false }, { - "judiciaryId": "d424f73e-4a19-396d-a00c-f4c38d1c864e", + "id": "d424f73e-4a19-396d-a00c-f4c38d1c864e", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": false, - "benchChairman": true + "isDeputy": false, + "isBenchChairman": true }, { - "judiciaryId": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", + "id": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false } ], "slotStartTimes": [] diff --git a/listing-domain/listing-domain-aggregate/pom.xml b/listing-domain/listing-domain-aggregate/pom.xml index 900fd4872..d0f4e700e 100644 --- a/listing-domain/listing-domain-aggregate/pom.xml +++ b/listing-domain/listing-domain-aggregate/pom.xml @@ -3,7 +3,7 @@ listing-domain uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/Case.java b/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/Case.java index 2c2621450..7e6112d1a 100644 --- a/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/Case.java +++ b/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/Case.java @@ -38,20 +38,16 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.Map; -import java.util.HashMap; import java.util.UUID; import java.util.stream.Stream; @SuppressWarnings({"pmd:BeanMembersShouldSerialize", "squid:S1068"}) public class Case implements Aggregate { - private static final long serialVersionUID = 204L; + private static final long serialVersionUID = 203L; private final Set hearingIds = new HashSet<>(); - private final Map currentDefendants = new HashMap<>(); - private final Set defendantsToBeUpdated = new HashSet<>(); public List getHearingIds() { @@ -63,7 +59,7 @@ public Object apply(final Object event) { return match(event).with( when(HearingAddedToCase.class).apply(this::onHearingAddedToCase), when(HearingUpdatedToCase.class).apply(this::onHearingUpdatedToCase), - when(DefendantsToBeUpdated.class).apply(this::onDefendantsToBeUpdated), + when(DefendantsToBeUpdated.class).apply(e -> onDefendantsToBeUpdated()), when(DefendantsToBeUpdatedLater.class).apply(this::onDefendantsToBeUpdatedLater), when(OffencesToBeAdded.class).apply(e -> onOffencesToBeAdded()), when(OffencesToBeDeleted.class).apply(e -> onOffencesToBeDeleted()), @@ -96,37 +92,20 @@ public Stream updateHearing(final UUID caseId, final UUID allocatedHeari } public Stream updateDefendant(UUID caseId, Defendant defendant) { - // Build the new defendant event - uk.gov.justice.listing.events.Defendant newDefendantEvent = NewDomainToEventConverter.buildDefendant(defendant); - - // Check if we have an existing defendant for this ID - uk.gov.justice.listing.events.Defendant existingDefendant = currentDefendants.get(defendant.getId()); - if (existingDefendant != null) { - // Merge: Use updateEventDefendant logic to preserve existing values where new ones are null - newDefendantEvent = NewDomainToEventConverter.updateEventDefendant( - NewDomainToEventConverter.buildNewBaseDefendant(defendant), // Convert domain to NewBaseDefendant for merging - existingDefendant - ); - } - if (hearingIds.isEmpty()) { - // For deferred updates, store the merged defendant - defendantsToBeUpdated.clear(); // Clear old ones if needed, or handle per defendant - defendantsToBeUpdated.add(newDefendantEvent); return apply(Stream.of(DefendantsToBeUpdatedLater.defendantsToBeUpdatedLater() .withCaseId(caseId) - .withDefendants(singletonList(newDefendantEvent)) + .withDefendants(singletonList(NewDomainToEventConverter.buildDefendant(defendant))) .build())); } return apply(Stream.of(DefendantsToBeUpdated.defendantsToBeUpdated() .withCaseId(caseId) - .withDefendants(singletonList(newDefendantEvent)) + .withDefendants(singletonList(NewDomainToEventConverter.buildDefendant(defendant))) .withHearings(new ArrayList<>(hearingIds)) .build())); } - public Stream updateDefendantOffences(CaseOffences caseOffences) { if (hearingIds.isEmpty()) { return Stream.empty(); @@ -270,18 +249,14 @@ private void onHearingUpdatedToCase(HearingUpdatedToCase event) { this.hearingIds.add(event.getExistingHearingId()); } - // Update onDefendantsToBeUpdatedLater to store the defendant private void onDefendantsToBeUpdatedLater(final DefendantsToBeUpdatedLater event) { - event.getDefendants().forEach(defendant -> currentDefendants.put(defendant.getId(), defendant)); - this.defendantsToBeUpdated.addAll(event.getDefendants()); + this.defendantsToBeUpdated.add(event.getDefendants().get(0)); } - private void onDefendantsToBeUpdated(final DefendantsToBeUpdated event) { - event.getDefendants().forEach(defendant -> currentDefendants.put(defendant.getId(), defendant)); + private void onDefendantsToBeUpdated() { this.defendantsToBeUpdated.clear(); } - private void onOffencesToBeUpdated() { // Do nothing } diff --git a/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/Hearing.java b/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/Hearing.java index 2c7984aed..9086fbeef 100644 --- a/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/Hearing.java +++ b/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/Hearing.java @@ -171,7 +171,6 @@ import java.time.LocalDate; import java.time.LocalTime; -import java.time.Period; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; @@ -487,13 +486,9 @@ public Stream list(final UUID hearingId, final Type type, builder.withSpecialRequirements(specialRequirements); isPossibleDisqualification.ifPresent(builder::withIsPossibleDisqualification); - final Stream hearingListedEvents = apply(Stream.of(hearingListed() + return apply(Stream.of(hearingListed() .withHearing(builder.build()) .build())); - if (weekCommencingStartDate.isPresent()) { - return concat(hearingListedEvents, emitYouthCourtListRestrictions(weekCommencingStartDate.get())); - } - return hearingListedEvents; } else { LOGGER.error("Cannot list hearing with id {} as it has already been listed", hearingId); return Stream.empty(); @@ -601,7 +596,7 @@ public Stream listUnscheduled(final UUID hearingId, .withRoomId(courtRoomId).build(); - final Stream unscheduledHearingListedEvents = apply(Stream.of(hearingListed() + return apply(Stream.of(hearingListed() .withHearing(uk.gov.justice.listing.events.Hearing.hearing() .withId(hearingId) .withType(uk.gov.justice.listing.events.Type.type() @@ -639,10 +634,6 @@ public Stream listUnscheduled(final UUID hearingId, .withWeekCommencingEndDate(weekCommencingEndDate.orElse(null)) .build()) .build())); - if (isNull(startDate) && weekCommencingStartDate.isPresent()) { - return concat(unscheduledHearingListedEvents, emitYouthCourtListRestrictions(weekCommencingStartDate.get())); - } - return unscheduledHearingListedEvents; } @SuppressWarnings("squid:S3358") @@ -825,15 +816,10 @@ public Stream changeStartDate(final LocalDate startDate, final UUID hear } if (hasChanged(this.startDate, startDate)) { - final Stream startDateEvents = apply(Stream.of(startDateChangedForHearing() + return apply(Stream.of(startDateChangedForHearing() .withStartDate(startDate.toString()) .withHearingId(hearingId) .build())); - - if (canAllocate() || isAllocated()) { - return concat(startDateEvents, emitYouthCourtListRestrictions(startDate)); - } - return startDateEvents; } else { LOGGER.info("Incoming start date {} is the same as current start date {} for hearing with id {} - Ignore", startDate, this.startDate, hearingId); return Stream.empty(); @@ -847,15 +833,13 @@ public Stream changeWeekCommencingDate(final LocalDate weekCommencingSta if (hasChanged(this.weekCommencingStartDate, weekCommencingStartDate) || hasChanged(this.weekCommencingEndDate, weekCommencingEndDate)) { - final Stream weekCommencingEvents = apply(Stream.of(weekCommencingDateChangedForHearing() + return apply(Stream.of(weekCommencingDateChangedForHearing() .withWeekCommencingStartDate(weekCommencingStartDate.toString()) .withWeekCommencingEndDate(weekCommencingEndDate.toString()) .withWeekCommencingDurationInWeeks(weekCommencingDurationInWeeks) .withHearingId(hearingId) .build())); - return concat(weekCommencingEvents, emitYouthCourtListRestrictions(weekCommencingStartDate)); - } else { LOGGER.info("Incoming week commencing date {} is the same as current week commencing date {} for hearing with id {} - Ignore", this.weekCommencingStartDate, this.weekCommencingEndDate, hearingId); return Stream.empty(); @@ -1194,37 +1178,11 @@ public Stream updateDefendants(final UUID caseId, final List isCaseContainsDefendant(caseId, defendant.getId())) .map(defendant -> defendantDetailsUpdatedEvent(caseId, defendant)) .collect(toList()); - - final boolean dobChanged = hasDateOfBirthChanged(caseId, defendants); - final Stream updatedStream = apply(events.stream()); - - if (dobChanged) { - return concat(updatedStream, emitYouthCourtListRestrictions()); - } - return updatedStream; + return apply(events.stream()); } return Stream.empty(); } - private boolean hasDateOfBirthChanged(final UUID caseId, final List defendants) { - if (isNull(currentHearingEventState) || isNull(currentHearingEventState.getListedCases())) { - return false; - } - return currentHearingEventState.getListedCases().stream() - .filter(listedCase -> listedCase.getId().equals(caseId)) - .findFirst() - .map(listedCase -> defendants.stream() - .filter(defendant -> isCaseContainsDefendant(caseId, defendant.getId())) - .anyMatch(defendant -> listedCase.getDefendants().stream() - .filter(eventDefendant -> eventDefendant.getId().equals(defendant.getId())) - .findFirst() - .map(eventDefendant -> !Objects.equals( - eventDefendant.getDateOfBirth(), - defendant.getDateOfBirth().orElse(null))) - .orElse(false))) - .orElse(false); - } - public Stream updateCaseMarkers(final UUID caseId, final List caseMarkers) { if (this.duplicate || this.deleted) { @@ -1279,7 +1237,7 @@ public Stream hearingVacateTrial(final Optional vacatingTrialReaso .withVacatedTrialReasonId(vacatingTrialReasonId.orElse(null)) .build()); - if (MAGISTRATES == jurisdictionType && vacatingTrialReasonId.isPresent()) { + if ((MAGISTRATES == jurisdictionType || JurisdictionType.CROWN == jurisdictionType) && vacatingTrialReasonId.isPresent()) { eventsStream = concat(eventsStream, Stream.of(availableSlotsForHearingFreed().withHearingId(this.hearingId).build())); } return apply(eventsStream); @@ -1320,27 +1278,27 @@ public Stream addCasesToHearing(final List prosecutionC if (this.duplicate || this.deleted) { return Stream.empty(); } - final Stream casesAddedEvents = apply(Stream.of(CasesAddedToHearing.casesAddedToHearing() + return apply(Stream.of(CasesAddedToHearing.casesAddedToHearing() .withUnAllocatedListedCases(prosecutionCases.stream() .map(prosecutionCase -> buildListedCase(prosecutionCase, shadowListedOffences)) .collect(Collectors.toList())) .withHearingId(hearingId) .withSeedingHearingId(seedingHearingId.orElse(null)) .build())); - return concat(casesAddedEvents, emitYouthCourtListRestrictions()); + } public Stream addCasesToUnAllocatedHearing(final List listedCases, final UUID existingHearingId) { if (this.duplicate || this.deleted || (canAllocate() && isAllocated())) { return Stream.empty(); } - final Stream casesAddedEvents = apply(Stream.of(CasesAddedToHearing.casesAddedToHearing() + return apply(Stream.of(CasesAddedToHearing.casesAddedToHearing() .withUnAllocatedListedCases(listedCases) .withHearingId(hearingId) .withSeedingHearingId(existingHearingId) .withAddCasesToUnAllocatedHearing(true) .build())); - return concat(casesAddedEvents, emitYouthCourtListRestrictions()); + } public Stream deleteUnAllocatedHearing() { @@ -1476,7 +1434,7 @@ public Stream deleteHearing(final UUID seedingHearingId, final UUID hear .withCaseIds(caseIds) .build()); - if (MAGISTRATES == jurisdictionType) { + if (MAGISTRATES == jurisdictionType || JurisdictionType.CROWN == jurisdictionType) { eventStreamBuilder.add(availableSlotsForHearingFreed() .withHearingId(hearingId).build()); } @@ -1677,8 +1635,7 @@ public Stream addDefendantsForCourtProceedings(final UUID caseId, final .filter(defendant -> !isCaseContainsDefendant(caseId, defendant.getId())) .map(defendant -> defendantsAddedForCourtProceedings(caseId, defendant)) .collect(toList()); - final Stream addedEvents = apply(events.stream()); - return concat(addedEvents, emitYouthCourtListRestrictions()); + return apply(events.stream()); } return Stream.empty(); } @@ -1713,17 +1670,14 @@ public Stream restrictDetailsFromCourt(final UUID hearingId, final Restr } if (!isHearingInThePast()) { - final Set masterDefendantIds = getMasterDefendantIdsForDefendants(restrictCourtList.getDefendantIds()); - return apply(Stream.of(CourtListRestricted.courtListRestricted() .withHearingId(hearingId) .withCaseIds(restrictCourtList.getCaseIds()) .withDefendantIds(restrictCourtList.getDefendantIds()) .withOffenceIds(restrictCourtList.getOffenceIds()) - .withCourtApplicationApplicantIds(mergePartyIds(restrictCourtList.getCourtApplicationApplicantIds(), getApplicantIdsByMasterDefendantIds(masterDefendantIds))) + .withCourtApplicationApplicantIds(restrictCourtList.getCourtApplicationApplicantIds()) .withCourtApplicationIds(restrictCourtList.getCourtApplicationIds()) - .withCourtApplicationRespondentIds(mergePartyIds(restrictCourtList.getCourtApplicationRespondentIds(), getRespondentIdsByMasterDefendantIds(masterDefendantIds))) - .withCourtApplicationSubjectIds(mergePartyIds(restrictCourtList.getCourtApplicationSubjectIds(), getSubjectIdsByMasterDefendantIds(masterDefendantIds))) + .withCourtApplicationRespondentIds(restrictCourtList.getCourtApplicationRespondentIds()) .withRestrictCourtList(restrictCourtList.getRestrictFromCourtList()) .withCourtApplicationType(restrictCourtList.getCourtApplicationType().orElse(null)) .build())); @@ -1732,67 +1686,6 @@ public Stream restrictDetailsFromCourt(final UUID hearingId, final Restr return Stream.empty(); } - private Set getMasterDefendantIdsForDefendants(final List defendantIds) { - if (isNull(currentHearingEventState) || isNull(currentHearingEventState.getListedCases()) || isEmpty(defendantIds)) { - return new HashSet<>(); - } - return currentHearingEventState.getListedCases().stream() - .filter(listedCase -> nonNull(listedCase.getDefendants())) - .flatMap(listedCase -> listedCase.getDefendants().stream()) - .filter(defendant -> defendantIds.contains(defendant.getId())) - .map(uk.gov.justice.listing.events.Defendant::getMasterDefendantId) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - - private List getSubjectIdsByMasterDefendantIds(final Set masterDefendantIds) { - if (isNull(currentHearingEventState) || isNull(currentHearingEventState.getCourtApplications()) || isEmpty(masterDefendantIds)) { - return emptyList(); - } - return currentHearingEventState.getCourtApplications().stream() - .filter(courtApplication -> nonNull(courtApplication.getSubject())) - .map(uk.gov.justice.listing.events.CourtApplication::getSubject) - .filter(subject -> nonNull(subject.getMasterDefendantId()) && masterDefendantIds.contains(subject.getMasterDefendantId())) - .map(uk.gov.justice.listing.events.ApplicantRespondent::getId) - .collect(toList()); - } - - private List getApplicantIdsByMasterDefendantIds(final Set masterDefendantIds) { - if (isNull(currentHearingEventState) || isNull(currentHearingEventState.getCourtApplications()) || isEmpty(masterDefendantIds)) { - return emptyList(); - } - return currentHearingEventState.getCourtApplications().stream() - .filter(courtApplication -> nonNull(courtApplication.getApplicant())) - .map(uk.gov.justice.listing.events.CourtApplication::getApplicant) - .filter(applicant -> nonNull(applicant.getMasterDefendantId()) && masterDefendantIds.contains(applicant.getMasterDefendantId())) - .map(uk.gov.justice.listing.events.ApplicantRespondent::getId) - .collect(toList()); - } - - private List getRespondentIdsByMasterDefendantIds(final Set masterDefendantIds) { - if (isNull(currentHearingEventState) || isNull(currentHearingEventState.getCourtApplications()) || isEmpty(masterDefendantIds)) { - return emptyList(); - } - return currentHearingEventState.getCourtApplications().stream() - .filter(courtApplication -> nonNull(courtApplication.getRespondents())) - .flatMap(courtApplication -> courtApplication.getRespondents().stream()) - .filter(respondent -> nonNull(respondent.getMasterDefendantId()) && masterDefendantIds.contains(respondent.getMasterDefendantId())) - .map(uk.gov.justice.listing.events.ApplicantRespondent::getId) - .collect(toList()); - } - - private List mergePartyIds(final List existingIds, final List additionalIds) { - if (isEmpty(additionalIds)) { - return existingIds; - } - if (isEmpty(existingIds)) { - return additionalIds; - } - return Stream.concat(existingIds.stream(), additionalIds.stream()) - .distinct() - .collect(toList()); - } - public Stream updateDefendantLegalAidStatusForHearing(final UUID hearingId, final UUID caseId, final UUID defendantId, final String legalAidStatus) { if (this.duplicate || this.deleted || !isCaseContainsDefendant(caseId, defendantId)) { @@ -1819,7 +1712,8 @@ boolean magistrateHearingIsInTheFutureAndAllCaseAndApplicationAreEjected(final U } - if (!uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES.equals(this.currentHearingEventState.getJurisdictionType())) { + if (!(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES.equals(this.currentHearingEventState.getJurisdictionType()) + || uk.gov.justice.core.courts.JurisdictionType.CROWN.equals(this.currentHearingEventState.getJurisdictionType()))) { return false; } @@ -2029,8 +1923,7 @@ private Stream onAllocationEvents(final Optional bookingReference, if (isAllocated()) { return apply(Stream.of(allocatedHearingUpdatedForListingEvent(source, sendNotificationToParties, isNotificationRelatedAllocatedFieldsUpdated))); } - final Stream allocationEvents = apply(Stream.of(Stream.of(hearingAllocatedForListingEvent(bookingReference, prosecutionCaseDefendantOffenceIds, source, sendNotificationToParties, isNotificationRelatedAllocatedFieldsUpdated, isGroupProceedings))).flatMap(i -> i)); - return concat(allocationEvents, emitYouthCourtListRestrictions()); + return apply(Stream.of(Stream.of(hearingAllocatedForListingEvent(bookingReference, prosecutionCaseDefendantOffenceIds, source, sendNotificationToParties, isNotificationRelatedAllocatedFieldsUpdated, isGroupProceedings))).flatMap(i -> i)); } public boolean isDuplicateOrDeleted() { @@ -2061,102 +1954,24 @@ private Stream onUnallocationBusinessRules() { return Stream.empty(); } - public Stream emitYouthCourtListRestrictions() { - if (canAllocate() || isAllocated()) { - return emitYouthCourtListRestrictions(getEffectiveHearingDate()); - } - return Stream.empty(); - } - - private Stream emitYouthCourtListRestrictions(final LocalDate effectiveHearingDate) { - if (isNull(effectiveHearingDate) || isNull(this.currentHearingEventState)) { - return Stream.empty(); - } - - final List under18DefendantIds = getUnder18DefendantIds(effectiveHearingDate); - final List under18SubjectIds = getUnder18CourtApplicationSubjectIds(effectiveHearingDate); - final List under18RespondentIds = getUnder18CourtApplicationRespondentIds(effectiveHearingDate); - - if (under18DefendantIds.isEmpty() && under18SubjectIds.isEmpty() && under18RespondentIds.isEmpty()) { - return Stream.empty(); - } - - LOGGER.info("Auto-restricting under-18 parties from court list for hearing {}: {} defendant(s), {} subject(s), {} respondent(s)", - this.hearingId, under18DefendantIds.size(), under18SubjectIds.size(), under18RespondentIds.size()); - - return apply(Stream.of(CourtListRestricted.courtListRestricted() - .withHearingId(this.hearingId) - .withRestrictCourtList(true) - .withDefendantIds(under18DefendantIds.isEmpty() ? null : under18DefendantIds) - .withCourtApplicationSubjectIds(under18SubjectIds.isEmpty() ? null : under18SubjectIds) - .withCourtApplicationRespondentIds(under18RespondentIds.isEmpty() ? null : under18RespondentIds) - .build())); - } - - private List getUnder18DefendantIds(final LocalDate effectiveHearingDate) { - if (isNull(this.currentHearingEventState.getListedCases())) { - return emptyList(); - } - return this.currentHearingEventState.getListedCases().stream() - .filter(listedCase -> nonNull(listedCase.getDefendants())) - .flatMap(listedCase -> listedCase.getDefendants().stream()) - .filter(defendant -> isUnder18OnHearingDate(defendant.getDateOfBirth(), effectiveHearingDate)) - .filter(defendant -> !toBoolean(defendant.getRestrictFromCourtList())) - .map(uk.gov.justice.listing.events.Defendant::getId) - .collect(toList()); - } - - private List getUnder18CourtApplicationSubjectIds(final LocalDate effectiveHearingDate) { - if (isNull(this.currentHearingEventState.getCourtApplications())) { - return emptyList(); - } - return this.currentHearingEventState.getCourtApplications().stream() - .filter(courtApplication -> nonNull(courtApplication.getSubject())) - .map(uk.gov.justice.listing.events.CourtApplication::getSubject) - .filter(subject -> nonNull(subject.getId())) - .filter(subject -> isUnder18OnHearingDate(subject.getDateOfBirth(), effectiveHearingDate)) - .filter(subject -> !toBoolean(subject.getRestrictFromCourtList())) - .map(uk.gov.justice.listing.events.ApplicantRespondent::getId) - .collect(toList()); - } - - private List getUnder18CourtApplicationRespondentIds(final LocalDate effectiveHearingDate) { - if (isNull(this.currentHearingEventState.getCourtApplications())) { - return emptyList(); - } - return this.currentHearingEventState.getCourtApplications().stream() - .filter(courtApplication -> nonNull(courtApplication.getRespondents())) - .flatMap(courtApplication -> courtApplication.getRespondents().stream()) - .filter(respondent -> nonNull(respondent.getId())) - .filter(respondent -> isUnder18OnHearingDate(respondent.getDateOfBirth(), effectiveHearingDate)) - .filter(respondent -> !toBoolean(respondent.getRestrictFromCourtList())) - .map(uk.gov.justice.listing.events.ApplicantRespondent::getId) - .collect(toList()); - } - - private LocalDate getEffectiveHearingDate() { - return nonNull(this.startDate) ? this.startDate : this.weekCommencingStartDate; - } - - private boolean isUnder18OnHearingDate(final String dateOfBirth, final LocalDate hearingDate) { - if (isBlank(dateOfBirth) || isNull(hearingDate)) { - return false; - } - try { - return Period.between(LocalDate.parse(dateOfBirth), hearingDate).getYears() < 18; - } catch (final Exception e) { - LOGGER.warn("Unable to parse date of birth '{}' for youth check", dateOfBirth, e); - return false; - } - } - private boolean canAllocate() { return canAllocateForMags() || canAllocateForCrown(); } private boolean canAllocateForCrown() { - return currentlyAssigned(this.hearingLanguage) && currentlyAssigned(this.jurisdictionType) && JurisdictionType.CROWN.equals(this.jurisdictionType) + final boolean basicCriteria = currentlyAssigned(this.hearingLanguage) && currentlyAssigned(this.jurisdictionType) && JurisdictionType.CROWN.equals(this.jurisdictionType) && currentlyAssigned(this.courtRoomId) && currentlyAssigned(this.endDate) && currentlyAssigned(this.startDate); + if (!basicCriteria) { + return false; + } + // Crown hearings require courtScheduleIds on all hearingDays, and none can be draft + return hasCourtScheduleIds(this.hearingDays) && noneHasDraftSession(this.hearingDays); + } + + private boolean noneHasDraftSession(final List hearingDays) { + return isNotEmpty(hearingDays) && + hearingDays.stream() + .noneMatch(HearingDay::isDraft); } private boolean canAllocateForMags() { @@ -2559,6 +2374,7 @@ private List mergeHearingDaySequences( .withCourtScheduleId(cd.getCourtScheduleId()) .withCourtRoomId(cd.getCourtRoomId()) .withCourtCentreId(cd.getCourtCentreId()) + .withIsDraft(cd.getIsDraft()) .build()) .collect(toList()); } @@ -2577,6 +2393,7 @@ private List mergePreviouslyChangedCou .withCourtScheduleId(cd.getCourtScheduleId()) .withCourtRoomId(existingHearingDay !=null? existingHearingDay.getCourtRoomId(): cd.getCourtRoomId()) .withCourtCentreId(existingHearingDay !=null? existingHearingDay.getCourtCentreId(): cd.getCourtCentreId()) + .withIsDraft(cd.getIsDraft()) .build(); }) .collect(toList()); @@ -2746,6 +2563,7 @@ private void onSequencesResetOnHearingDays(final SequencesResetOnHearingDays eve .withCourtScheduleId(cd.getCourtScheduleId()) .withCourtRoomId(cd.getCourtRoomId()) .withCourtCentreId(cd.getCourtCentreId()) + .withIsDraft(cd.isDraft()) .build()) .collect(toList()); @@ -3021,6 +2839,7 @@ private List convertHearingDaysToDomain(final List convertHearingDaysToEvent .withIsCancelled(cd.isCancelled()) .withCourtCentreId(cd.getCourtCentreId()) .withCourtRoomId(cd.getCourtRoomId()) + .withIsDraft(cd.isDraft()) .build()) .collect(toList()); } @@ -3501,7 +3322,7 @@ private Stream getOffencesRemovedFromHearingStream(final UUID seedingHea .build()); } - if (isAllocated() && MAGISTRATES == jurisdictionType) { + if (isAllocated() && (MAGISTRATES == jurisdictionType || JurisdictionType.CROWN == jurisdictionType)) { eventStreamBuilder.add(availableSlotsForHearingFreed() .withHearingId(hearingId).build()); } @@ -3599,9 +3420,10 @@ private Predicate matchingHearingDay(f } private HearingDay buildHearingDayWithCancelledStatus(final HearingDay hearingDayInAggregate, final boolean cancelled) { - return new HearingDay(hearingDayInAggregate.getDurationMinutes(), hearingDayInAggregate.getEndTime(), hearingDayInAggregate.getHearingDate(), - hearingDayInAggregate.getSequence(), hearingDayInAggregate.getStartTime(), hearingDayInAggregate.getCourtScheduleId(), cancelled, - hearingDayInAggregate.getCourtCentreId(), hearingDayInAggregate.getCourtRoomId()); + return HearingDay.hearingDay() + .withValuesFrom(hearingDayInAggregate) + .withIsCancelled(cancelled) + .build(); } private ZonedDateTime getEarliestStartDate() { @@ -3868,19 +3690,6 @@ void onCourtApplicationAddedForHearing(final uk.gov.justice.listing.events.Court if (app != null && !confirmedCourtApplicationIds.contains(app.getId())) { confirmedCourtApplicationIds.add(app.getId()); } - if (nonNull(this.currentHearingEventState) && nonNull(app)) { - if (isNull(this.currentHearingEventState.getCourtApplications())) { - this.currentHearingEventState = uk.gov.justice.listing.events.Hearing.hearing() - .withValuesFrom(currentHearingEventState) - .withCourtApplications(new ArrayList<>()) - .build(); - } - final List existingApps = - this.currentHearingEventState.getCourtApplications(); - if (existingApps.stream().noneMatch(ca -> app.getId().equals(ca.getId()))) { - existingApps.add(app); - } - } } } diff --git a/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/HearingDay.java b/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/HearingDay.java index ccc25abb8..3307a987f 100644 --- a/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/HearingDay.java +++ b/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/HearingDay.java @@ -31,7 +31,9 @@ public class HearingDay implements Serializable { private final UUID courtRoomId; - public HearingDay(final Integer durationMinutes, final ZonedDateTime endTime, final LocalDate hearingDate, final Integer sequence, final ZonedDateTime startTime, final UUID courtScheduleId, final Boolean isCancelled, final UUID courtCentreId, final UUID courtRoomId) { + private final Boolean isDraft; + + public HearingDay(final Integer durationMinutes, final ZonedDateTime endTime, final LocalDate hearingDate, final Integer sequence, final ZonedDateTime startTime, final UUID courtScheduleId, final Boolean isCancelled, final UUID courtCentreId, final UUID courtRoomId, final Boolean isDraft) { this.durationMinutes = durationMinutes; this.endTime = endTime; this.hearingDate = hearingDate; @@ -41,6 +43,7 @@ public HearingDay(final Integer durationMinutes, final ZonedDateTime endTime, fi this.isCancelled = isCancelled; this.courtCentreId = courtCentreId; this.courtRoomId = courtRoomId; + this.isDraft = isDraft; } public Integer getDurationMinutes() { @@ -79,6 +82,10 @@ public UUID getCourtRoomId() { return courtRoomId; } + public Boolean isDraft() { + return isNull(isDraft) ? FALSE : isDraft; + } + public static Builder hearingDay() { return new Builder(); } @@ -100,12 +107,13 @@ public boolean equals(final Object o) { Objects.equals(getStartTime(), that.getStartTime()) && Objects.equals(isCancelled(), that.isCancelled()) && Objects.equals(getCourtCentreId(), that.getCourtCentreId()) && - Objects.equals(getCourtRoomId(), that.getCourtRoomId()); + Objects.equals(getCourtRoomId(), that.getCourtRoomId()) && + Objects.equals(isDraft(), that.isDraft()); } @Override public int hashCode() { - return Objects.hash(getCourtScheduleId(), getDurationMinutes(), getEndTime(), getHearingDate(), getSequence(), getStartTime(), isCancelled(), getCourtCentreId(), getCourtRoomId()); + return Objects.hash(getCourtScheduleId(), getDurationMinutes(), getEndTime(), getHearingDate(), getSequence(), getStartTime(), isCancelled(), getCourtCentreId(), getCourtRoomId(), isDraft()); } @Override @@ -120,6 +128,7 @@ public String toString() { ", isCancelled=" + isCancelled + ", courtCentreId=" + courtCentreId + ", courtRoomId=" + courtRoomId + + ", isDraft=" + isDraft + '}'; } @@ -144,6 +153,8 @@ public static class Builder { private UUID courtRoomId; + private Boolean isDraft; + public Builder withDurationMinutes(final Integer durationMinutes) { this.durationMinutes = durationMinutes; return this; @@ -189,6 +200,11 @@ public Builder withCourtRoomId(final UUID courtRoomId) { return this; } + public Builder withIsDraft(final Boolean isDraft) { + this.isDraft = isDraft; + return this; + } + public HearingDay.Builder withValuesFrom(HearingDay hearingDay) { this.courtCentreId = hearingDay.getCourtCentreId(); this.courtRoomId = hearingDay.getCourtRoomId(); @@ -199,12 +215,13 @@ public HearingDay.Builder withValuesFrom(HearingDay hearingDay) { this.isCancelled = hearingDay.isCancelled(); this.sequence = hearingDay.getSequence(); this.startTime = hearingDay.getStartTime(); + this.isDraft = hearingDay.isDraft(); return this; } public HearingDay build() { - return new HearingDay(durationMinutes, endTime, hearingDate, sequence, startTime, courtScheduleId, isCancelled, courtCentreId, courtRoomId); + return new HearingDay(durationMinutes, endTime, hearingDate, sequence, startTime, courtScheduleId, isCancelled, courtCentreId, courtRoomId, isDraft); } } } diff --git a/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/NewDomainToEventConverter.java b/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/NewDomainToEventConverter.java index 5e1d5d2a9..c8ecb1d11 100644 --- a/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/NewDomainToEventConverter.java +++ b/listing-domain/listing-domain-aggregate/src/main/java/uk/gov/moj/cpp/listing/domain/aggregate/NewDomainToEventConverter.java @@ -168,61 +168,24 @@ public static NewBaseDefendant buildNewBaseDefendant(final uk.gov.moj.cpp.listin .build(); } - public static Defendant updateEventDefendant(final NewBaseDefendant newDefendant, final Defendant defendant) { - final Defendant.Builder builder = Defendant.defendant().withValuesFrom(defendant); - applyDefendantUpdatesFromNewBase(newDefendant, builder); - applyIsYouthUpdate(defendant, newDefendant, builder); - return builder.build(); - } - - private static void applyDefendantUpdatesFromNewBase(final NewBaseDefendant newDefendant, final Defendant.Builder builder) { - if (nonNull(newDefendant.getFirstName())) { - builder.withFirstName(newDefendant.getFirstName()); - } - if (nonNull(newDefendant.getLastName())) { - builder.withLastName(newDefendant.getLastName()); - } - if (nonNull(newDefendant.getOrganisationName())) { - builder.withOrganisationName(newDefendant.getOrganisationName()); - } - if (nonNull(newDefendant.getBailStatus())) { - builder.withBailStatus(newDefendant.getBailStatus()); - } - if (nonNull(newDefendant.getCustodyTimeLimit())) { - builder.withCustodyTimeLimit(newDefendant.getCustodyTimeLimit()); - } - if (nonNull(newDefendant.getId())) { - builder.withId(newDefendant.getId()); - } - if (nonNull(newDefendant.getDateOfBirth())) { - builder.withDateOfBirth(newDefendant.getDateOfBirth()); - } - if (nonNull(newDefendant.getDefenceOrganisation())) { - builder.withDefenceOrganisation(newDefendant.getDefenceOrganisation()); - } - if (nonNull(newDefendant.getSpecificRequirements())) { - builder.withSpecificRequirements(newDefendant.getSpecificRequirements()); - } - if (nonNull(newDefendant.getAddress())) { - builder.withAddress(newDefendant.getAddress()); - } - if (nonNull(newDefendant.getNationalityDescription())) { - builder.withNationalityDescription(newDefendant.getNationalityDescription()); - } - if (nonNull(newDefendant.getMasterDefendantId())) { - builder.withMasterDefendantId(newDefendant.getMasterDefendantId()); - } - } + public static Defendant updateEventDefendant(NewBaseDefendant newDefendant, Defendant defendant){ + return uk.gov.justice.listing.events.Defendant.defendant() + .withValuesFrom(defendant) + .withFirstName(newDefendant.getFirstName()) + .withLastName(newDefendant.getLastName()) + .withOrganisationName(newDefendant.getOrganisationName()) + .withBailStatus(newDefendant.getBailStatus()) + .withCustodyTimeLimit(newDefendant.getCustodyTimeLimit()) + .withId(newDefendant.getId()) + .withDateOfBirth(newDefendant.getDateOfBirth()) + .withDefenceOrganisation(newDefendant.getDefenceOrganisation()) + .withSpecificRequirements(newDefendant.getSpecificRequirements()) + .withIsYouth(newDefendant.getIsYouth()) + .withAddress(newDefendant.getAddress()) + .withNationalityDescription(newDefendant.getNationalityDescription()) + .withMasterDefendantId(newDefendant.getMasterDefendantId()) + .build(); - /** - * Retain {@code isYouth} as true if it was ever true on the existing or incoming defendant. - */ - private static void applyIsYouthUpdate(final Defendant defendant, final NewBaseDefendant newDefendant, final Defendant.Builder builder) { - if (Boolean.TRUE.equals(defendant.getIsYouth()) || Boolean.TRUE.equals(newDefendant.getIsYouth())) { - builder.withIsYouth(true); - } else if (nonNull(newDefendant.getIsYouth())) { - builder.withIsYouth(newDefendant.getIsYouth()); - } } public static List buildSimpleOffences(final List offences) { @@ -381,8 +344,6 @@ private static uk.gov.justice.listing.events.ApplicantRespondent buildCourtAppli .withRestrictFromCourtList(false) .withCourtApplicationPartyType(buildCourtApplicationPartyTypeEvent(applicant.getCourtApplicationPartyType())) .withAddress(NewDomainToEventConverter.buildAddress(applicant.getAddress())) - .withMasterDefendantId(applicant.getMasterDefendantId().orElse(null)) - .withDateOfBirth(applicant.getDateOfBirth().orElse(null)) .build(); } return null; @@ -456,6 +417,7 @@ public static List convertHearingDaysD .withCourtScheduleId(hearingDay.getCourtScheduleId().orElse(null)) .withCourtRoomId(hearingDay.getCourtRoomId().orElse(null)) .withCourtCentreId(hearingDay.getCourtCentreId().orElse(null)) + .withIsDraft(hearingDay.getIsDraft().orElse(null)) .build()) .toList(); diff --git a/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/CaseAggregateTest.java b/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/CaseAggregateTest.java index ebd507762..2c3bc606b 100644 --- a/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/CaseAggregateTest.java +++ b/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/CaseAggregateTest.java @@ -1,11 +1,8 @@ package uk.gov.moj.cpp.listing.domain.aggregate; -import static java.util.Optional.of; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import uk.gov.justice.listing.events.DefendantsToBeUpdated; import uk.gov.justice.listing.events.DefendantsToBeUpdatedLater; @@ -60,160 +57,6 @@ void shouldAddUpdateDefendant() { assertThat(events.get(0), instanceOf(DefendantsToBeUpdated.class)); } - @Test - void shouldMergeDefendantFieldsWhenUpdatingExistingDefendantWithHearing() { - final Case caseAggregate = new Case(); - final UUID caseId = UUID.randomUUID(); - final UUID hearingId = UUID.randomUUID(); - final UUID defendantId = UUID.randomUUID(); - final UUID masterDefendantId = UUID.randomUUID(); - - caseAggregate.addHearing(caseId, hearingId); - - caseAggregate.updateDefendant(caseId, Defendant.defendant() - .withId(defendantId) - .withFirstName(of("John")) - .withLastName(of("Doe")) - .withIsYouth(of(true)) - .withMasterDefendantId(of(masterDefendantId)) - .build()); - - final List events = caseAggregate.updateDefendant(caseId, Defendant.defendant() - .withId(defendantId) - .withLastName(of("Smith")) - .build()).toList(); - - assertThat(events.size(), is(1)); - assertThat(events.get(0), instanceOf(DefendantsToBeUpdated.class)); - - final uk.gov.justice.listing.events.Defendant mergedDefendant = - ((DefendantsToBeUpdated) events.get(0)).getDefendants().get(0); - - assertEquals(defendantId, mergedDefendant.getId()); - assertEquals("John", mergedDefendant.getFirstName()); - assertEquals("Smith", mergedDefendant.getLastName()); - assertTrue(Boolean.TRUE.equals(mergedDefendant.getIsYouth())); - assertEquals(masterDefendantId, mergedDefendant.getMasterDefendantId()); - assertEquals(List.of(hearingId), ((DefendantsToBeUpdated) events.get(0)).getHearings()); - } - - @Test - void shouldMergeDefendantFieldsWhenUpdatingExistingDefendantWithoutHearing() { - final Case caseAggregate = new Case(); - final UUID caseId = UUID.randomUUID(); - final UUID defendantId = UUID.randomUUID(); - - caseAggregate.updateDefendant(caseId, Defendant.defendant() - .withId(defendantId) - .withFirstName(of("Jane")) - .withLastName(of("Doe")) - .withIsYouth(of(true)) - .build()); - - final List events = caseAggregate.updateDefendant(caseId, Defendant.defendant() - .withId(defendantId) - .withLastName(of("Smith")) - .build()).toList(); - - assertThat(events.size(), is(1)); - assertThat(events.get(0), instanceOf(DefendantsToBeUpdatedLater.class)); - - final uk.gov.justice.listing.events.Defendant mergedDefendant = - ((DefendantsToBeUpdatedLater) events.get(0)).getDefendants().get(0); - - assertEquals("Jane", mergedDefendant.getFirstName()); - assertEquals("Smith", mergedDefendant.getLastName()); - assertTrue(Boolean.TRUE.equals(mergedDefendant.getIsYouth())); - } - - @Test - void shouldRetainIsYouthWhenPartialUpdateDoesNotProvideIsYouth() { - final Case caseAggregate = new Case(); - final UUID caseId = UUID.randomUUID(); - final UUID hearingId = UUID.randomUUID(); - final UUID defendantId = UUID.randomUUID(); - - caseAggregate.addHearing(caseId, hearingId); - - caseAggregate.updateDefendant(caseId, Defendant.defendant() - .withId(defendantId) - .withFirstName(of("Youth")) - .withIsYouth(of(true)) - .build()); - - final uk.gov.justice.listing.events.Defendant mergedDefendant = extractDefendantFromUpdate( - caseAggregate.updateDefendant(caseId, Defendant.defendant() - .withId(defendantId) - .withFirstName(of("Updated")) - .build())); - - assertEquals("Updated", mergedDefendant.getFirstName()); - assertTrue(Boolean.TRUE.equals(mergedDefendant.getIsYouth())); - } - - @Test - void shouldReplayMergedDefendantWhenHearingAddedAfterDeferredUpdate() { - final Case caseAggregate = new Case(); - final UUID caseId = UUID.randomUUID(); - final UUID hearingId = UUID.randomUUID(); - final UUID defendantId = UUID.randomUUID(); - - caseAggregate.updateDefendant(caseId, Defendant.defendant() - .withId(defendantId) - .withFirstName(of("Deferred")) - .withLastName(of("Original")) - .build()); - - caseAggregate.updateDefendant(caseId, Defendant.defendant() - .withId(defendantId) - .withLastName(of("Merged")) - .build()); - - final List events = caseAggregate.addHearing(caseId, hearingId).toList(); - - assertThat(events.size(), is(2)); - assertThat(events.get(0), instanceOf(HearingAddedToCase.class)); - assertThat(events.get(1), instanceOf(DefendantsToBeUpdated.class)); - - final uk.gov.justice.listing.events.Defendant replayedDefendant = - ((DefendantsToBeUpdated) events.get(1)).getDefendants().get(0); - - assertEquals("Deferred", replayedDefendant.getFirstName()); - assertEquals("Merged", replayedDefendant.getLastName()); - } - - @Test - void shouldUseFullDefendantOnFirstUpdateWithoutMerge() { - final Case caseAggregate = new Case(); - final UUID caseId = UUID.randomUUID(); - final UUID hearingId = UUID.randomUUID(); - final UUID defendantId = UUID.randomUUID(); - - caseAggregate.addHearing(caseId, hearingId); - - final uk.gov.justice.listing.events.Defendant eventDefendant = extractDefendantFromUpdate( - caseAggregate.updateDefendant(caseId, Defendant.defendant() - .withId(defendantId) - .withFirstName(of("First")) - .withLastName(of("Update")) - .build())); - - assertEquals(defendantId, eventDefendant.getId()); - assertEquals("First", eventDefendant.getFirstName()); - assertEquals("Update", eventDefendant.getLastName()); - } - - private static uk.gov.justice.listing.events.Defendant extractDefendantFromUpdate(final Stream eventStream) { - final Object event = eventStream.toList().get(0); - if (event instanceof DefendantsToBeUpdated defendantsToBeUpdated) { - return defendantsToBeUpdated.getDefendants().get(0); - } - if (event instanceof DefendantsToBeUpdatedLater defendantsToBeUpdatedLater) { - return defendantsToBeUpdatedLater.getDefendants().get(0); - } - throw new AssertionError("Unexpected event type: " + event.getClass()); - } - @Test public void shouldNotAddAlreadyAddedHearing() { Case caseAggregate = new Case(); diff --git a/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/HearingAggregateTest.java b/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/HearingAggregateTest.java index 7713c64fd..96071b5aa 100644 --- a/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/HearingAggregateTest.java +++ b/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/HearingAggregateTest.java @@ -38,7 +38,6 @@ import uk.gov.justice.listing.events.CaseIdentifierUpdated; import uk.gov.justice.listing.events.CasesAddedToHearing; import uk.gov.justice.listing.events.CourtApplicationAddedForHearing; -import uk.gov.justice.listing.events.CourtListRestricted; import uk.gov.justice.listing.events.CourtRoomRemovedFromHearing; import uk.gov.justice.listing.events.Defendant; import uk.gov.justice.listing.events.DefendantCourtProceedingsUpdatedV2; @@ -60,7 +59,6 @@ import uk.gov.justice.listing.events.JudiciaryChangedForHearingsStatus; import uk.gov.justice.listing.events.Marker; import uk.gov.justice.listing.events.NewDefendantAddedForCourtProceedings; -import uk.gov.justice.listing.events.NewDefendantDetailsUpdated; import uk.gov.justice.listing.events.Offence; import uk.gov.justice.listing.events.OffenceAdded; import uk.gov.justice.listing.events.OffenceDeleted; @@ -72,10 +70,8 @@ import uk.gov.justice.listing.events.ProsecutionCaseDefendantOffenceIdsV2; import uk.gov.justice.listing.events.SeedingHearing; import uk.gov.justice.listing.events.SequencesResetOnHearingDays; -import uk.gov.justice.listing.events.StartDateChangedForHearing; import uk.gov.justice.listing.events.StatementOfOffence; import uk.gov.justice.listing.events.UnallocatedHearingDeleted; -import uk.gov.justice.listing.events.WeekCommencingDateChangedForHearing; import uk.gov.justice.services.common.converter.JsonObjectToObjectConverter; import uk.gov.moj.cpp.listing.domain.CourtApplication; import uk.gov.moj.cpp.listing.domain.CourtApplicationPartyListingNeeds; @@ -740,7 +736,7 @@ public void shouldNotRaiseAnyEventsAsHearingIsResulted() { } @Test - public void shouldCreateAllocatedHearingDeletedWithoutFreeingSlotsOfNonMagistratesJurisdiction() { + void shouldCreateAllocatedHearingDeletedAndFreeSlotsForCrownJurisdiction() { final UUID case1Id = randomUUID(); final UUID case2Id = randomUUID(); final UUID defendant1Id = randomUUID(); @@ -791,13 +787,14 @@ public void shouldCreateAllocatedHearingDeletedWithoutFreeingSlotsOfNonMagistrat final Stream events = hearing.deleteHearing(seedingHearingId, hearingId); final List eventsList = events.collect(Collectors.toList()); - assertThat(eventsList.size(), is(1)); + assertThat(eventsList.size(), is(2)); final AllocatedHearingDeleted unallocatedHearingDeleted = (AllocatedHearingDeleted) eventsList.get(0); assertThat(unallocatedHearingDeleted.getHearingId(), is(hearingId)); assertThat(unallocatedHearingDeleted.getCaseIds().size(), is(2)); assertThat(unallocatedHearingDeleted.getCaseIds(), hasItems(case1Id, case2Id)); + assertTrue(eventsList.get(1) instanceof AvailableSlotsForHearingFreed); } @Test @@ -964,7 +961,7 @@ public void shouldRemoveAllocatedNextHearingWhenSeedingHearingAmendedAndNextHear final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final AllocatedHearingDeleted allocatedHearingDeleted = (AllocatedHearingDeleted) deleteHearingEventsList.get(0); @@ -991,7 +988,7 @@ public void shouldRemoveOffencesFromNextHearingWhenSeedingHearingAmendedAndNextH final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final OffencesRemovedFromHearing offencesRemovedFromHearing = (OffencesRemovedFromHearing) deleteHearingEventsList.get(0); @@ -1026,7 +1023,7 @@ case2Id, asList(Map.of(defendant2Id, asList(Map.of(offence3Id, Optional.of(seedi final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final OffencesRemovedFromHearing offencesRemovedFromHearing = (OffencesRemovedFromHearing) deleteHearingEventsList.get(0); @@ -1579,7 +1576,7 @@ void shouldBeAbleToEjectHearingAndAvailableSlotsForHearingFreed_IfHearingIsListe } @Test - void shouldBeAbleToEjectApplicationAndNoSlotsForHearingFreedForCrown() { + void shouldBeAbleToEjectApplicationAndAvailableSlotsForHearingFreedForCrown() { final UUID applicationId = randomUUID(); final UUID hearingId = randomUUID(); @@ -1593,6 +1590,7 @@ void shouldBeAbleToEjectApplicationAndNoSlotsForHearingFreedForCrown() { .withJurisdictionType(CROWN) .withHearingDays(emptyList()) .withCourtRoomId(randomUUID()) + .withAllocated(true) .withStartDate(LocalDate.now().plusDays(1)) .withEstimatedMinutes(30) .withEstimatedDuration("30 minutes") @@ -1608,13 +1606,57 @@ void shouldBeAbleToEjectApplicationAndNoSlotsForHearingFreedForCrown() { var listedHearing = hearing.ejectApplication(hearingId, applicationId, removalReason).toList(); - assertThat(listedHearing, hasSize(1)); + assertThat(listedHearing, hasSize(2)); - var applicationEjected = (ApplicationEjected)listedHearing.get(0); + var availableSlotsForHearingFreed = (AvailableSlotsForHearingFreed)listedHearing.get(0); + var applicationEjected = (ApplicationEjected)listedHearing.get(1); + assertThat(availableSlotsForHearingFreed.getHearingId(), is(hearingId)); assertThat(applicationEjected.getHearingId(), is(hearingId)); } + @Test + void shouldBeAbleToEjectCaseAndAvailableSlotsForHearingFreedForCrown() { + + final UUID caseId = randomUUID(); + final UUID hearingId = randomUUID(); + final String removalReason = "removal reason"; + + hearing.apply(HearingListed.hearingListed() + .withHearing(uk.gov.justice.listing.events.Hearing.hearing() + .withId(hearingId) + .withType(uk.gov.justice.listing.events.Type.type().build()) + .withHearingLanguage(HearingLanguage.ENGLISH) + .withJurisdictionType(CROWN) + .withHearingDays(emptyList()) + .withCourtRoomId(randomUUID()) + .withAllocated(true) + .withStartDate(LocalDate.now().plusDays(1)) + .withEstimatedMinutes(30) + .withEstimatedDuration("30 minutes") + .withListedCases(new ArrayList<>( + asList(uk.gov.justice.listing.events.ListedCase.listedCase() + .withId(caseId) + .withDefendants(emptyList()) + .withIsEjected(true) + .build() + ) + )) + .build()) + .build() + ); + + var listedHearing = hearing.ejectCase(hearingId, caseId, removalReason).toList(); + + assertThat(listedHearing, hasSize(2)); + + var availableSlotsForHearingFreed = (AvailableSlotsForHearingFreed) listedHearing.get(0); + var caseEjected = (CaseEjected) listedHearing.get(1); + + assertThat(availableSlotsForHearingFreed.getHearingId(), is(hearingId)); + assertThat(caseEjected.getHearingId(), is(hearingId)); + } + @Test void shouldBeAbleToEjectApplicationAndNoSlotsForHearingFreedForHearingThatHasAlreadyStarted() { @@ -4382,148 +4424,6 @@ public void shouldAddDefendantsForCourtProceedings() { assertThat(events.get(0).getDefendant().getId(), is(defendantId2)); } - @Test - void shouldEmitCourtListRestrictedForUnder18DefendantAddedViaCourtProceedings() { - final UUID caseId = randomUUID(); - final UUID existingDefendantId = randomUUID(); - final UUID existingOffenceId = randomUUID(); - final UUID under18DefendantId = randomUUID(); - final UUID under18MasterDefendantId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - startDate = ZonedDateTime.of(now(), defaultStartTime, UTC).plusDays(30); - final String under18Dob = hearingStartDate.minusYears(16).toString(); - - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).withStartTime(startDate).build())) - .withCourtRoomId(randomUUID()) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(new ArrayList<>(Arrays.asList(Defendant.defendant() - .withId(existingDefendantId) - .withOffences(new ArrayList<>(Arrays.asList(Offence.offence().withId(existingOffenceId).build()))) - .build()))) - .build())) - .build()) - .build()); - - hearing.apply(HearingAllocatedForListing.hearingAllocatedForListing() - .withHearingId(hearingId) - .withProsecutionCaseDefendantsOffenceIds(Arrays.asList(ProsecutionCaseDefendantOffenceIds.prosecutionCaseDefendantOffenceIds() - .withId(caseId) - .withDefendants(Arrays.asList(DefendantOffenceIds.defendantOffenceIds() - .withId(existingDefendantId) - .withOffenceIds(Arrays.asList(existingOffenceId)) - .build())) - .build())) - .build()); - - final uk.gov.moj.cpp.listing.domain.Defendant under18Defendant = uk.gov.moj.cpp.listing.domain.Defendant.defendant() - .withId(under18DefendantId) - .withMasterDefendantId(Optional.of(under18MasterDefendantId)) - .withDateOfBirth(Optional.of(under18Dob)) - .withOffences(emptyList()) - .build(); - - final List resultEvents = hearing.addDefendantsForCourtProceedings(caseId, asList(under18Defendant)) - .collect(Collectors.toList()); - - assertThat(resultEvents, hasSize(2)); - assertThat(resultEvents.get(0), CoreMatchers.instanceOf(NewDefendantAddedForCourtProceedings.class)); - assertThat(resultEvents.get(1), CoreMatchers.instanceOf(CourtListRestricted.class)); - - final CourtListRestricted courtListRestricted = (CourtListRestricted) resultEvents.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getDefendantIds(), hasSize(1)); - assertThat(courtListRestricted.getDefendantIds(), hasItem(under18DefendantId)); - } - - @Test - void shouldEmitCourtListRestrictedForUnder18DefendantWhenCasesAddedToAllocatedHearing() { - final UUID caseId = randomUUID(); - final UUID existingDefendantId = randomUUID(); - final UUID existingOffenceId = randomUUID(); - final UUID under18DefendantId = randomUUID(); - final UUID under18MasterDefendantId = randomUUID(); - final UUID newOffenceId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String under18Dob = hearingStartDate.minusYears(16).toString(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) - .withListedCases(new ArrayList<>(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(new ArrayList<>(Arrays.asList(Defendant.defendant() - .withId(existingDefendantId) - .withOffences(new ArrayList<>(Arrays.asList(Offence.offence().withId(existingOffenceId).build()))) - .build()))) - .build()))) - .build()) - .build()); - - hearing.apply(HearingAllocatedForListing.hearingAllocatedForListing() - .withHearingId(hearingId) - .withProsecutionCaseDefendantsOffenceIds(Arrays.asList(ProsecutionCaseDefendantOffenceIds.prosecutionCaseDefendantOffenceIds() - .withId(caseId) - .withDefendants(Arrays.asList(DefendantOffenceIds.defendantOffenceIds() - .withId(existingDefendantId) - .withOffenceIds(Arrays.asList(existingOffenceId)) - .build())) - .build())) - .build()); - - final UUID newCaseId = randomUUID(); - final List resultEvents = hearing.addCasesToHearing( - Arrays.asList(ProsecutionCase.prosecutionCase() - .withId(newCaseId) - .withDefendants(Arrays.asList(uk.gov.justice.core.courts.Defendant.defendant() - .withId(under18DefendantId) - .withMasterDefendantId(under18MasterDefendantId) - .withPersonDefendant(uk.gov.justice.core.courts.PersonDefendant.personDefendant() - .withPersonDetails(uk.gov.justice.core.courts.Person.person() - .withDateOfBirth(under18Dob) - .build()) - .build()) - .withOffences(Arrays.asList(uk.gov.justice.core.courts.Offence.offence().withId(newOffenceId).build())) - .build())) - .withProsecutionCaseIdentifier(prosecutionCaseIdentifier() - .withProsecutionAuthorityCode(STRING.next()) - .withProsecutionAuthorityId(randomUUID()) - .withProsecutionAuthorityReference(STRING.next()) - .build()) - .build()), - null, empty()) - .collect(Collectors.toList()); - - assertThat(resultEvents, hasSize(2)); - assertThat(resultEvents.get(0), CoreMatchers.instanceOf(CasesAddedToHearing.class)); - assertThat(resultEvents.get(1), CoreMatchers.instanceOf(CourtListRestricted.class)); - - final CourtListRestricted courtListRestricted = (CourtListRestricted) resultEvents.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getDefendantIds(), hasSize(1)); - assertThat(courtListRestricted.getDefendantIds(), hasItem(under18DefendantId)); - } - private uk.gov.justice.listing.events.Hearing prepareHearing(final UUID hearingId, final Boolean allocated, final Map>>>>> cases){ return uk.gov.justice.listing.events.Hearing.hearing() .withId(hearingId) @@ -4646,7 +4546,7 @@ public void shouldNotRemoveAllocatedNextHearingWhenSeedingHearingAmendedAndNextH final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final OffencesRemovedFromHearing offencesRemovedFromHearing = (OffencesRemovedFromHearing) deleteHearingEventsList.get(0); @@ -4691,7 +4591,7 @@ public void shouldNotRemoveAllocatedNextHearingWhenSeedingHearingAmendedAndNextH final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final OffencesRemovedFromHearing offencesRemovedFromHearing = (OffencesRemovedFromHearing) deleteHearingEventsList.get(0); @@ -4741,7 +4641,7 @@ case2Id, asList(Map.of(defendant2Id, asList(Map.of(offence3Id, Optional.of(seedi final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final OffencesRemovedFromHearing offencesRemovedFromHearing = (OffencesRemovedFromHearing) deleteHearingEventsList.get(0); @@ -4780,7 +4680,7 @@ case2Id, asList(Map.of(defendant2Id, asList(Map.of(offence3Id, Optional.of(seedi final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final OffencesRemovedFromHearing offencesRemovedFromHearing = (OffencesRemovedFromHearing) deleteHearingEventsList.get(0); @@ -4814,7 +4714,7 @@ public void shouldDeleteNextHearingWhenNextHearingHasSeedingHearingsAndNewOffenc final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final AllocatedHearingDeleted offencesRemovedFromHearing = (AllocatedHearingDeleted) deleteHearingEventsList.get(0); @@ -4847,7 +4747,7 @@ public void shouldDeleteOffencesWhenNextHearingHasMultipleSeedingHearingsOneCase final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final OffencesRemovedFromHearing offencesRemovedFromHearing = (OffencesRemovedFromHearing) deleteHearingEventsList.get(0); @@ -4888,7 +4788,7 @@ defendant2Id, asList(Map.of(offence3Id, Optional.of(seedingHearingId)), Map.of(o final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final OffencesRemovedFromHearing offencesRemovedFromHearing = (OffencesRemovedFromHearing) deleteHearingEventsList.get(0); @@ -4930,7 +4830,7 @@ case2Id, asList(Map.of(defendant2Id, asList(Map.of(offence3Id, Optional.of(seedi final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final AllocatedHearingDeleted offencesRemovedFromHearing = (AllocatedHearingDeleted) deleteHearingEventsList.get(0); @@ -4971,7 +4871,7 @@ case2Id, asList(Map.of(defendant2Id, asList(Map.of(offence1Id, Optional.of(seedi final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final OffencesRemovedFromHearing offencesRemovedFromHearing = (OffencesRemovedFromHearing) deleteHearingEventsList.get(0); @@ -5015,7 +4915,7 @@ case2Id, asList(Map.of(defendant2Id, asList(Map.of(offence1Id, Optional.empty()) final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - assertThat(deleteHearingEventsList.size(), is(1)); + assertThat(deleteHearingEventsList.size(), is(2)); final OffencesRemovedFromHearing offencesRemovedFromHearing = (OffencesRemovedFromHearing) deleteHearingEventsList.get(0); @@ -5039,7 +4939,7 @@ void shouldReturnFalse_WhenCurrentHearingEventStateIsNull() { } @Test - void shouldReturnFalse_WhenJurisdictionTypeIsNotMagistrates() { + void shouldReturnTrue_WhenJurisdictionTypeIsCrownAndOtherConditionsMatch() { final UUID ejectedItemId = randomUUID(); final UUID hearingId = randomUUID(); @@ -5048,7 +4948,7 @@ void shouldReturnFalse_WhenJurisdictionTypeIsNotMagistrates() { .withId(hearingId) .withType(uk.gov.justice.listing.events.Type.type().build()) .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(CROWN) // Not MAGISTRATES + .withJurisdictionType(CROWN) .withHearingDays(emptyList()) .withCourtRoomId(randomUUID()) .withAllocated(true) @@ -5061,7 +4961,7 @@ void shouldReturnFalse_WhenJurisdictionTypeIsNotMagistrates() { boolean result = hearing.magistrateHearingIsInTheFutureAndAllCaseAndApplicationAreEjected(ejectedItemId); - assertThat(result, is(false)); + assertThat(result, is(true)); } @Test @@ -5897,64 +5797,6 @@ void shouldAddMultipleCourtApplicationIdsToConfirmedList() { assertThat(allocatedEvent.getCourtApplicationIds(), hasItems(applicationId1, applicationId2)); } - @Test - void shouldAddCourtApplicationToCurrentHearingEventStateWhenCourtApplicationsIsNull() { - final UUID applicationId = randomUUID(); - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID offenceId = randomUUID(); - - applyHearingListedWithMinimalCase(caseId, defendantId, offenceId); - hearing.onCourtApplicationAddedForHearing(createCourtApplicationAddedForHearingEvent(applicationId)); - - assertThat(hearing.getCurrentHearingEventState().getCourtApplications(), is(notNullValue())); - assertThat(hearing.getCurrentHearingEventState().getCourtApplications(), hasSize(1)); - assertThat(hearing.getCurrentHearingEventState().getCourtApplications().get(0).getId(), is(applicationId)); - } - - @Test - void shouldAddCourtApplicationToCurrentHearingEventStateWhenExistingApplicationsPresent() { - final UUID existingApplicationId = randomUUID(); - final UUID newApplicationId = randomUUID(); - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID offenceId = randomUUID(); - - applyHearingListedWithCourtApplicationsAndCase(asList(existingApplicationId), caseId, defendantId, offenceId); - hearing.onCourtApplicationAddedForHearing(createCourtApplicationAddedForHearingEvent(newApplicationId)); - - assertThat(hearing.getCurrentHearingEventState().getCourtApplications(), hasSize(2)); - assertThat(hearing.getCurrentHearingEventState().getCourtApplications().stream() - .map(uk.gov.justice.listing.events.CourtApplication::getId) - .collect(Collectors.toList()), hasItems(existingApplicationId, newApplicationId)); - } - - @Test - void shouldNotDuplicateCourtApplicationInCurrentHearingEventStateWhenAlreadyPresent() { - final UUID applicationId = randomUUID(); - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID offenceId = randomUUID(); - - applyHearingListedWithCourtApplicationsAndCase(asList(applicationId), caseId, defendantId, offenceId); - hearing.onCourtApplicationAddedForHearing(createCourtApplicationAddedForHearingEvent(applicationId)); - - assertThat(hearing.getCurrentHearingEventState().getCourtApplications(), hasSize(1)); - assertThat(hearing.getCurrentHearingEventState().getCourtApplications().get(0).getId(), is(applicationId)); - } - - @Test - void shouldNotUpdateCurrentHearingEventStateWhenCourtApplicationIsNull() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID offenceId = randomUUID(); - - applyHearingListedWithMinimalCase(caseId, defendantId, offenceId); - hearing.onCourtApplicationAddedForHearing(createCourtApplicationAddedForHearingEvent(null)); - - assertThat(hearing.getCurrentHearingEventState().getCourtApplications(), is(nullValue())); - } - private void applyBasicHearingListed() { hearing.apply(createBasicHearingListedEvent()); } @@ -6238,1604 +6080,304 @@ void shouldReturnEmptyStream_WhenHearingIsDeleted() { assertThat(eventsList.size(), is(0)); } + // ─── CROWN allocation with courtScheduleIds and isDraft tests ──────── + @Test - public void shouldNotRaisedOffencesRemovedFromHearingEvent() { - final UUID seedingHearingId = randomUUID(); - final UUID seedingHearingId2 = randomUUID(); - final UUID case1Id = randomUUID(); - final UUID case2Id = randomUUID(); - final UUID defendant1Id = randomUUID(); - final UUID defendant2Id = randomUUID(); - final UUID offence1Id = randomUUID(); - final UUID offence2Id = randomUUID(); - final UUID offence3Id = randomUUID(); + void shouldAllocateCrownHearingWhenAllHearingDaysHaveCourtScheduleIdsAndNoneAreDraft() { + final UUID crownHearingId = randomUUID(); + final UUID crownCourtRoomId = randomUUID(); hearing.apply(HearingListed.hearingListed() .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) + .withId(crownHearingId) .withType(uk.gov.justice.listing.events.Type.type().build()) .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withEndDate(now().plusDays(1)) - .withStartDate(now()) - .withEstimatedMinutes(30) - .withEstimatedDuration("30 minutes") - .withListedCases(new ArrayList<>(asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(case1Id) - .withDefendants(new ArrayList<>(asList(Defendant.defendant() - .withId(defendant1Id) - .withOffences(new ArrayList<>(asList(Offence.offence() - .withId(offence1Id) - .withSeedingHearing(SeedingHearing.seedingHearing() - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.CROWN) - .withSeedingHearingId(seedingHearingId) - .build()) - .build()))) - .build()))) - .build(), - uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(case2Id) - .withDefendants(new ArrayList<>(asList(Defendant.defendant() - .withId(defendant2Id) - .withOffences(new ArrayList<>(asList( - Offence.offence() - .withId(offence2Id) - .withSeedingHearing(SeedingHearing.seedingHearing() - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.CROWN) - .withSeedingHearingId(seedingHearingId2) - .build()) - .build()))) - - .build()))) - .build()))) + .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.CROWN) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withCourtScheduleId(randomUUID()) + .withHearingDate(LocalDate.now().plusDays(5)) + .withIsDraft(false) + .build())) + .withCourtRoomId(crownCourtRoomId) + .withStartDate(LocalDate.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(5)) + .withEstimatedMinutes(240) + .withEstimatedDuration("240 minutes") + .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() + .withId(randomUUID()) + .withDefendants(Arrays.asList(Defendant.defendant() + .withId(randomUUID()) + .withOffences(Arrays.asList(Offence.offence() + .withId(randomUUID()) + .build())) + .build())) + .build())) .build()) .build()); - hearing.apply(HearingAllocatedForListingV2.hearingAllocatedForListingV2() - .withHearingId(hearingId) - .withCourtRoomId(randomUUID()) - .withProsecutionCaseDefendantsOffenceIds(new ArrayList<>(asList( - ProsecutionCaseDefendantOffenceIdsV2.prosecutionCaseDefendantOffenceIdsV2() - .withId(case1Id) - .withDefendants(new ArrayList<>(asList(DefendantOffenceIdsV2.defendantOffenceIdsV2() - .withId(defendant1Id) - .withOffenceIds(new ArrayList<>(asList(OffenceIds.offenceIds() - .withId(offence3Id) - .withSeedingHearing(SeedingHearing.seedingHearing() - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.CROWN) - .withSeedingHearingId(seedingHearingId) - .build()) - .build()))) - .build()))) - .build()))) - .build()); - - final Stream deleteHearingStream = hearing.deleteHearing(seedingHearingId, hearingId); - - final List deleteHearingEventsList = deleteHearingStream.collect(Collectors.toList()); - // no OffencesRemovedFromHearing event raised - assertThat(deleteHearingEventsList.size(), is(1)); - assertThat(deleteHearingEventsList.get(0), CoreMatchers.instanceOf(AvailableSlotsForHearingFreed.class)); - } - @Test - void shouldEmitCourtListRestrictedForUnder18DefendantOnAllocation() { - final UUID caseId = randomUUID(); - final UUID under18DefendantId = randomUUID(); - final UUID under18MasterDefendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String under18Dob = hearingStartDate.minusYears(16).toString(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(under18DefendantId) - .withMasterDefendantId(under18MasterDefendantId) - .withDateOfBirth(under18Dob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) - .build())) - .build())) - .build())) - .build()) - .build()); - - final List allocationEvents = hearing.applyAllocationRules( - Arrays.asList(uk.gov.moj.cpp.listing.domain.ProsecutionCaseDefendantOffenceIds.prosecutionCaseDefendantOffenceIds() - .withId(caseId) - .withDefendants(Arrays.asList(uk.gov.moj.cpp.listing.domain.DefendantOffenceIds.defendantOffenceIds() - .withId(under18DefendantId) - .withOffences(Arrays.asList(uk.gov.moj.cpp.listing.domain.OffenceIds.offenceIds() - .withId(offenceId) - .build())) - .build())) - .build()), - true, true).collect(Collectors.toList()); + final Stream allocationStream = Stream.of(hearing.applyAllocationRules(of(randomUUID()), true, true, emptyList(), empty(), null)).flatMap(i -> i); + final List allocationEvents = allocationStream.toList(); - assertThat(allocationEvents, hasSize(2)); - assertThat(allocationEvents.get(0), is(CoreMatchers.instanceOf(HearingAllocatedForListingV2.class))); - assertThat(allocationEvents.get(1), is(CoreMatchers.instanceOf(CourtListRestricted.class))); - - final CourtListRestricted courtListRestricted = (CourtListRestricted) allocationEvents.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getDefendantIds(), hasSize(1)); - assertThat(courtListRestricted.getDefendantIds(), hasItem(under18DefendantId)); + assertThat(allocationEvents.size(), is(1)); + assertTrue(allocationEvents.get(0) instanceof HearingAllocatedForListingV2); } @Test - void shouldNotEmitCourtListRestrictedForAdultDefendantOnAllocation() { - final UUID caseId = randomUUID(); - final UUID adultDefendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String adultDob = hearingStartDate.minusYears(25).toString(); + void shouldNotAllocateCrownHearingWhenAnyHearingDayIsDraft() { + final UUID crownHearingId = randomUUID(); + final UUID crownCourtRoomId = randomUUID(); hearing.apply(HearingListed.hearingListed() .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) + .withId(crownHearingId) .withType(uk.gov.justice.listing.events.Type.type().build()) .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) + .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.CROWN) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withCourtScheduleId(randomUUID()) + .withHearingDate(LocalDate.now().plusDays(5)) + .withIsDraft(true) // draft session + .build())) + .withCourtRoomId(crownCourtRoomId) + .withStartDate(LocalDate.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(5)) + .withEstimatedMinutes(240) + .withEstimatedDuration("240 minutes") .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) + .withId(randomUUID()) .withDefendants(Arrays.asList(Defendant.defendant() - .withId(adultDefendantId) - .withDateOfBirth(adultDob) - .withRestrictFromCourtList(false) + .withId(randomUUID()) .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) + .withId(randomUUID()) .build())) .build())) .build())) .build()) .build()); - final List allocationEvents = hearing.applyAllocationRules( - Arrays.asList(uk.gov.moj.cpp.listing.domain.ProsecutionCaseDefendantOffenceIds.prosecutionCaseDefendantOffenceIds() - .withId(caseId) - .withDefendants(Arrays.asList(uk.gov.moj.cpp.listing.domain.DefendantOffenceIds.defendantOffenceIds() - .withId(adultDefendantId) - .withOffences(Arrays.asList(uk.gov.moj.cpp.listing.domain.OffenceIds.offenceIds() - .withId(offenceId) - .build())) - .build())) - .build()), - true, true).collect(Collectors.toList()); + final Stream allocationStream = Stream.of(hearing.applyAllocationRules(of(randomUUID()), true, true, emptyList(), empty(), null)).flatMap(i -> i); + final List allocationEvents = allocationStream.toList(); - assertThat(allocationEvents, hasSize(1)); - assertThat(allocationEvents.get(0), is(CoreMatchers.instanceOf(HearingAllocatedForListingV2.class))); + // Should NOT allocate because isDraft=true + assertThat(allocationEvents.size(), is(0)); } @Test - void shouldOnlyRestrictUnder18DefendantsWhenMixOfAdultAndYouth() { - final UUID caseId = randomUUID(); - final UUID youthDefendantId = randomUUID(); - final UUID adultDefendantId = randomUUID(); - final UUID offence1Id = randomUUID(); - final UUID offence2Id = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String youthDob = hearingStartDate.minusYears(15).toString(); - final String adultDob = hearingStartDate.minusYears(30).toString(); + void shouldNotAllocateCrownHearingWhenMultiDayAndOneHearingDayIsDraft() { + final UUID crownHearingId = randomUUID(); + final UUID crownCourtRoomId = randomUUID(); hearing.apply(HearingListed.hearingListed() .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) + .withId(crownHearingId) .withType(uk.gov.justice.listing.events.Type.type().build()) .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList( - Defendant.defendant() - .withId(youthDefendantId) - .withMasterDefendantId(youthDefendantId) - .withDateOfBirth(youthDob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offence1Id) - .build())) - .build(), - Defendant.defendant() - .withId(adultDefendantId) - .withMasterDefendantId(adultDefendantId) - .withDateOfBirth(adultDob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offence2Id) - .build())) - .build())) - .build())) - .build()) - .build()); - - final List allocationEvents = hearing.applyAllocationRules( - Arrays.asList(uk.gov.moj.cpp.listing.domain.ProsecutionCaseDefendantOffenceIds.prosecutionCaseDefendantOffenceIds() - .withId(caseId) - .withDefendants(Arrays.asList( - uk.gov.moj.cpp.listing.domain.DefendantOffenceIds.defendantOffenceIds() - .withId(youthDefendantId) - .withOffences(Arrays.asList(uk.gov.moj.cpp.listing.domain.OffenceIds.offenceIds() - .withId(offence1Id) - .build())) + .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.CROWN) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withCourtScheduleId(randomUUID()) + .withHearingDate(LocalDate.now().plusDays(5)) + .withIsDraft(false) .build(), - uk.gov.moj.cpp.listing.domain.DefendantOffenceIds.defendantOffenceIds() - .withId(adultDefendantId) - .withOffences(Arrays.asList(uk.gov.moj.cpp.listing.domain.OffenceIds.offenceIds() - .withId(offence2Id) - .build())) + HearingDay.hearingDay() + .withCourtScheduleId(randomUUID()) + .withHearingDate(LocalDate.now().plusDays(6)) + .withIsDraft(true) // one day is draft .build())) - .build()), - true, true).collect(Collectors.toList()); - - assertThat(allocationEvents, hasSize(2)); - assertThat(allocationEvents.get(0), is(CoreMatchers.instanceOf(HearingAllocatedForListingV2.class))); - assertThat(allocationEvents.get(1), is(CoreMatchers.instanceOf(CourtListRestricted.class))); - - final CourtListRestricted courtListRestricted = (CourtListRestricted) allocationEvents.get(1); - assertThat(courtListRestricted.getDefendantIds(), hasSize(1)); - assertThat(courtListRestricted.getDefendantIds(), hasItem(youthDefendantId)); - } - - @Test - void shouldNotEmitCourtListRestrictedWhenDefendantAlreadyRestricted() { - final UUID caseId = randomUUID(); - final UUID under18DefendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String under18Dob = hearingStartDate.minusYears(16).toString(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) + .withCourtRoomId(crownCourtRoomId) + .withStartDate(LocalDate.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(6)) + .withEstimatedMinutes(720) + .withEstimatedDuration("720 minutes") .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) + .withId(randomUUID()) .withDefendants(Arrays.asList(Defendant.defendant() - .withId(under18DefendantId) - .withDateOfBirth(under18Dob) - .withRestrictFromCourtList(true) + .withId(randomUUID()) .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) + .withId(randomUUID()) .build())) .build())) .build())) .build()) .build()); - final List allocationEvents = hearing.applyAllocationRules( - Arrays.asList(uk.gov.moj.cpp.listing.domain.ProsecutionCaseDefendantOffenceIds.prosecutionCaseDefendantOffenceIds() - .withId(caseId) - .withDefendants(Arrays.asList(uk.gov.moj.cpp.listing.domain.DefendantOffenceIds.defendantOffenceIds() - .withId(under18DefendantId) - .withOffences(Arrays.asList(uk.gov.moj.cpp.listing.domain.OffenceIds.offenceIds() - .withId(offenceId) - .build())) - .build())) - .build()), - true, true).collect(Collectors.toList()); - - assertThat(allocationEvents, hasSize(1)); - assertThat(allocationEvents.get(0), is(CoreMatchers.instanceOf(HearingAllocatedForListingV2.class))); - } - - @Test - void shouldEmitCourtListRestrictedForUnder18DefendantAddedViaHearingExtension() { - final UUID existingDefendantId = randomUUID(); - final UUID newUnder18DefendantId = randomUUID(); - final UUID prosecutionCaseId = randomUUID(); - final UUID newCaseId = randomUUID(); - final UUID hearingId = randomUUID(); - final UUID offenceId = randomUUID(); - final UUID newOffenceId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String under18Dob = hearingStartDate.minusYears(15).toString(); - - uk.gov.justice.listing.events.Hearing firstHearing = uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withCourtRoomId(randomUUID()) - .withAllocated(Boolean.TRUE) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(prosecutionCaseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(existingDefendantId) - .withDateOfBirth(hearingStartDate.minusYears(30).toString()) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) - .build())) - .build())) - .build())) - .build(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(firstHearing) - .build()); - - uk.gov.justice.listing.events.ListedCase newCase = uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(newCaseId) - .withMarkers(emptyList()) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(newUnder18DefendantId) - .withDateOfBirth(under18Dob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(newOffenceId) - .build())) - .build())) - .build(); - - uk.gov.justice.listing.events.Hearing extendedHearing = uk.gov.justice.listing.events.Hearing.hearing() - .withValuesFrom(firstHearing) - .withListedCases(Arrays.asList(firstHearing.getListedCases().get(0), newCase)) - .build(); - - hearing.apply(HearingListedCaseUpdated.hearingListedCaseUpdated() - .withHearing(extendedHearing) - .withUnAllocatedListedCases(Arrays.asList(newCase)) - .build()); - - final List extensionEvents = hearing.applyAllocationRulesForExtendedHearing(extendedHearing, false, false) - .collect(Collectors.toList()); - - assertThat(extensionEvents, hasSize(1)); - assertThat(extensionEvents.get(0), is(CoreMatchers.instanceOf(AllocatedHearingExtendedForListingV2.class))); + final Stream allocationStream = Stream.of(hearing.applyAllocationRules(of(randomUUID()), true, true, emptyList(), empty(), null)).flatMap(i -> i); + final List allocationEvents = allocationStream.toList(); + // Should NOT allocate because one hearingDay has isDraft=true + assertThat(allocationEvents.size(), is(0)); } @Test - void shouldEmitCourtListRestrictedForUnder18DefendantOnWeekCommencingDateChange() { - final UUID caseId = randomUUID(); - final UUID under18DefendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final LocalDate wcStartDate = LocalDate.now().plusDays(30); - final LocalDate wcEndDate = wcStartDate.plusWeeks(1); - final String under18Dob = wcStartDate.minusYears(16).toString(); + void shouldNotAllocateCrownHearingWhenHearingDaysMissCourtScheduleIds() { + final UUID crownHearingId = randomUUID(); + final UUID crownCourtRoomId = randomUUID(); hearing.apply(HearingListed.hearingListed() .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) + .withId(crownHearingId) .withType(uk.gov.justice.listing.events.Type.type().build()) .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withEstimatedMinutes(30) + .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.CROWN) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now().plusDays(5)) + .withIsDraft(false) + .build())) // no courtScheduleId — must NOT allocate + .withCourtRoomId(crownCourtRoomId) + .withStartDate(LocalDate.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(5)) + .withEstimatedMinutes(240) + .withEstimatedDuration("240 minutes") .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) + .withId(randomUUID()) .withDefendants(Arrays.asList(Defendant.defendant() - .withId(under18DefendantId) - .withMasterDefendantId(under18DefendantId) - .withDateOfBirth(under18Dob) - .withRestrictFromCourtList(false) + .withId(randomUUID()) .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) + .withId(randomUUID()) .build())) .build())) .build())) .build()) .build()); - final List events = hearing.changeWeekCommencingDate( - wcStartDate, wcEndDate, 1, hearingId).collect(Collectors.toList()); - - assertThat(events, hasSize(2)); - assertThat(events.get(0), is(CoreMatchers.instanceOf(WeekCommencingDateChangedForHearing.class))); - assertThat(events.get(1), is(CoreMatchers.instanceOf(CourtListRestricted.class))); + final Stream allocationStream = Stream.of(hearing.applyAllocationRules(of(randomUUID()), true, true, emptyList(), empty(), null)).flatMap(i -> i); + final List allocationEvents = allocationStream.toList(); - final CourtListRestricted courtListRestricted = (CourtListRestricted) events.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getDefendantIds(), hasSize(1)); - assertThat(courtListRestricted.getDefendantIds(), hasItem(under18DefendantId)); + // Crown hearings without courtScheduleIds must NOT allocate + assertThat(allocationEvents.size(), is(0)); } @Test - void shouldNotEmitCourtListRestrictedForAdultDefendantOnWeekCommencingDateChange() { - final UUID caseId = randomUUID(); - final UUID adultDefendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final LocalDate wcStartDate = LocalDate.now().plusDays(30); - final LocalDate wcEndDate = wcStartDate.plusWeeks(1); - final String adultDob = wcStartDate.minusYears(25).toString(); + void shouldNotAllocateCrownHearingWhenHearingDaysEmpty() { + final UUID crownHearingId = randomUUID(); + final UUID crownCourtRoomId = randomUUID(); hearing.apply(HearingListed.hearingListed() .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) + .withId(crownHearingId) .withType(uk.gov.justice.listing.events.Type.type().build()) .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withEstimatedMinutes(30) + .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.CROWN) + .withHearingDays(emptyList()) + .withCourtRoomId(crownCourtRoomId) + .withStartDate(LocalDate.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(5)) + .withEstimatedMinutes(240) + .withEstimatedDuration("240 minutes") .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) + .withId(randomUUID()) .withDefendants(Arrays.asList(Defendant.defendant() - .withId(adultDefendantId) - .withDateOfBirth(adultDob) - .withRestrictFromCourtList(false) + .withId(randomUUID()) .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) + .withId(randomUUID()) .build())) .build())) .build())) .build()) .build()); - final List events = hearing.changeWeekCommencingDate( - wcStartDate, wcEndDate, 1, hearingId).collect(Collectors.toList()); + final Stream allocationStream = Stream.of(hearing.applyAllocationRules(of(randomUUID()), true, true, emptyList(), empty(), null)).flatMap(i -> i); + final List allocationEvents = allocationStream.toList(); - assertThat(events, hasSize(1)); - assertThat(events.get(0), is(CoreMatchers.instanceOf(WeekCommencingDateChangedForHearing.class))); + // Crown hearings with empty hearingDays must NOT allocate — no courtScheduleIds + assertThat(allocationEvents.size(), is(0)); } + // ─── CROWN vacate-trial slot payback tests ─────────────────────────── + @Test - void shouldEmitCourtListRestrictedForUnder18DefendantOnStartDateChangeWhenAllocated() { - final UUID caseId = randomUUID(); - final UUID under18DefendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final LocalDate originalStartDate = LocalDate.now().plusDays(60); - final LocalDate newStartDate = LocalDate.now().plusDays(30); - final String under18Dob = newStartDate.minusYears(16).toString(); + void shouldEmitAvailableSlotsForHearingFreedWhenVacatingCrownTrialWithCourtScheduleIds() { + final UUID crownHearingId = randomUUID(); + final UUID vacatingTrialReasonId = randomUUID(); hearing.apply(HearingListed.hearingListed() .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) + .withId(crownHearingId) .withType(uk.gov.justice.listing.events.Type.type().build()) .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withAllocated(Boolean.TRUE) - .withStartDate(originalStartDate) - .withEndDate(originalStartDate.plusDays(3)) - .withEstimatedMinutes(30) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(under18DefendantId) - .withMasterDefendantId(under18DefendantId) - .withDateOfBirth(under18Dob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) - .build())) + .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.CROWN) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withCourtScheduleId(randomUUID()) + .withHearingDate(LocalDate.now().plusDays(5)) + .withIsDraft(false) .build())) - .build())) + .withCourtRoomId(randomUUID()) + .withStartDate(LocalDate.now().plusDays(5)) + .withEndDate(LocalDate.now().plusDays(5)) + .withEstimatedMinutes(240) + .withEstimatedDuration("240 minutes") .build()) .build()); - final List events = hearing.changeStartDate(newStartDate, hearingId) - .collect(Collectors.toList()); - - assertThat(events, hasSize(2)); - assertThat(events.get(0), is(CoreMatchers.instanceOf(StartDateChangedForHearing.class))); - assertThat(events.get(1), is(CoreMatchers.instanceOf(CourtListRestricted.class))); + final Stream events = hearing.hearingVacateTrial(Optional.of(vacatingTrialReasonId)); + final List eventsList = events.toList(); - final CourtListRestricted courtListRestricted = (CourtListRestricted) events.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getDefendantIds(), hasSize(1)); - assertThat(courtListRestricted.getDefendantIds(), hasItem(under18DefendantId)); + // Should emit both HearingTrialVacated + AvailableSlotsForHearingFreed + assertThat(eventsList.size(), is(2)); + assertTrue(eventsList.stream().anyMatch(e -> e instanceof uk.gov.justice.listing.events.HearingTrialVacated)); + assertTrue(eventsList.stream().anyMatch(AvailableSlotsForHearingFreed.class::isInstance)); } @Test - void shouldEmitCourtListRestrictedForUnder18DefendantOnAllocationViaListCourtHearing() { - final UUID caseId = randomUUID(); - final UUID under18DefendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String under18Dob = hearingStartDate.minusYears(16).toString(); - final UUID bookingReference = randomUUID(); + void shouldEmitAvailableSlotsForHearingFreedWhenVacatingCrownTrial() { + final UUID crownHearingId = randomUUID(); + final UUID vacatingTrialReasonId = randomUUID(); hearing.apply(HearingListed.hearingListed() .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) + .withId(crownHearingId) .withType(uk.gov.justice.listing.events.Type.type().build()) .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) + .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.CROWN) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withHearingDate(LocalDate.now().plusDays(5)) + .build())) // no courtScheduleId .withCourtRoomId(randomUUID()) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(under18DefendantId) - .withMasterDefendantId(under18DefendantId) - .withDateOfBirth(under18Dob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) - .build())) - .build())) - .build())) + .withStartDate(LocalDate.now().plusDays(5)) + .withEstimatedMinutes(240) .build()) .build()); - final List allocationEvents = hearing.applyAllocationRules( - Optional.of(bookingReference), false, false, - Arrays.asList(uk.gov.moj.cpp.listing.domain.ProsecutionCaseDefendantOffenceIds.prosecutionCaseDefendantOffenceIds() - .withId(caseId) - .withDefendants(Arrays.asList(uk.gov.moj.cpp.listing.domain.DefendantOffenceIds.defendantOffenceIds() - .withId(under18DefendantId) - .withOffences(Arrays.asList(uk.gov.moj.cpp.listing.domain.OffenceIds.offenceIds() - .withId(offenceId) - .build())) - .build())) - .build()), - Optional.empty(), false).collect(Collectors.toList()); - - assertThat(allocationEvents, hasSize(2)); - assertThat(allocationEvents.get(0), is(CoreMatchers.instanceOf(HearingAllocatedForListingV2.class))); - assertThat(allocationEvents.get(1), is(CoreMatchers.instanceOf(CourtListRestricted.class))); + final Stream events = hearing.hearingVacateTrial(Optional.of(vacatingTrialReasonId)); + final List eventsList = events.toList(); - final CourtListRestricted courtListRestricted = (CourtListRestricted) allocationEvents.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getDefendantIds(), hasSize(1)); - assertThat(courtListRestricted.getDefendantIds(), hasItem(under18DefendantId)); + // Should emit both HearingTrialVacated + AvailableSlotsForHearingFreed for Crown jurisdiction + assertThat(eventsList.size(), is(2)); + assertTrue(eventsList.stream().anyMatch(e -> e instanceof uk.gov.justice.listing.events.HearingTrialVacated)); + assertTrue(eventsList.stream().anyMatch(AvailableSlotsForHearingFreed.class::isInstance)); } @Test - void shouldEmitCourtListRestrictedWhenDefendantDateOfBirthChangesToUnder18() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String adultDob = hearingStartDate.minusYears(25).toString(); - final String under18Dob = hearingStartDate.minusYears(16).toString(); + void shouldNotEmitAvailableSlotsForHearingFreedWhenVacatingCrownTrialWithoutReason() { + final UUID crownHearingId = randomUUID(); hearing.apply(HearingListed.hearingListed() .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) + .withId(crownHearingId) .withType(uk.gov.justice.listing.events.Type.type().build()) .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withAllocated(Boolean.TRUE) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withMasterDefendantId(defendantId) - .withDateOfBirth(adultDob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) - .build())) + .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.CROWN) + .withHearingDays(Arrays.asList( + HearingDay.hearingDay() + .withCourtScheduleId(randomUUID()) + .withHearingDate(LocalDate.now().plusDays(5)) .build())) - .build())) + .withCourtRoomId(randomUUID()) + .withStartDate(LocalDate.now().plusDays(5)) + .withEstimatedMinutes(240) .build()) .build()); - final List updatedDefendants = Arrays.asList( - uk.gov.moj.cpp.listing.domain.Defendant.defendant() - .withId(defendantId) - .withMasterDefendantId( Optional.of(defendantId)) - .withDateOfBirth(of(under18Dob)) - .build() - ); - - final List events = hearing.updateDefendants(caseId, updatedDefendants).collect(Collectors.toList()); - - assertThat(events, hasSize(2)); - assertThat(events.get(0), is(CoreMatchers.instanceOf(NewDefendantDetailsUpdated.class))); - assertThat(events.get(1), is(CoreMatchers.instanceOf(CourtListRestricted.class))); + final Stream events = hearing.hearingVacateTrial(Optional.empty()); // no reason + final List eventsList = events.toList(); - final CourtListRestricted courtListRestricted = (CourtListRestricted) events.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getDefendantIds(), hasSize(1)); - assertThat(courtListRestricted.getDefendantIds(), hasItem(defendantId)); + // Should only emit HearingTrialVacated (no slot freed because vacatingTrialReasonId is empty) + assertThat(eventsList.size(), is(1)); + assertTrue(eventsList.get(0) instanceof uk.gov.justice.listing.events.HearingTrialVacated); } - - @Test - void shouldNotEmitCourtListRestrictedWhenDefendantDateOfBirthIsUnchanged() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String adultDob = hearingStartDate.minusYears(25).toString(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withAllocated(Boolean.TRUE) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withDateOfBirth(adultDob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) - .build())) - .build())) - .build())) - .build()) - .build()); - - final List updatedDefendants = Arrays.asList( - uk.gov.moj.cpp.listing.domain.Defendant.defendant() - .withId(defendantId) - .withDateOfBirth(of(adultDob)) - .build() - ); - - final List events = hearing.updateDefendants(caseId, updatedDefendants).collect(Collectors.toList()); - - assertThat(events, hasSize(1)); - assertThat(events.get(0), is(CoreMatchers.instanceOf(NewDefendantDetailsUpdated.class))); - } - - @Test - void shouldEmitCourtListRestrictedForUnder18SubjectOnAllocation() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final UUID courtApplicationId = randomUUID(); - final UUID subjectId = randomUUID(); - final UUID subjectMasterDefendantId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String adultDob = hearingStartDate.minusYears(25).toString(); - final String under18Dob = hearingStartDate.minusYears(16).toString(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withDateOfBirth(adultDob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) - .build())) - .build())) - .build())) - .withCourtApplications(new ArrayList<>(asList(uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(courtApplicationId) - .withApplicant(ApplicantRespondent.applicantRespondent().withId(randomUUID()).build()) - .withSubject(ApplicantRespondent.applicantRespondent() - .withId(subjectId) - .withMasterDefendantId(subjectMasterDefendantId) - .withDateOfBirth(under18Dob) - .withRestrictFromCourtList(false) - .build()) - .build()))) - .build()) - .build()); - - final List allocationEvents = hearing.applyAllocationRules( - Arrays.asList(uk.gov.moj.cpp.listing.domain.ProsecutionCaseDefendantOffenceIds.prosecutionCaseDefendantOffenceIds() - .withId(caseId) - .withDefendants(Arrays.asList(uk.gov.moj.cpp.listing.domain.DefendantOffenceIds.defendantOffenceIds() - .withId(defendantId) - .withOffences(Arrays.asList(uk.gov.moj.cpp.listing.domain.OffenceIds.offenceIds() - .withId(offenceId) - .build())) - .build())) - .build()), - true, true).collect(Collectors.toList()); - - assertThat(allocationEvents, hasSize(2)); - assertThat(allocationEvents.get(0), is(CoreMatchers.instanceOf(HearingAllocatedForListingV2.class))); - assertThat(allocationEvents.get(1), is(CoreMatchers.instanceOf(CourtListRestricted.class))); - - final CourtListRestricted courtListRestricted = (CourtListRestricted) allocationEvents.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getCourtApplicationSubjectIds(), hasSize(1)); - assertThat(courtListRestricted.getCourtApplicationSubjectIds(), hasItem(subjectId)); - } - - @Test - void shouldEmitCourtListRestrictedForUnder18RespondentOnAllocation() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final UUID courtApplicationId = randomUUID(); - final UUID respondentId = randomUUID(); - final UUID respondentMasterDefendantId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String adultDob = hearingStartDate.minusYears(25).toString(); - final String under18Dob = hearingStartDate.minusYears(16).toString(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withDateOfBirth(adultDob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) - .build())) - .build())) - .build())) - .withCourtApplications(new ArrayList<>(asList(uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(courtApplicationId) - .withApplicant(ApplicantRespondent.applicantRespondent().withId(randomUUID()).build()) - .withRespondents(Arrays.asList(ApplicantRespondent.applicantRespondent() - .withId(respondentId) - .withMasterDefendantId(respondentMasterDefendantId) - .withDateOfBirth(under18Dob) - .withRestrictFromCourtList(false) - .withIsRespondent(true) - .build())) - .build()))) - .build()) - .build()); - - final List allocationEvents = hearing.applyAllocationRules( - Arrays.asList(uk.gov.moj.cpp.listing.domain.ProsecutionCaseDefendantOffenceIds.prosecutionCaseDefendantOffenceIds() - .withId(caseId) - .withDefendants(Arrays.asList(uk.gov.moj.cpp.listing.domain.DefendantOffenceIds.defendantOffenceIds() - .withId(defendantId) - .withOffences(Arrays.asList(uk.gov.moj.cpp.listing.domain.OffenceIds.offenceIds() - .withId(offenceId) - .build())) - .build())) - .build()), - true, true).collect(Collectors.toList()); - - assertThat(allocationEvents, hasSize(2)); - assertThat(allocationEvents.get(0), is(CoreMatchers.instanceOf(HearingAllocatedForListingV2.class))); - assertThat(allocationEvents.get(1), is(CoreMatchers.instanceOf(CourtListRestricted.class))); - - final CourtListRestricted courtListRestricted = (CourtListRestricted) allocationEvents.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getCourtApplicationRespondentIds(), hasSize(1)); - assertThat(courtListRestricted.getCourtApplicationRespondentIds(), hasItem(respondentId)); - } - - @Test - void shouldNotEmitCourtListRestrictedForAdultSubjectAndRespondentOnAllocation() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final UUID courtApplicationId = randomUUID(); - final UUID subjectId = randomUUID(); - final UUID respondentId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String adultDob = hearingStartDate.minusYears(25).toString(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withDateOfBirth(adultDob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) - .build())) - .build())) - .build())) - .withCourtApplications(new ArrayList<>(asList(uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(courtApplicationId) - .withApplicant(ApplicantRespondent.applicantRespondent().withId(randomUUID()).build()) - .withSubject(ApplicantRespondent.applicantRespondent() - .withId(subjectId) - .withMasterDefendantId(randomUUID()) - .withDateOfBirth(adultDob) - .withRestrictFromCourtList(false) - .build()) - .withRespondents(Arrays.asList(ApplicantRespondent.applicantRespondent() - .withId(respondentId) - .withMasterDefendantId(randomUUID()) - .withDateOfBirth(adultDob) - .withRestrictFromCourtList(false) - .withIsRespondent(true) - .build())) - .build()))) - .build()) - .build()); - - final List allocationEvents = hearing.applyAllocationRules( - Arrays.asList(uk.gov.moj.cpp.listing.domain.ProsecutionCaseDefendantOffenceIds.prosecutionCaseDefendantOffenceIds() - .withId(caseId) - .withDefendants(Arrays.asList(uk.gov.moj.cpp.listing.domain.DefendantOffenceIds.defendantOffenceIds() - .withId(defendantId) - .withOffences(Arrays.asList(uk.gov.moj.cpp.listing.domain.OffenceIds.offenceIds() - .withId(offenceId) - .build())) - .build())) - .build()), - true, true).collect(Collectors.toList()); - - assertThat(allocationEvents, hasSize(1)); - assertThat(allocationEvents.get(0), is(CoreMatchers.instanceOf(HearingAllocatedForListingV2.class))); - } - - @Test - void shouldEmitCourtListRestrictedForMixOfUnder18SubjectRespondentAndDefendantOnAllocation() { - final UUID caseId = randomUUID(); - final UUID under18DefendantId = randomUUID(); - final UUID under18MasterDefendantId = randomUUID(); - final UUID offenceId = randomUUID(); - final UUID courtApplicationId = randomUUID(); - final UUID under18SubjectId = randomUUID(); - final UUID under18SubjectMasterDefendantId = randomUUID(); - final UUID under18RespondentId = randomUUID(); - final UUID under18RespondentMasterDefendantId = randomUUID(); - final UUID adultRespondentId = randomUUID(); - final LocalDate hearingStartDate = LocalDate.now().plusDays(30); - final String under18Dob = hearingStartDate.minusYears(16).toString(); - final String adultDob = hearingStartDate.minusYears(30).toString(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(Arrays.asList(HearingDay.hearingDay().withCourtScheduleId(randomUUID()).build())) - .withCourtRoomId(randomUUID()) - .withStartDate(hearingStartDate) - .withEndDate(hearingStartDate.plusDays(1)) - .withEstimatedMinutes(30) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withMasterDefendantId(under18MasterDefendantId) - .withId(under18DefendantId) - .withDateOfBirth(under18Dob) - .withRestrictFromCourtList(false) - .withOffences(Arrays.asList(Offence.offence() - .withId(offenceId) - .build())) - .build())) - .build())) - .withCourtApplications(new ArrayList<>(asList(uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(courtApplicationId) - .withApplicant(ApplicantRespondent.applicantRespondent().withId(randomUUID()).build()) - .withSubject(ApplicantRespondent.applicantRespondent() - .withId(under18SubjectId) - .withMasterDefendantId(under18SubjectMasterDefendantId) - .withDateOfBirth(under18Dob) - .withRestrictFromCourtList(false) - .build()) - .withRespondents(Arrays.asList( - ApplicantRespondent.applicantRespondent() - .withId(under18RespondentId) - .withMasterDefendantId(under18RespondentMasterDefendantId) - .withDateOfBirth(under18Dob) - .withRestrictFromCourtList(false) - .withIsRespondent(true) - .build(), - ApplicantRespondent.applicantRespondent() - .withId(adultRespondentId) - .withMasterDefendantId(randomUUID()) - .withDateOfBirth(adultDob) - .withRestrictFromCourtList(false) - .withIsRespondent(true) - .build())) - .build()))) - .build()) - .build()); - - final List allocationEvents = hearing.applyAllocationRules( - Arrays.asList(uk.gov.moj.cpp.listing.domain.ProsecutionCaseDefendantOffenceIds.prosecutionCaseDefendantOffenceIds() - .withId(caseId) - .withDefendants(Arrays.asList(uk.gov.moj.cpp.listing.domain.DefendantOffenceIds.defendantOffenceIds() - .withId(under18DefendantId) - .withOffences(Arrays.asList(uk.gov.moj.cpp.listing.domain.OffenceIds.offenceIds() - .withId(offenceId) - .build())) - .build())) - .build()), - true, true).collect(Collectors.toList()); - - assertThat(allocationEvents, hasSize(2)); - assertThat(allocationEvents.get(0), is(CoreMatchers.instanceOf(HearingAllocatedForListingV2.class))); - assertThat(allocationEvents.get(1), is(CoreMatchers.instanceOf(CourtListRestricted.class))); - - final CourtListRestricted courtListRestricted = (CourtListRestricted) allocationEvents.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getDefendantIds(), hasSize(1)); - assertThat(courtListRestricted.getDefendantIds(), hasItem(under18DefendantId)); - assertThat(courtListRestricted.getCourtApplicationSubjectIds(), hasSize(1)); - assertThat(courtListRestricted.getCourtApplicationSubjectIds(), hasItem(under18SubjectId)); - assertThat(courtListRestricted.getCourtApplicationRespondentIds(), hasSize(1)); - assertThat(courtListRestricted.getCourtApplicationRespondentIds(), hasItem(under18RespondentId)); - } - - @Test - void shouldEmitCourtListRestrictedForUnder18DefendantWhenListedWithWeekCommencingStartDate() { - final UUID caseId = randomUUID(); - final UUID under18DefendantId = randomUUID(); - final LocalDate wcStartDate = LocalDate.now().plusDays(30); - final LocalDate wcEndDate = wcStartDate.plusWeeks(1); - final String under18Dob = wcStartDate.minusYears(16).toString(); - - final List cases = Arrays.asList( - ListedCase.listedCase() - .withId(caseId) - .withCaseIdentifier(uk.gov.moj.cpp.listing.domain.CaseIdentifier.caseIdentifier().withAuthorityCode("test").build()) - .withIsCivil(Optional.empty()) - .withGroupId(Optional.empty()) - .withIsGroupMember(Optional.empty()) - .withIsGroupMaster(Optional.empty()) - .withDefendants(Arrays.asList( - uk.gov.moj.cpp.listing.domain.Defendant.defendant() - .withId(under18DefendantId) - .withDateOfBirth(of(under18Dob)) - .build() - )) - .build() - ); - - final List events = hearing.list( - hearingId, type, estimateMinutes, estimatedDuration, cases, - courtCentreId, judiciary, courtRoomId, listingDirections, jurisdictionType, - prosecutorDatesToAvoid, reportingRestrictionReason, null, null, courtCentreDefaults, - courtApplications, courtApplicationPartyListingNeeds, adjournedFromDate, - of(wcStartDate), of(wcEndDate), of(1), - emptyList(), emptyList(), emptyList(), isSlotsBooked, "", "", null, Optional.empty(), of(false), empty() - ).collect(Collectors.toList()); - - assertThat(events, hasSize(2)); - assertThat(events.get(0), is(CoreMatchers.instanceOf(HearingListed.class))); - assertThat(events.get(1), is(CoreMatchers.instanceOf(CourtListRestricted.class))); - - final CourtListRestricted courtListRestricted = (CourtListRestricted) events.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getDefendantIds(), hasSize(1)); - assertThat(courtListRestricted.getDefendantIds(), hasItem(under18DefendantId)); - } - - @Test - void shouldNotEmitCourtListRestrictedForAdultDefendantWhenListedWithWeekCommencingStartDate() { - final UUID caseId = randomUUID(); - final UUID adultDefendantId = randomUUID(); - final LocalDate wcStartDate = LocalDate.now().plusDays(30); - final LocalDate wcEndDate = wcStartDate.plusWeeks(1); - final String adultDob = wcStartDate.minusYears(25).toString(); - - final List cases = Arrays.asList( - ListedCase.listedCase() - .withId(caseId) - .withCaseIdentifier(uk.gov.moj.cpp.listing.domain.CaseIdentifier.caseIdentifier().withAuthorityCode("test").build()) - .withIsCivil(Optional.empty()) - .withGroupId(Optional.empty()) - .withIsGroupMember(Optional.empty()) - .withIsGroupMaster(Optional.empty()) - .withDefendants(Arrays.asList( - uk.gov.moj.cpp.listing.domain.Defendant.defendant() - .withId(adultDefendantId) - .withDateOfBirth(of(adultDob)) - .build() - )) - .build() - ); - - final List events = hearing.list( - hearingId, type, estimateMinutes, estimatedDuration, cases, - courtCentreId, judiciary, courtRoomId, listingDirections, jurisdictionType, - prosecutorDatesToAvoid, reportingRestrictionReason, null, null, courtCentreDefaults, - courtApplications, courtApplicationPartyListingNeeds, adjournedFromDate, - of(wcStartDate), of(wcEndDate), of(1), - emptyList(), emptyList(), emptyList(), isSlotsBooked, "", "", null, Optional.empty(), of(false), empty() - ).collect(Collectors.toList()); - - assertThat(events, hasSize(1)); - assertThat(events.get(0), is(CoreMatchers.instanceOf(HearingListed.class))); - } - - @Test - void shouldEmitCourtListRestrictedForUnder18DefendantWhenListedUnscheduledWithWeekCommencingStartDate() { - final UUID caseId = randomUUID(); - final UUID under18DefendantId = randomUUID(); - final LocalDate wcStartDate = LocalDate.now().plusDays(30); - final LocalDate wcEndDate = wcStartDate.plusWeeks(1); - final String under18Dob = wcStartDate.minusYears(16).toString(); - - final List cases = Arrays.asList( - ListedCase.listedCase() - .withId(caseId) - .withCaseIdentifier(uk.gov.moj.cpp.listing.domain.CaseIdentifier.caseIdentifier().withAuthorityCode("test").build()) - .withIsCivil(Optional.empty()) - .withGroupId(Optional.empty()) - .withIsGroupMember(Optional.empty()) - .withIsGroupMaster(Optional.empty()) - .withDefendants(Arrays.asList( - uk.gov.moj.cpp.listing.domain.Defendant.defendant() - .withId(under18DefendantId) - .withDateOfBirth(of(under18Dob)) - .build() - )) - .build() - ); - - final List events = hearing.listUnscheduled( - hearingId, type, cases, courtCentreId, judiciary, courtRoomId, listingDirections, - jurisdictionType, prosecutorDatesToAvoid, reportingRestrictionReason, null, - LocalDate.now().plusDays(37), courtCentreDefaults, courtApplications, - courtApplicationPartyListingNeeds, estimateMinutes, - of(wcStartDate), of(wcEndDate), of(1), null - ).collect(Collectors.toList()); - - assertThat(events, hasSize(2)); - assertThat(events.get(0), is(CoreMatchers.instanceOf(HearingListed.class))); - assertThat(events.get(1), is(CoreMatchers.instanceOf(CourtListRestricted.class))); - - final CourtListRestricted courtListRestricted = (CourtListRestricted) events.get(1); - assertThat(courtListRestricted.getHearingId(), is(hearingId)); - assertThat(courtListRestricted.getRestrictCourtList(), is(true)); - assertThat(courtListRestricted.getDefendantIds(), hasSize(1)); - assertThat(courtListRestricted.getDefendantIds(), hasItem(under18DefendantId)); - } - - @Test - void shouldNotEmitCourtListRestrictedForAdultDefendantWhenListedUnscheduledWithWeekCommencingStartDate() { - final UUID caseId = randomUUID(); - final UUID adultDefendantId = randomUUID(); - final LocalDate wcStartDate = LocalDate.now().plusDays(30); - final LocalDate wcEndDate = wcStartDate.plusWeeks(1); - final String adultDob = wcStartDate.minusYears(25).toString(); - - final List cases = Arrays.asList( - ListedCase.listedCase() - .withId(caseId) - .withCaseIdentifier(uk.gov.moj.cpp.listing.domain.CaseIdentifier.caseIdentifier().withAuthorityCode("test").build()) - .withIsCivil(Optional.empty()) - .withGroupId(Optional.empty()) - .withIsGroupMember(Optional.empty()) - .withIsGroupMaster(Optional.empty()) - .withDefendants(Arrays.asList( - uk.gov.moj.cpp.listing.domain.Defendant.defendant() - .withId(adultDefendantId) - .withDateOfBirth(of(adultDob)) - .build() - )) - .build() - ); - - final List events = hearing.listUnscheduled( - hearingId, type, cases, courtCentreId, judiciary, courtRoomId, listingDirections, - jurisdictionType, prosecutorDatesToAvoid, reportingRestrictionReason, null, - LocalDate.now().plusDays(37), courtCentreDefaults, courtApplications, - courtApplicationPartyListingNeeds, estimateMinutes, - of(wcStartDate), of(wcEndDate), of(1), null - ).collect(Collectors.toList()); - - assertThat(events, hasSize(1)); - assertThat(events.get(0), is(CoreMatchers.instanceOf(HearingListed.class))); - } - - @Test - void shouldEnrichCourtApplicationSubjectIdsWhenDefendantMasterDefendantIdMatchesSubject() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID masterDefendantId = randomUUID(); - final UUID applicationId = randomUUID(); - final UUID subjectId = randomUUID(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(emptyList()) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withMasterDefendantId(masterDefendantId) - .withOffences(Arrays.asList(Offence.offence().withId(randomUUID()).build())) - .build())) - .build())) - .withCourtApplications(Arrays.asList(uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(applicationId) - .withSubject(ApplicantRespondent.applicantRespondent() - .withId(subjectId) - .withMasterDefendantId(masterDefendantId) - .build()) - .build())) - .build()) - .build()); - - final uk.gov.moj.cpp.listing.domain.RestrictCourtList restrictCourtList = uk.gov.moj.cpp.listing.domain.RestrictCourtList.restrictCourtList() - .withHearingId(hearingId) - .withDefendantIds(Arrays.asList(defendantId)) - .withCaseIds(emptyList()) - .withOffenceIds(emptyList()) - .withCourtApplicationApplicantIds(emptyList()) - .withCourtApplicatonIds(emptyList()) - .withCourtApplicatonRespondentIds(emptyList()) - .withCourtApplicationSubjectIds(emptyList()) - .withRestrictFromCourtList(true) - .build(); - - final List events = hearing.restrictDetailsFromCourt(hearingId, restrictCourtList) - .collect(Collectors.toList()); - - assertThat(events, hasSize(1)); - assertThat(events.get(0), CoreMatchers.instanceOf(CourtListRestricted.class)); - final CourtListRestricted event = (CourtListRestricted) events.get(0); - assertThat(event.getCourtApplicationSubjectIds(), hasSize(1)); - assertThat(event.getCourtApplicationSubjectIds(), hasItem(subjectId)); - assertThat(event.getCourtApplicationApplicantIds(), hasSize(0)); - assertThat(event.getCourtApplicationRespondentIds(), hasSize(0)); - } - - @Test - void shouldEnrichCourtApplicationApplicantIdsWhenDefendantMasterDefendantIdMatchesApplicant() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID masterDefendantId = randomUUID(); - final UUID applicationId = randomUUID(); - final UUID applicantId = randomUUID(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(emptyList()) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withMasterDefendantId(masterDefendantId) - .withOffences(Arrays.asList(Offence.offence().withId(randomUUID()).build())) - .build())) - .build())) - .withCourtApplications(Arrays.asList(uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(applicationId) - .withApplicant(ApplicantRespondent.applicantRespondent() - .withId(applicantId) - .withMasterDefendantId(masterDefendantId) - .build()) - .build())) - .build()) - .build()); - - final uk.gov.moj.cpp.listing.domain.RestrictCourtList restrictCourtList = uk.gov.moj.cpp.listing.domain.RestrictCourtList.restrictCourtList() - .withHearingId(hearingId) - .withDefendantIds(Arrays.asList(defendantId)) - .withCaseIds(emptyList()) - .withOffenceIds(emptyList()) - .withCourtApplicationApplicantIds(emptyList()) - .withCourtApplicatonIds(emptyList()) - .withCourtApplicatonRespondentIds(emptyList()) - .withCourtApplicationSubjectIds(emptyList()) - .withRestrictFromCourtList(true) - .build(); - - final List events = hearing.restrictDetailsFromCourt(hearingId, restrictCourtList) - .collect(Collectors.toList()); - - assertThat(events, hasSize(1)); - assertThat(events.get(0), CoreMatchers.instanceOf(CourtListRestricted.class)); - final CourtListRestricted event = (CourtListRestricted) events.get(0); - assertThat(event.getCourtApplicationApplicantIds(), hasSize(1)); - assertThat(event.getCourtApplicationApplicantIds(), hasItem(applicantId)); - assertThat(event.getCourtApplicationSubjectIds(), hasSize(0)); - assertThat(event.getCourtApplicationRespondentIds(), hasSize(0)); - } - - @Test - void shouldEnrichCourtApplicationRespondentIdsWhenDefendantMasterDefendantIdMatchesRespondent() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID masterDefendantId = randomUUID(); - final UUID applicationId = randomUUID(); - final UUID respondentId = randomUUID(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(emptyList()) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withMasterDefendantId(masterDefendantId) - .withOffences(Arrays.asList(Offence.offence().withId(randomUUID()).build())) - .build())) - .build())) - .withCourtApplications(Arrays.asList(uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(applicationId) - .withRespondents(Arrays.asList(ApplicantRespondent.applicantRespondent() - .withId(respondentId) - .withMasterDefendantId(masterDefendantId) - .build())) - .build())) - .build()) - .build()); - - final uk.gov.moj.cpp.listing.domain.RestrictCourtList restrictCourtList = uk.gov.moj.cpp.listing.domain.RestrictCourtList.restrictCourtList() - .withHearingId(hearingId) - .withDefendantIds(Arrays.asList(defendantId)) - .withCaseIds(emptyList()) - .withOffenceIds(emptyList()) - .withCourtApplicationApplicantIds(emptyList()) - .withCourtApplicatonIds(emptyList()) - .withCourtApplicatonRespondentIds(emptyList()) - .withCourtApplicationSubjectIds(emptyList()) - .withRestrictFromCourtList(true) - .build(); - - final List events = hearing.restrictDetailsFromCourt(hearingId, restrictCourtList) - .collect(Collectors.toList()); - - assertThat(events, hasSize(1)); - assertThat(events.get(0), CoreMatchers.instanceOf(CourtListRestricted.class)); - final CourtListRestricted event = (CourtListRestricted) events.get(0); - assertThat(event.getCourtApplicationRespondentIds(), hasSize(1)); - assertThat(event.getCourtApplicationRespondentIds(), hasItem(respondentId)); - assertThat(event.getCourtApplicationSubjectIds(), hasSize(0)); - assertThat(event.getCourtApplicationApplicantIds(), hasSize(0)); - } - - @Test - void shouldNotEnrichPartyIdsWhenDefendantHasNoMasterDefendantId() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID applicationId = randomUUID(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(emptyList()) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withOffences(Arrays.asList(Offence.offence().withId(randomUUID()).build())) - // no masterDefendantId - .build())) - .build())) - .withCourtApplications(Arrays.asList(uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(applicationId) - .withSubject(ApplicantRespondent.applicantRespondent() - .withId(randomUUID()) - .withMasterDefendantId(randomUUID()) - .build()) - .build())) - .build()) - .build()); - - final uk.gov.moj.cpp.listing.domain.RestrictCourtList restrictCourtList = uk.gov.moj.cpp.listing.domain.RestrictCourtList.restrictCourtList() - .withHearingId(hearingId) - .withDefendantIds(Arrays.asList(defendantId)) - .withCaseIds(emptyList()) - .withOffenceIds(emptyList()) - .withCourtApplicationApplicantIds(emptyList()) - .withCourtApplicatonIds(emptyList()) - .withCourtApplicatonRespondentIds(emptyList()) - .withCourtApplicationSubjectIds(emptyList()) - .withRestrictFromCourtList(true) - .build(); - - final List events = hearing.restrictDetailsFromCourt(hearingId, restrictCourtList) - .collect(Collectors.toList()); - - assertThat(events, hasSize(1)); - final CourtListRestricted event = (CourtListRestricted) events.get(0); - assertThat(event.getCourtApplicationSubjectIds(), hasSize(0)); - assertThat(event.getCourtApplicationApplicantIds(), hasSize(0)); - assertThat(event.getCourtApplicationRespondentIds(), hasSize(0)); - } - - @Test - void shouldNotEnrichPartyIdsWhenNoApplicationPartySharesMasterDefendantId() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID masterDefendantId = randomUUID(); - final UUID applicationId = randomUUID(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(emptyList()) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withMasterDefendantId(masterDefendantId) - .withOffences(Arrays.asList(Offence.offence().withId(randomUUID()).build())) - .build())) - .build())) - .withCourtApplications(Arrays.asList(uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(applicationId) - .withSubject(ApplicantRespondent.applicantRespondent() - .withId(randomUUID()) - .withMasterDefendantId(randomUUID()) // different masterDefendantId - .build()) - .build())) - .build()) - .build()); - - final uk.gov.moj.cpp.listing.domain.RestrictCourtList restrictCourtList = uk.gov.moj.cpp.listing.domain.RestrictCourtList.restrictCourtList() - .withHearingId(hearingId) - .withDefendantIds(Arrays.asList(defendantId)) - .withCaseIds(emptyList()) - .withOffenceIds(emptyList()) - .withCourtApplicationApplicantIds(emptyList()) - .withCourtApplicatonIds(emptyList()) - .withCourtApplicatonRespondentIds(emptyList()) - .withCourtApplicationSubjectIds(emptyList()) - .withRestrictFromCourtList(true) - .build(); - - final List events = hearing.restrictDetailsFromCourt(hearingId, restrictCourtList) - .collect(Collectors.toList()); - - assertThat(events, hasSize(1)); - final CourtListRestricted event = (CourtListRestricted) events.get(0); - assertThat(event.getCourtApplicationSubjectIds(), hasSize(0)); - assertThat(event.getCourtApplicationApplicantIds(), hasSize(0)); - assertThat(event.getCourtApplicationRespondentIds(), hasSize(0)); - } - - @Test - void shouldDeduplicateSubjectIdAlreadyPresentInCommandAndFoundViaMasterDefendantId() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID masterDefendantId = randomUUID(); - final UUID applicationId = randomUUID(); - final UUID subjectId = randomUUID(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(emptyList()) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withMasterDefendantId(masterDefendantId) - .withOffences(Arrays.asList(Offence.offence().withId(randomUUID()).build())) - .build())) - .build())) - .withCourtApplications(Arrays.asList(uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(applicationId) - .withSubject(ApplicantRespondent.applicantRespondent() - .withId(subjectId) - .withMasterDefendantId(masterDefendantId) - .build()) - .build())) - .build()) - .build()); - - final uk.gov.moj.cpp.listing.domain.RestrictCourtList restrictCourtList = uk.gov.moj.cpp.listing.domain.RestrictCourtList.restrictCourtList() - .withHearingId(hearingId) - .withDefendantIds(Arrays.asList(defendantId)) - .withCaseIds(emptyList()) - .withOffenceIds(emptyList()) - .withCourtApplicationApplicantIds(emptyList()) - .withCourtApplicatonIds(emptyList()) - .withCourtApplicatonRespondentIds(emptyList()) - .withCourtApplicationSubjectIds(Arrays.asList(subjectId)) // already present in command - .withRestrictFromCourtList(true) - .build(); - - final List events = hearing.restrictDetailsFromCourt(hearingId, restrictCourtList) - .collect(Collectors.toList()); - - assertThat(events, hasSize(1)); - final CourtListRestricted event = (CourtListRestricted) events.get(0); - assertThat(event.getCourtApplicationSubjectIds(), hasSize(1)); // deduplicated - assertThat(event.getCourtApplicationSubjectIds(), hasItem(subjectId)); - } - - @Test - void shouldReturnEmptyStreamWhenHearingEndDateIsInThePast() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID masterDefendantId = randomUUID(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(emptyList()) - .withStartDate(LocalDate.now().minusDays(2)) - .withEndDate(LocalDate.now().minusDays(1)) // past hearing - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withMasterDefendantId(masterDefendantId) - .withOffences(Arrays.asList(Offence.offence().withId(randomUUID()).build())) - .build())) - .build())) - .build()) - .build()); - - final uk.gov.moj.cpp.listing.domain.RestrictCourtList restrictCourtList = uk.gov.moj.cpp.listing.domain.RestrictCourtList.restrictCourtList() - .withHearingId(hearingId) - .withDefendantIds(Arrays.asList(defendantId)) - .withCaseIds(emptyList()) - .withOffenceIds(emptyList()) - .withCourtApplicationApplicantIds(emptyList()) - .withCourtApplicatonIds(emptyList()) - .withCourtApplicatonRespondentIds(emptyList()) - .withCourtApplicationSubjectIds(emptyList()) - .withRestrictFromCourtList(true) - .build(); - - final List events = hearing.restrictDetailsFromCourt(hearingId, restrictCourtList) - .collect(Collectors.toList()); - - assertThat(events, hasSize(0)); - } - - @Test - void shouldEnrichAllRolesWhenDefendantMasterDefendantIdMatchesPartyInMultipleApplications() { - final UUID caseId = randomUUID(); - final UUID defendantId = randomUUID(); - final UUID masterDefendantId = randomUUID(); - final UUID subjectId = randomUUID(); - final UUID applicantId = randomUUID(); - final UUID respondentId = randomUUID(); - - hearing.apply(HearingListed.hearingListed() - .withHearing(uk.gov.justice.listing.events.Hearing.hearing() - .withId(hearingId) - .withType(uk.gov.justice.listing.events.Type.type().build()) - .withHearingLanguage(HearingLanguage.ENGLISH) - .withJurisdictionType(uk.gov.justice.core.courts.JurisdictionType.MAGISTRATES) - .withHearingDays(emptyList()) - .withListedCases(Arrays.asList(uk.gov.justice.listing.events.ListedCase.listedCase() - .withId(caseId) - .withDefendants(Arrays.asList(Defendant.defendant() - .withId(defendantId) - .withMasterDefendantId(masterDefendantId) - .withOffences(Arrays.asList(Offence.offence().withId(randomUUID()).build())) - .build())) - .build())) - .withCourtApplications(Arrays.asList( - uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(randomUUID()) - .withSubject(ApplicantRespondent.applicantRespondent() - .withId(subjectId) - .withMasterDefendantId(masterDefendantId) - .build()) - .build(), - uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(randomUUID()) - .withApplicant(ApplicantRespondent.applicantRespondent() - .withId(applicantId) - .withMasterDefendantId(masterDefendantId) - .build()) - .build(), - uk.gov.justice.listing.events.CourtApplication.courtApplication() - .withId(randomUUID()) - .withRespondents(Arrays.asList(ApplicantRespondent.applicantRespondent() - .withId(respondentId) - .withMasterDefendantId(masterDefendantId) - .build())) - .build() - )) - .build()) - .build()); - - final uk.gov.moj.cpp.listing.domain.RestrictCourtList restrictCourtList = uk.gov.moj.cpp.listing.domain.RestrictCourtList.restrictCourtList() - .withHearingId(hearingId) - .withDefendantIds(Arrays.asList(defendantId)) - .withCaseIds(emptyList()) - .withOffenceIds(emptyList()) - .withCourtApplicationApplicantIds(emptyList()) - .withCourtApplicatonIds(emptyList()) - .withCourtApplicatonRespondentIds(emptyList()) - .withCourtApplicationSubjectIds(emptyList()) - .withRestrictFromCourtList(true) - .build(); - - final List events = hearing.restrictDetailsFromCourt(hearingId, restrictCourtList) - .collect(Collectors.toList()); - - assertThat(events, hasSize(1)); - final CourtListRestricted event = (CourtListRestricted) events.get(0); - assertThat(event.getCourtApplicationSubjectIds(), hasSize(1)); - assertThat(event.getCourtApplicationSubjectIds(), hasItem(subjectId)); - assertThat(event.getCourtApplicationApplicantIds(), hasSize(1)); - assertThat(event.getCourtApplicationApplicantIds(), hasItem(applicantId)); - assertThat(event.getCourtApplicationRespondentIds(), hasSize(1)); - assertThat(event.getCourtApplicationRespondentIds(), hasItem(respondentId)); - } - } diff --git a/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/HearingDayTest.java b/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/HearingDayTest.java new file mode 100644 index 000000000..8637fb60f --- /dev/null +++ b/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/HearingDayTest.java @@ -0,0 +1,103 @@ +package uk.gov.moj.cpp.listing.domain.aggregate; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; + +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +class HearingDayTest { + + @Test + void isDraftShouldReturnFalseWhenNull() { + final HearingDay hearingDay = HearingDay.hearingDay() + .withHearingDate(LocalDate.now()) + .withDurationMinutes(120) + .build(); + assertThat(hearingDay.isDraft(), is(false)); + } + + @Test + void isDraftShouldReturnTrueWhenSetToTrue() { + final HearingDay hearingDay = HearingDay.hearingDay() + .withHearingDate(LocalDate.now()) + .withDurationMinutes(120) + .withIsDraft(true) + .build(); + assertThat(hearingDay.isDraft(), is(true)); + } + + @Test + void isDraftShouldReturnFalseWhenSetToFalse() { + final HearingDay hearingDay = HearingDay.hearingDay() + .withHearingDate(LocalDate.now()) + .withDurationMinutes(120) + .withIsDraft(false) + .build(); + assertThat(hearingDay.isDraft(), is(false)); + } + + @Test + void isCancelledShouldReturnFalseWhenNull() { + final HearingDay hearingDay = HearingDay.hearingDay() + .withHearingDate(LocalDate.now()) + .withDurationMinutes(120) + .build(); + assertThat(hearingDay.isCancelled(), is(false)); + } + + @Test + void equalsShouldConsiderIsDraft() { + final LocalDate date = LocalDate.now(); + final HearingDay day1 = HearingDay.hearingDay() + .withHearingDate(date).withDurationMinutes(120).withIsDraft(true).build(); + final HearingDay day2 = HearingDay.hearingDay() + .withHearingDate(date).withDurationMinutes(120).withIsDraft(false).build(); + assertThat(day1, is(not(day2))); + } + + @Test + void equalsShouldBeEqualWhenAllFieldsMatch() { + final LocalDate date = LocalDate.now(); + final UUID courtScheduleId = UUID.randomUUID(); + final HearingDay day1 = HearingDay.hearingDay() + .withHearingDate(date).withDurationMinutes(120).withCourtScheduleId(courtScheduleId).withIsDraft(true).build(); + final HearingDay day2 = HearingDay.hearingDay() + .withHearingDate(date).withDurationMinutes(120).withCourtScheduleId(courtScheduleId).withIsDraft(true).build(); + assertThat(day1, is(day2)); + assertThat(day1.hashCode(), is(day2.hashCode())); + } + + @Test + void withValuesFromShouldCopyAllFieldsIncludingIsDraft() { + final UUID courtScheduleId = UUID.randomUUID(); + final UUID courtCentreId = UUID.randomUUID(); + final UUID courtRoomId = UUID.randomUUID(); + final LocalDate date = LocalDate.now(); + final ZonedDateTime start = date.atTime(10, 0).atZone(ZoneOffset.UTC); + final ZonedDateTime end = date.atTime(12, 0).atZone(ZoneOffset.UTC); + + final HearingDay original = HearingDay.hearingDay() + .withCourtScheduleId(courtScheduleId).withDurationMinutes(120).withEndTime(end) + .withHearingDate(date).withSequence(1).withStartTime(start) + .withIsCancelled(false).withCourtCentreId(courtCentreId) + .withCourtRoomId(courtRoomId).withIsDraft(true).build(); + + final HearingDay copy = HearingDay.hearingDay().withValuesFrom(original).build(); + assertThat(copy, is(original)); + assertThat(copy.isDraft(), is(true)); + assertThat(copy.getCourtScheduleId(), is(courtScheduleId)); + } + + @Test + void toStringShouldContainIsDraft() { + final HearingDay hearingDay = HearingDay.hearingDay() + .withHearingDate(LocalDate.now()).withIsDraft(true).build(); + assertThat(hearingDay.toString().contains("isDraft=true"), is(true)); + } +} diff --git a/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/NewDomainToEventConverterTest.java b/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/NewDomainToEventConverterTest.java index b80bc1136..87e00c7cf 100644 --- a/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/NewDomainToEventConverterTest.java +++ b/listing-domain/listing-domain-aggregate/src/test/java/uk/gov/moj/cpp/listing/domain/aggregate/NewDomainToEventConverterTest.java @@ -13,10 +13,10 @@ import static uk.gov.moj.cpp.listing.domain.CourtApplication.courtApplication; import static uk.gov.moj.cpp.listing.domain.CourtApplicationPartyType.PERSON; import static uk.gov.moj.cpp.listing.domain.aggregate.NewDomainToEventConverter.buildCourtApplications; -import static org.junit.Assert.*; +import static uk.gov.moj.cpp.listing.domain.aggregate.NewDomainToEventConverter.convertHearingDaysDomainToEvent; -import uk.gov.justice.listing.events.NewBaseDefendant; import uk.gov.moj.cpp.listing.domain.Address; +import uk.gov.moj.cpp.listing.domain.HearingDay; import uk.gov.moj.cpp.listing.domain.CivilOffence; import uk.gov.moj.cpp.listing.domain.CommittingCourt; import uk.gov.moj.cpp.listing.domain.CourtApplication; @@ -27,11 +27,12 @@ import uk.gov.moj.cpp.listing.domain.ReportingRestriction; import uk.gov.moj.cpp.listing.domain.SeedingHearing; import uk.gov.moj.cpp.listing.domain.StatementOfOffence; -import uk.gov.justice.listing.events.Defendant; -import uk.gov.justice.listing.events.NewBaseDefendant; import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Arrays; +import java.util.List; import java.util.UUID; import org.junit.jupiter.api.Test; @@ -52,11 +53,6 @@ public void shouldBuildCourtApplications() { checkAddress(courtApplication.getRespondents().get(0).getAddress(), courtApplicationBuilt.getRespondents().get(0).getAddress()); assertThat(courtApplicationBuilt.getSubject().getAddress(), is(notNullValue())); checkAddress(courtApplication.getSubject().getAddress(), courtApplicationBuilt.getSubject().getAddress()); - - assertThat(courtApplicationBuilt.getRespondents().get(0).getMasterDefendantId(), is(courtApplication.getRespondents().get(0).getMasterDefendantId().orElse(null))); - assertThat(courtApplicationBuilt.getRespondents().get(0).getDateOfBirth(), is(courtApplication.getRespondents().get(0).getDateOfBirth().orElse(null))); - assertThat(courtApplicationBuilt.getSubject().getMasterDefendantId(), is(courtApplication.getSubject().getMasterDefendantId().orElse(null))); - assertThat(courtApplicationBuilt.getSubject().getDateOfBirth(), is(courtApplication.getSubject().getDateOfBirth().orElse(null))); } @Test @@ -187,7 +183,7 @@ public void shouldConvertOffence() { assertThat(committingCourt.getCourtHouseCode(), is(courtHouseCode)); assertThat(committingCourt.getCourtHouseName(), is(courtHouseName)); assertThat(committingCourt.getCourtHouseShortName(), is(courtHouseShortName)); - // justice listing events CommittingCourt does not expose courtHouseType on the current API +// assertThat(committingCourt.getCourtHouseType(), is(uk.gov.justice.listing.events.CourtHouseType.CROWN)); final uk.gov.justice.listing.events.StatementOfOffence statementOfOffence = eventOffence.getStatementOfOffence(); assertThat(statementOfOffence.getWelshLegislation(), is(welshLegislation)); @@ -289,109 +285,53 @@ public void shouldConvertOffenceWhenOnlyMandatoryFieldsFilled() { } @Test - public void existingIsYouthTrue_newIsNull_retainsTrue() { - final UUID defendantId = randomUUID(); - final UUID masterDefendantId = randomUUID(); - final Defendant existing = buildExistingDefendant(defendantId, "OldFirst", Boolean.TRUE, masterDefendantId); - final NewBaseDefendant update = buildNewBaseDefendant(defendantId, null, null, null); - - final Defendant result = NewDomainToEventConverter.updateEventDefendant(update, existing); - - assertNotNull(result); - assertTrue("isYouth should remain true when existing true and incoming null", Boolean.TRUE.equals(result.getIsYouth())); - assertEquals("First name should be preserved", "OldFirst", result.getFirstName()); - assertEquals("Master id preserved", masterDefendantId, result.getMasterDefendantId()); - } - - @Test - public void existingIsYouthFalse_newIsTrue_becomesTrue() { - final UUID defendantId = randomUUID(); - final Defendant existing = buildExistingDefendant(defendantId, "OldFirst", Boolean.FALSE, null); - final NewBaseDefendant update = buildNewBaseDefendant(defendantId, null, Boolean.TRUE, null); - - final Defendant result = NewDomainToEventConverter.updateEventDefendant(update, existing); - - assertNotNull(result); - assertTrue("isYouth should become true when incoming true", Boolean.TRUE.equals(result.getIsYouth())); - } - - @Test - public void existingIsYouthTrue_newIsFalse_retainsTrue() { - final UUID defendantId = randomUUID(); - final Defendant existing = buildExistingDefendant(defendantId, "OldFirst", Boolean.TRUE, null); - final NewBaseDefendant update = buildNewBaseDefendant(defendantId, null, Boolean.FALSE, null); - - final Defendant result = NewDomainToEventConverter.updateEventDefendant(update, existing); - - assertNotNull(result); - assertTrue("isYouth should remain true even if incoming is false", Boolean.TRUE.equals(result.getIsYouth())); - } - - @Test - public void existingIsYouthFalse_newIsNull_remainsFalse() { - final UUID defendantId = randomUUID(); - final Defendant existing = buildExistingDefendant(defendantId, "OldFirst", Boolean.FALSE, null); - final NewBaseDefendant update = buildNewBaseDefendant(defendantId, null, null, null); + public void shouldMapIsDraftFromDomainToEventHearingDay() { + final UUID courtScheduleId = randomUUID(); + final UUID courtRoomId = randomUUID(); + final UUID courtCentreId = randomUUID(); + final ZonedDateTime startTime = ZonedDateTime.of(2026, 4, 10, 10, 0, 0, 0, ZoneOffset.UTC); + final ZonedDateTime endTime = startTime.plusMinutes(30); + + final HearingDay domainHearingDay = HearingDay.hearingDay() + .withCourtScheduleId(of(courtScheduleId)) + .withCourtRoomId(of(courtRoomId)) + .withCourtCentreId(of(courtCentreId)) + .withHearingDate(startTime.toLocalDate()) + .withStartTime(startTime) + .withEndTime(endTime) + .withDurationMinutes(30) + .withSequence(0) + .withIsCancelled(of(false)) + .withIsDraft(of(true)) + .build(); - final Defendant result = NewDomainToEventConverter.updateEventDefendant(update, existing); + final List eventHearingDays = convertHearingDaysDomainToEvent(List.of(domainHearingDay)); - assertNotNull(result); - assertEquals("isYouth remains false when no incoming value", Boolean.FALSE, result.getIsYouth()); + assertThat(eventHearingDays.size(), is(1)); + assertThat(eventHearingDays.get(0).getIsDraft(), is(true)); + assertThat(eventHearingDays.get(0).getCourtScheduleId(), is(courtScheduleId)); + assertThat(eventHearingDays.get(0).getCourtRoomId(), is(courtRoomId)); + assertThat(eventHearingDays.get(0).getCourtCentreId(), is(courtCentreId)); } @Test - public void firstName_updateWhenProvided_or_preserveWhenNull() { - final UUID defendantId = randomUUID(); - - final Defendant existing = buildExistingDefendant(defendantId, "ExistingFirst", Boolean.FALSE, null); - - // incoming with null firstName -> preserve existing - final NewBaseDefendant updateNull = buildNewBaseDefendant(defendantId, null, null, null); - final Defendant resultPreserve = NewDomainToEventConverter.updateEventDefendant(updateNull, existing); - assertEquals("Existing first name preserved when incoming null", "ExistingFirst", resultPreserve.getFirstName()); - - // incoming with new firstName -> update to new - final NewBaseDefendant updateNew = buildNewBaseDefendant(defendantId, "NewFirst", null, null); - final Defendant resultUpdated = NewDomainToEventConverter.updateEventDefendant(updateNew, existing); - assertEquals("First name updated when incoming non-null", "NewFirst", resultUpdated.getFirstName()); - } + public void shouldMapIsDraftNullWhenNotSetOnDomainHearingDay() { + final ZonedDateTime startTime = ZonedDateTime.of(2026, 4, 10, 10, 0, 0, 0, ZoneOffset.UTC); + + final HearingDay domainHearingDay = HearingDay.hearingDay() + .withHearingDate(startTime.toLocalDate()) + .withStartTime(startTime) + .withEndTime(startTime.plusMinutes(30)) + .withDurationMinutes(30) + .withSequence(0) + .build(); - @Test - public void masterDefendantId_updatesWhenProvided_or_preserveWhenNull() { - final UUID defendantId = randomUUID(); - final UUID masterDefendantId = randomUUID(); - final UUID masterDefendantId1 = randomUUID(); - final Defendant existing = buildExistingDefendant(defendantId, "Name", Boolean.FALSE, masterDefendantId); - - // incoming sets null master id -> preserve existing - final NewBaseDefendant updateNull = buildNewBaseDefendant(defendantId, null, null, null); - final Defendant preserved = NewDomainToEventConverter.updateEventDefendant(updateNull, existing); - assertEquals("Master id preserved when incoming null", masterDefendantId, preserved.getMasterDefendantId()); - - // incoming provides new master id -> update - final NewBaseDefendant updateNew = buildNewBaseDefendant(defendantId, null, null, masterDefendantId1); - final Defendant updated = NewDomainToEventConverter.updateEventDefendant(updateNew, existing); - assertEquals("Master id updated when incoming provided", masterDefendantId1, updated.getMasterDefendantId()); - } + final List eventHearingDays = convertHearingDaysDomainToEvent(List.of(domainHearingDay)); - @Test - public void combination_existingTrue_and_incomingChanges_retainsIsYouthTrue_and_updatesOtherFields() { - final UUID defendantId = randomUUID(); - final UUID masterDefendantId = randomUUID(); - final Defendant existing = buildExistingDefendant(defendantId, "ExistFirst", Boolean.TRUE, masterDefendantId); - final NewBaseDefendant update = buildNewBaseDefendant(defendantId, "UpdatedFirst", Boolean.FALSE, null); - - final Defendant result = NewDomainToEventConverter.updateEventDefendant(update, existing); - - // isYouth should still be true (retained once true) - assertTrue("isYouth retained true", Boolean.TRUE.equals(result.getIsYouth())); - // first name should be updated - assertEquals("firstName updated", "UpdatedFirst", result.getFirstName()); - // master id should remain (incoming null -> preserve) - assertEquals("master id preserved", masterDefendantId, result.getMasterDefendantId()); + assertThat(eventHearingDays.size(), is(1)); + assertThat(eventHearingDays.get(0).getIsDraft(), is((Boolean) null)); } - private CourtApplication createCourtApplication() { return courtApplication() .withApplicationParticulars(of(STRING.next())) @@ -408,8 +348,6 @@ private CourtApplication createCourtApplication() { .build()) .withRespondents(singletonList(courtApplicationParty() .withCourtApplicationPartyType(PERSON) - .withMasterDefendantId(randomUUID()) - .withDateOfBirth("1990-05-15") .withAddress(address() .withAddress1(STRING.next()) .withAddress2(of(STRING.next())) @@ -421,8 +359,6 @@ private CourtApplication createCourtApplication() { .build())) .withSubject(courtApplicationParty() .withCourtApplicationPartyType(PERSON) - .withMasterDefendantId(randomUUID()) - .withDateOfBirth("1985-12-01") .withAddress(address() .withAddress1(STRING.next()) .withAddress2(of(STRING.next())) @@ -448,31 +384,4 @@ private void checkAddress(final Address address, final uk.gov.justice.core.court assertThat(addressBuilt.getPostcode(), is(notNullValue())); assertThat(addressBuilt.getPostcode(), is(address.getPostcode().get())); } - - // Helper: build a NewBaseDefendant (incoming update) with fields we care about - private NewBaseDefendant buildNewBaseDefendant(final UUID id, - final String firstName, - final Boolean isYouth, - final UUID masterDefendantId) { - return NewBaseDefendant.newBaseDefendant() - .withId(id) - .withFirstName(firstName) - .withMasterDefendantId(masterDefendantId) - .withIsYouth(isYouth != null ? isYouth : null) - .build(); - } - - // Helper: build an event Defendant with fields we care about - private Defendant buildExistingDefendant(final UUID id, - final String firstName, - final Boolean isYouth, - final UUID masterDefendantId) { - return Defendant.defendant() - .withId(id) - .withFirstName(firstName) - .withMasterDefendantId(masterDefendantId) - .withIsYouth(isYouth != null ? isYouth : null) - .build(); - } - } \ No newline at end of file diff --git a/listing-domain/listing-domain-aggregate/src/test/resources/domain-features/allocate-hearing.feature b/listing-domain/listing-domain-aggregate/src/test/resources/domain-features/allocate-hearing.feature index ea0aa63a1..dc10a6019 100644 --- a/listing-domain/listing-domain-aggregate/src/test/resources/domain-features/allocate-hearing.feature +++ b/listing-domain/listing-domain-aggregate/src/test/resources/domain-features/allocate-hearing.feature @@ -46,14 +46,3 @@ Feature: Allocate a hearing And hearing days changed for hearing When you applyAllocationRules to a Hearing using a defendants offences Then hearing allocated for listing - - Scenario: When a hearing is allocated with a defendant under 18 at the hearing start date, - the defendant is automatically restricted from the court list. - - Given hearing listed with an under-18 defendant - And court room assigned to hearing - And non default days assigned to hearing - And hearing days changed for hearing - When you applyAllocationRules to a Hearing using a defendants offences - Then hearing allocated for listing - And court list restricted for under-18 defendants diff --git a/listing-domain/listing-domain-aggregate/src/test/resources/domain-features/vacate-trial-hearing.feature b/listing-domain/listing-domain-aggregate/src/test/resources/domain-features/vacate-trial-hearing.feature index 244315a8a..64a6a3bad 100644 --- a/listing-domain/listing-domain-aggregate/src/test/resources/domain-features/vacate-trial-hearing.feature +++ b/listing-domain/listing-domain-aggregate/src/test/resources/domain-features/vacate-trial-hearing.feature @@ -9,11 +9,12 @@ Feature: Vacating a hearing initiated from hearing And hearing gets vacated from listing And available slots for hearing are freed - Scenario: Should not attempt to free any slots when a hearing is vacated in Crown jurisdiction + Scenario: Should vacate the hearing and free allocated slots when vacating reason is provided and jurisdiction is Crown Given hearing listed When you hearingVacateTrial to a Hearing with a vacate reason Then hearing gets vacated from listing + And available slots for hearing are freed Scenario: Should not attempt to free any slots when a hearing is vacated in Magistrates jurisdiction and vacate reason is missing diff --git a/listing-domain/listing-domain-common/pom.xml b/listing-domain/listing-domain-common/pom.xml index 6a403cc0e..0413845af 100644 --- a/listing-domain/listing-domain-common/pom.xml +++ b/listing-domain/listing-domain-common/pom.xml @@ -3,7 +3,7 @@ listing-domain uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtApplicationParty.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtApplicationParty.java index 6c9332405..597bbf0d0 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtApplicationParty.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtApplicationParty.java @@ -18,21 +18,15 @@ public class CourtApplicationParty { private final Address address; - private final UUID masterDefendantId; - - private final String dateOfBirth; - public CourtApplicationParty(final UUID id, final String firstName, final Boolean isRespondent, final String lastName, final CourtApplicationPartyType courtApplicationPartyType, - final Address address, final UUID masterDefendantId, final String dateOfBirth) { + final Address address) { this.id = id; this.firstName = firstName; this.isRespondent = isRespondent; this.lastName = lastName; this.courtApplicationPartyType = courtApplicationPartyType; this.address = address; - this.masterDefendantId = masterDefendantId; - this.dateOfBirth = dateOfBirth; } public Optional getFirstName() { @@ -55,14 +49,6 @@ public Address getAddress() { return address; } - public Optional getMasterDefendantId() { - return Optional.ofNullable(masterDefendantId); - } - - public Optional getDateOfBirth() { - return Optional.ofNullable(dateOfBirth); - } - public static Builder courtApplicationParty() { return new CourtApplicationParty.Builder(); } @@ -85,14 +71,12 @@ public boolean equals(final Object obj) { java.util.Objects.equals(this.firstName, that.firstName) && java.util.Objects.equals(this.isRespondent, that.isRespondent) && java.util.Objects.equals(this.lastName, that.lastName) && - java.util.Objects.equals(this.courtApplicationPartyType, that.courtApplicationPartyType) && - java.util.Objects.equals(this.masterDefendantId, that.masterDefendantId) && - java.util.Objects.equals(this.dateOfBirth, that.dateOfBirth) ; + java.util.Objects.equals(this.courtApplicationPartyType, that.courtApplicationPartyType) ; } @Override public int hashCode() { - return java.util.Objects.hash(id, firstName, isRespondent, lastName, courtApplicationPartyType, masterDefendantId, dateOfBirth); + return java.util.Objects.hash(id, firstName, isRespondent, lastName, courtApplicationPartyType); } @Override @@ -102,10 +86,8 @@ public String toString() { "firstName='" + firstName + "'," + "isRespondent='" + isRespondent + "'," + "lastName='" + lastName + "'," + - "courtApplicationPartyType='" + courtApplicationPartyType + "'," + - "address=" + address + "," + - "masterDefendantId='" + masterDefendantId + "'," + - "dateOfBirth='" + dateOfBirth + "'" + + "courtApplicationPartyType='" + courtApplicationPartyType + "'" + + "address=" + address + "}"; } @@ -123,10 +105,6 @@ public static class Builder { private Address address; - private UUID masterDefendantId; - - private String dateOfBirth; - public Builder withId(final UUID id) { this.id = id; return this; @@ -157,18 +135,8 @@ public Builder withAddress(final Address address) { return this; } - public Builder withMasterDefendantId(final UUID masterDefendantId) { - this.masterDefendantId = masterDefendantId; - return this; - } - - public Builder withDateOfBirth(final String dateOfBirth) { - this.dateOfBirth = dateOfBirth; - return this; - } - public CourtApplicationParty build() { - return new CourtApplicationParty(id, firstName, isRespondent, lastName, courtApplicationPartyType, address, masterDefendantId, dateOfBirth); + return new CourtApplicationParty(id, firstName, isRespondent, lastName, courtApplicationPartyType, address); } } } diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtSchedule.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtSchedule.java index a09a97cd0..f439a152c 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtSchedule.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtSchedule.java @@ -32,6 +32,7 @@ public class CourtSchedule implements Comparable { private String courtHouseId; private boolean slotBased; private boolean allDaySplit; + private boolean isDraft; private Date sessionStartTime; private Date sessionEndTime; private final List judiciaries = new ArrayList(); @@ -237,6 +238,14 @@ public void setAllDaySplit(final boolean allDaySplit) { this.allDaySplit = allDaySplit; } + public boolean isDraft() { + return isDraft; + } + + public void setDraft(final boolean isDraft) { + this.isDraft = isDraft; + } + public Date getSessionStartTime() { return sessionStartTime; } diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtScheduleJudiciary.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtScheduleJudiciary.java index 5c3a88bd7..081ba5cbe 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtScheduleJudiciary.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/CourtScheduleJudiciary.java @@ -1,8 +1,10 @@ package uk.gov.moj.cpp.listing.domain; +import java.util.List; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; @SuppressWarnings("squid:S1067") @JsonInclude(JsonInclude.Include.NON_NULL) public class CourtScheduleJudiciary { @@ -31,6 +33,14 @@ public class CourtScheduleJudiciary { private Boolean isDeputy; + private Integer seqId; + private String titleJudicialPrefix; + private String titleJudicialPrefixWelsh; + private String personId; + private List specialisms; + private String requestedName; + + @JsonProperty("id") public String getJudiciaryId() { return judiciaryId; } @@ -47,6 +57,7 @@ public void setRotaJudiciaryId(final String rotaJudiciaryId) { this.rotaJudiciaryId = rotaJudiciaryId; } + @JsonProperty("titlePrefix") public String getTitle() { return title; } @@ -111,6 +122,7 @@ public void setPosition(final String position) { this.position = position; } + @JsonProperty("isBenchChairman") public Boolean getBenchChairman() { return isBenchChairman; } @@ -119,6 +131,7 @@ public void setBenchChairman(final Boolean benchChairman) { isBenchChairman = benchChairman; } + @JsonProperty("isDeputy") public Boolean getDeputy() { return isDeputy; } @@ -127,6 +140,54 @@ public void setDeputy(final Boolean deputy) { isDeputy = deputy; } + public Integer getSeqId() { + return seqId; + } + + public void setSeqId(final Integer seqId) { + this.seqId = seqId; + } + + public String getTitleJudicialPrefix() { + return titleJudicialPrefix; + } + + public void setTitleJudicialPrefix(final String titleJudicialPrefix) { + this.titleJudicialPrefix = titleJudicialPrefix; + } + + public String getTitleJudicialPrefixWelsh() { + return titleJudicialPrefixWelsh; + } + + public void setTitleJudicialPrefixWelsh(final String titleJudicialPrefixWelsh) { + this.titleJudicialPrefixWelsh = titleJudicialPrefixWelsh; + } + + public String getPersonId() { + return personId; + } + + public void setPersonId(final String personId) { + this.personId = personId; + } + + public List getSpecialisms() { + return specialisms; + } + + public void setSpecialisms(final List specialisms) { + this.specialisms = specialisms; + } + + public String getRequestedName() { + return requestedName; + } + + public void setRequestedName(final String requestedName) { + this.requestedName = requestedName; + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/Defendant.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/Defendant.java index 89723e048..fbcc927fe 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/Defendant.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/Defendant.java @@ -9,11 +9,6 @@ import java.util.Optional; import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -@JsonIgnoreProperties(ignoreUnknown = true) @SuppressWarnings({"squid:S00107", "squid:S00121", "squid:S1067", "squid:S2065", "PMD.BeanMembersShouldSerialize"}) public class Defendant { private Optional bailStatus; @@ -54,25 +49,7 @@ public class Defendant { private final transient Optional
address; @SuppressWarnings({"squid:S00107", "squid:S1067"}) - @JsonCreator - public Defendant(@JsonProperty("bailStatus") final Optional bailStatus, - @JsonProperty("custodyTimeLimit") final Optional custodyTimeLimit, - @JsonProperty("dateOfBirth") final Optional dateOfBirth, - @JsonProperty("datesToAvoid") final Optional datesToAvoid, - @JsonProperty("defenceOrganisation") final Optional defenceOrganisation, - @JsonProperty("firstName") final Optional firstName, - @JsonProperty("hearingLanguageNeeds") final Optional hearingLanguageNeeds, - @JsonProperty("id") final UUID id, - @JsonProperty("masterDefendantId") final Optional masterDefendantId, - @JsonProperty("courtProceedingsInitiated") final Optional courtProceedingsInitiated, - @JsonProperty("lastName") final Optional lastName, - @JsonProperty("prosecutionCaseId") final UUID prosecutionCaseId, - @JsonProperty("offences") final List offences, - @JsonProperty("organisationName") final Optional organisationName, - @JsonProperty("specificRequirements") final Optional specificRequirements, - @JsonProperty("isYouth") final Optional isYouth, - @JsonProperty("nationalityDescription") final Optional nationalityDescription, - @JsonProperty("address") final Optional
address) { + public Defendant(final Optional bailStatus, final Optional custodyTimeLimit, final Optional dateOfBirth, final Optional datesToAvoid, final Optional defenceOrganisation, final Optional firstName, final Optional hearingLanguageNeeds, final UUID id, final Optional masterDefendantId, final Optional courtProceedingsInitiated, final Optional lastName, final UUID prosecutionCaseId, final List offences, final Optional organisationName, final Optional specificRequirements, final Optional isYouth, final Optional nationalityDescription, final Optional
address) { this.bailStatus = bailStatus; this.custodyTimeLimit = custodyTimeLimit; this.dateOfBirth = dateOfBirth; diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/HearingDay.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/HearingDay.java index 35a4357c2..747ae7bbb 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/HearingDay.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/HearingDay.java @@ -31,7 +31,9 @@ public class HearingDay implements Serializable { private final Optional courtRoomId; - public HearingDay(final Integer durationMinutes, final ZonedDateTime endTime, final LocalDate hearingDate, final Integer sequence, final ZonedDateTime startTime, final Optional courtScheduleId, final Optional isCancelled, final Optional courtCentreId, final Optional courtRoomId) { + private final Optional isDraft; + + public HearingDay(final Integer durationMinutes, final ZonedDateTime endTime, final LocalDate hearingDate, final Integer sequence, final ZonedDateTime startTime, final Optional courtScheduleId, final Optional isCancelled, final Optional courtCentreId, final Optional courtRoomId, final Optional isDraft) { this.durationMinutes = durationMinutes; this.endTime = endTime; this.hearingDate = hearingDate; @@ -41,6 +43,7 @@ public HearingDay(final Integer durationMinutes, final ZonedDateTime endTime, fi this.isCancelled = isCancelled; this.courtCentreId = courtCentreId; this.courtRoomId = courtRoomId; + this.isDraft = isDraft; } public Integer getDurationMinutes() { @@ -83,6 +86,10 @@ public Optional getCourtRoomId() { return courtRoomId; } + public Optional getIsDraft() { + return isDraft; + } + @Override public boolean equals(final Object o) { if (this == o) { @@ -100,12 +107,13 @@ public boolean equals(final Object o) { Objects.equals(getStartTime(), that.getStartTime()) && Objects.equals(getIsCancelled(), that.getIsCancelled()) && Objects.equals(getCourtCentreId(), that.getCourtCentreId()) && - Objects.equals(getCourtRoomId(), that.getCourtRoomId()); + Objects.equals(getCourtRoomId(), that.getCourtRoomId()) && + Objects.equals(getIsDraft(), that.getIsDraft()); } @Override public int hashCode() { - return Objects.hash(getCourtScheduleId(), getDurationMinutes(), getEndTime(), getHearingDate(), getSequence(), getStartTime(), getIsCancelled(), getCourtCentreId(), getCourtRoomId()); + return Objects.hash(getCourtScheduleId(), getDurationMinutes(), getEndTime(), getHearingDate(), getSequence(), getStartTime(), getIsCancelled(), getCourtCentreId(), getCourtRoomId(), getIsDraft()); } @Override @@ -120,6 +128,7 @@ public String toString() { ", isCancelled=" + isCancelled + ", courtCentreId=" + courtCentreId + ", courtRoomId=" + courtRoomId + + ", isDraft=" + isDraft + '}'; } @@ -142,6 +151,8 @@ public static class Builder { private Optional courtRoomId = empty(); + private Optional isDraft = empty(); + public Builder withDurationMinutes(final Integer durationMinutes) { this.durationMinutes = durationMinutes; return this; @@ -187,8 +198,13 @@ public Builder withCourtRoomId(final Optional courtRoomId) { return this; } + public Builder withIsDraft(final Optional isDraft) { + this.isDraft = isDraft; + return this; + } + public HearingDay build() { - return new HearingDay(durationMinutes, endTime, hearingDate, sequence, startTime, courtScheduleId, isCancelled, courtCentreId, courtRoomId); + return new HearingDay(durationMinutes, endTime, hearingDate, sequence, startTime, courtScheduleId, isCancelled, courtCentreId, courtRoomId, isDraft); } } } diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/HearingSlotSearchResponse.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/HearingSlotSearchResponse.java index 16da00ca1..af9a7914d 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/HearingSlotSearchResponse.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/HearingSlotSearchResponse.java @@ -8,7 +8,8 @@ public record HearingSlotSearchResponse(String hearingId, String courtRoomId, String sessionStartTime, Integer duration, - List judiciaries) { + List judiciaries, + Boolean isDraft) { @Override public boolean equals(final Object o) { if (this == o) return true; @@ -19,12 +20,13 @@ public boolean equals(final Object o) { that.courtRoomId()) && Objects.equals(this.sessionStartTime(), that.sessionStartTime()) && Objects.equals(this.duration(), that.duration()) && Objects.equals(this.judiciaries(), - that.judiciaries()); + that.judiciaries()) && Objects.equals(this.isDraft(), + that.isDraft()); } @Override public int hashCode() { return Objects.hash(this.hearingId(), this.courtScheduleId(), this.courtRoomId(), this.sessionStartTime(), - this.duration(), this.judiciaries()); + this.duration(), this.judiciaries(), this.isDraft()); } } diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/RestrictCourtList.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/RestrictCourtList.java index e04463752..33b1c7eec 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/RestrictCourtList.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/RestrictCourtList.java @@ -16,8 +16,6 @@ public class RestrictCourtList { private final List courtApplicationRespondentIds; - private final List courtApplicationSubjectIds; - private final List defendantIds; private final UUID hearingId; @@ -28,12 +26,11 @@ public class RestrictCourtList { private final Boolean restrictFromCourtList; - public RestrictCourtList(final List caseIds, final List courtApplicationApplicantIds, final List courtApplicationIds, final List courtApplicationRespondentIds, final List courtApplicationSubjectIds, final List defendantIds, final UUID hearingId, final List offenceIds, final String courtApplicationType, final Boolean restrictFromCourtList) { + public RestrictCourtList(final List caseIds, final List courtApplicationApplicantIds, final List courtApplicationIds, final List courtApplicationRespondentIds, final List defendantIds, final UUID hearingId, final List offenceIds, final String courtApplicationType, final Boolean restrictFromCourtList) { this.caseIds = caseIds; this.courtApplicationApplicantIds = courtApplicationApplicantIds; this.courtApplicationIds = courtApplicationIds; this.courtApplicationRespondentIds = courtApplicationRespondentIds; - this.courtApplicationSubjectIds = courtApplicationSubjectIds; this.defendantIds = defendantIds; this.hearingId = hearingId; this.offenceIds = offenceIds; @@ -77,10 +74,6 @@ public List getCourtApplicationRespondentIds() { return courtApplicationRespondentIds; } - public List getCourtApplicationSubjectIds() { - return courtApplicationSubjectIds; - } - public static Builder restrictCourtList() { return new RestrictCourtList.Builder(); } @@ -100,7 +93,6 @@ public boolean equals(Object o) { .append(courtApplicationApplicantIds, that.courtApplicationApplicantIds) .append(courtApplicationIds, that.courtApplicationIds) .append(courtApplicationRespondentIds, that.courtApplicationRespondentIds) - .append(courtApplicationSubjectIds, that.courtApplicationSubjectIds) .append(defendantIds, that.defendantIds) .append(hearingId, that.hearingId) .append(offenceIds, that.offenceIds) @@ -111,7 +103,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return java.util.Objects.hash(caseIds, courtApplicationApplicantIds, courtApplicationIds, courtApplicationRespondentIds, courtApplicationSubjectIds, defendantIds, hearingId, offenceIds, courtApplicationType,restrictFromCourtList); + return java.util.Objects.hash(caseIds, courtApplicationApplicantIds, courtApplicationIds, courtApplicationRespondentIds, defendantIds, hearingId, offenceIds, courtApplicationType,restrictFromCourtList); } @Override @@ -121,7 +113,6 @@ public String toString() { "courtApplicationApplicantIds='" + courtApplicationApplicantIds + "'," + "courtApplicationIds='" + courtApplicationIds + "'," + "courtApplicationRespondentIds='" + courtApplicationRespondentIds + "'," + - "courtApplicationSubjectIds='" + courtApplicationSubjectIds + "'," + "defendantIds='" + defendantIds + "'," + "hearingId='" + hearingId + "'," + "offenceIds='" + offenceIds + "'," + @@ -138,8 +129,6 @@ public static class Builder { private List courtApplicatonRespondentIds; - private List courtApplicationSubjectIds; - private List defendantsId; private UUID hearingId; @@ -170,11 +159,6 @@ public Builder withCourtApplicatonRespondentIds(final List courtApplicaton return this; } - public Builder withCourtApplicationSubjectIds(final List courtApplicationSubjectIds) { - this.courtApplicationSubjectIds = courtApplicationSubjectIds; - return this; - } - public Builder withDefendantIds(final List defendantsId) { this.defendantsId = defendantsId; return this; @@ -201,7 +185,7 @@ public Builder withCourtApplicationType(final String courtApplicationType) { } public RestrictCourtList build() { - return new RestrictCourtList(caseIds, courtApplicationApplicantIds, courtApplicatonIds, courtApplicatonRespondentIds, courtApplicationSubjectIds, defendantsId, hearingId, offencesId, courtApplicationType, restrictFromCourtList); + return new RestrictCourtList(caseIds, courtApplicationApplicantIds, courtApplicatonIds, courtApplicatonRespondentIds, defendantsId, hearingId, offencesId, courtApplicationType, restrictFromCourtList); } } diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtMapping.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtMapping.java index 94ccb1fc8..a4b85eeeb 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtMapping.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtMapping.java @@ -3,11 +3,6 @@ import java.time.LocalDate; import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -@JsonIgnoreProperties(ignoreUnknown = true) @SuppressWarnings("pmd:BeanMembersShouldSerialize") public class CourtMapping { @@ -35,19 +30,18 @@ public class CourtMapping { private String courtType; - @JsonCreator - public CourtMapping(@JsonProperty("id") final UUID id, - @JsonProperty("oucode") final String oucode, - @JsonProperty("crestCourtId") final String crestCourtId, - @JsonProperty("crestCourtSiteId") final String crestCourtSiteId, - @JsonProperty("crestCourtSiteName") final String crestCourtSiteName, - @JsonProperty("validFrom") final LocalDate validFrom, - @JsonProperty("validTo") final LocalDate validTo, - @JsonProperty("crestCourtName") final String crestCourtName, - @JsonProperty("crestCourtShortName") final String crestCourtShortName, - @JsonProperty("crestCourtFullName") final String crestCourtFullName, - @JsonProperty("crestCourtSiteCode") final String crestCourtSiteCode, - @JsonProperty("courtType") final String courtType) { + public CourtMapping(final UUID id, + final String oucode, + final String crestCourtId, + final String crestCourtSiteId, + final String crestCourtSiteName, + final LocalDate validFrom, + final LocalDate validTo, + final String crestCourtName, + final String crestCourtShortName, + final String crestCourtFullName, + final String crestCourtSiteCode, + final String courtType) { this.id = id; this.oucode = oucode; this.crestCourtId = crestCourtId; diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtMappingsList.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtMappingsList.java index c12c18fb5..2adf1b918 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtMappingsList.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtMappingsList.java @@ -3,14 +3,13 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; public class CourtMappingsList { private List cpXhibitCourtMappings; @JsonCreator - public CourtMappingsList(@JsonProperty("cpXhibitCourtMappings") final List cpXhibitCourtMappings) { + public CourtMappingsList(final List cpXhibitCourtMappings) { this.cpXhibitCourtMappings = cpXhibitCourtMappings; } diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtRoomMapping.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtRoomMapping.java index 9e281510e..4d6e8beab 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtRoomMapping.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtRoomMapping.java @@ -2,11 +2,6 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -@JsonIgnoreProperties(ignoreUnknown = true) @SuppressWarnings("pmd:BeanMembersShouldSerialize") public class CourtRoomMapping { @@ -21,17 +16,7 @@ public class CourtRoomMapping { private String crestCourtRoomName; private UUID crestCourtSiteUUID; - @JsonCreator - public CourtRoomMapping(@JsonProperty("id") final UUID id, - @JsonProperty("courtRoomUUID") final UUID courtRoomUUID, - @JsonProperty("crestCourtSiteName") final String crestCourtSiteName, - @JsonProperty("oucode") final String oucode, - @JsonProperty("courtRoomId") final Integer courtRoomId, - @JsonProperty("crestCourtId") final String crestCourtId, - @JsonProperty("crestCourtSiteId") final String crestCourtSiteId, - @JsonProperty("crestCourtSiteCode") final String crestCourtSiteCode, - @JsonProperty("crestCourtRoomName") final String crestCourtRoomName, - @JsonProperty("crestCourtSiteUUID") final UUID crestCourtSiteUUID) { + public CourtRoomMapping(UUID id, UUID courtRoomUUID, String crestCourtSiteName, String oucode, Integer courtRoomId, String crestCourtId, String crestCourtSiteId, String crestCourtSiteCode, String crestCourtRoomName, UUID crestCourtSiteUUID) { this.id = id; this.courtRoomUUID = courtRoomUUID; this.crestCourtSiteName = crestCourtSiteName; diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtRoomMappingsList.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtRoomMappingsList.java index b62e2ccab..5b3df7dbb 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtRoomMappingsList.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtRoomMappingsList.java @@ -3,14 +3,13 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; public class CourtRoomMappingsList { private List cpXhibitCourtRoomMappings; @JsonCreator - public CourtRoomMappingsList(@JsonProperty("cpXhibitCourtRoomMappings") final List cpXhibitCourtRoomMappings) { + public CourtRoomMappingsList(final List cpXhibitCourtRoomMappings) { this.cpXhibitCourtRoomMappings = cpXhibitCourtRoomMappings; } diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/HearingTypesList.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/HearingTypesList.java index 95e84dc05..6f9891f7c 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/HearingTypesList.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/HearingTypesList.java @@ -3,14 +3,13 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; public class HearingTypesList { private List hearingTypes; @JsonCreator - public HearingTypesList(@JsonProperty("hearingTypes") final List hearingTypes) { + public HearingTypesList(final List hearingTypes) { this.hearingTypes = hearingTypes; } diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/JudiciariesList.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/JudiciariesList.java index 2fe72ac25..514e8d590 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/JudiciariesList.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/JudiciariesList.java @@ -3,14 +3,13 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; public class JudiciariesList { private List judiciaries; @JsonCreator - public JudiciariesList(@JsonProperty("judiciaries") final List judiciaries) { + public JudiciariesList(final List judiciaries) { this.judiciaries = judiciaries; } diff --git a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/OrganisationUnitList.java b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/OrganisationUnitList.java index 150689a8b..68e0bd7b2 100644 --- a/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/OrganisationUnitList.java +++ b/listing-domain/listing-domain-common/src/main/java/uk/gov/moj/cpp/listing/domain/referencedata/OrganisationUnitList.java @@ -3,14 +3,13 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; public class OrganisationUnitList { private List organisationunits; @JsonCreator - public OrganisationUnitList(@JsonProperty("organisationunits") final List organisationunits) { + public OrganisationUnitList(final List organisationunits) { this.organisationunits = organisationunits; } diff --git a/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/CourtScheduleTest.java b/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/CourtScheduleTest.java new file mode 100644 index 000000000..892b388d8 --- /dev/null +++ b/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/CourtScheduleTest.java @@ -0,0 +1,71 @@ +package uk.gov.moj.cpp.listing.domain; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; + +import java.time.LocalDate; +import java.util.Date; + +import org.junit.jupiter.api.Test; + +class CourtScheduleTest { + + @Test + void isDraftShouldDefaultToFalse() { + final CourtSchedule courtSchedule = new CourtSchedule(); + assertThat(courtSchedule.isDraft(), is(false)); + } + + @Test + void shouldSetAndGetIsDraft() { + final CourtSchedule courtSchedule = new CourtSchedule(); + courtSchedule.setDraft(true); + assertThat(courtSchedule.isDraft(), is(true)); + } + + @Test + void shouldGetAndSetJudiciaries() { + final CourtSchedule courtSchedule = new CourtSchedule(); + assertThat(courtSchedule.getJudiciaries().isEmpty(), is(true)); + + final CourtScheduleJudiciary judiciary = new CourtScheduleJudiciary(); + judiciary.setJudiciaryId("judge-1"); + judiciary.setJudiciaryType("CIRCUIT_JUDGE"); + judiciary.setBenchChairman(true); + courtSchedule.getJudiciaries().add(judiciary); + + assertThat(courtSchedule.getJudiciaries().size(), is(1)); + assertThat(courtSchedule.getJudiciaries().get(0).getJudiciaryType(), is("CIRCUIT_JUDGE")); + } + + @Test + void getTotalSessionDurationShouldReturnZeroWhenTimesAreNull() { + final CourtSchedule courtSchedule = new CourtSchedule(); + assertThat(courtSchedule.getTotalSessionDurationInMinutes(), is(0)); + } + + @Test + void getTotalSessionDurationShouldCalculateCorrectly() { + final CourtSchedule courtSchedule = new CourtSchedule(); + courtSchedule.setSessionStartTime(new Date(0)); + courtSchedule.setSessionEndTime(new Date(2 * 60 * 60 * 1000)); + assertThat(courtSchedule.getTotalSessionDurationInMinutes(), is(120)); + } + + @Test + void compareToShouldCompareBySessionDate() { + final CourtSchedule cs1 = new CourtSchedule(); + cs1.setSessionDate(LocalDate.of(2026, 3, 1)); + final CourtSchedule cs2 = new CourtSchedule(); + cs2.setSessionDate(LocalDate.of(2026, 3, 5)); + assertThat(cs1.compareTo(cs2) < 0, is(true)); + } + + @Test + void compareToShouldReturnZeroWhenSessionDateIsNull() { + final CourtSchedule cs1 = new CourtSchedule(); + final CourtSchedule cs2 = new CourtSchedule(); + cs2.setSessionDate(LocalDate.now()); + assertThat(cs1.compareTo(cs2), is(0)); + } +} diff --git a/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/HearingDayTest.java b/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/HearingDayTest.java new file mode 100644 index 000000000..29055f4af --- /dev/null +++ b/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/HearingDayTest.java @@ -0,0 +1,70 @@ +package uk.gov.moj.cpp.listing.domain; + +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; + +import java.time.LocalDate; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +class HearingDayTest { + + @Test + void getIsDraftShouldReturnEmptyByDefault() { + final HearingDay hearingDay = HearingDay.hearingDay() + .withHearingDate(LocalDate.now()) + .withDurationMinutes(120) + .build(); + assertThat(hearingDay.getIsDraft(), is(empty())); + } + + @Test + void getIsDraftShouldReturnValueWhenSet() { + final HearingDay hearingDay = HearingDay.hearingDay() + .withHearingDate(LocalDate.now()) + .withIsDraft(of(true)) + .build(); + assertThat(hearingDay.getIsDraft(), is(of(true))); + } + + @Test + void equalsShouldConsiderIsDraft() { + final LocalDate date = LocalDate.now(); + final HearingDay day1 = HearingDay.hearingDay() + .withHearingDate(date).withDurationMinutes(120).withIsDraft(of(true)).build(); + final HearingDay day2 = HearingDay.hearingDay() + .withHearingDate(date).withDurationMinutes(120).withIsDraft(of(false)).build(); + assertThat(day1, is(not(day2))); + } + + @Test + void equalsShouldBeEqualWhenAllFieldsMatch() { + final LocalDate date = LocalDate.now(); + final UUID courtScheduleId = UUID.randomUUID(); + final HearingDay day1 = HearingDay.hearingDay() + .withHearingDate(date).withDurationMinutes(120).withCourtScheduleId(of(courtScheduleId)).withIsDraft(of(true)).build(); + final HearingDay day2 = HearingDay.hearingDay() + .withHearingDate(date).withDurationMinutes(120).withCourtScheduleId(of(courtScheduleId)).withIsDraft(of(true)).build(); + assertThat(day1, is(day2)); + assertThat(day1.hashCode(), is(day2.hashCode())); + } + + @Test + void getIsCancelledShouldReturnEmptyByDefault() { + final HearingDay hearingDay = HearingDay.hearingDay() + .withHearingDate(LocalDate.now()).build(); + assertThat(hearingDay.getIsCancelled(), is(empty())); + } + + @Test + void toStringShouldContainIsDraft() { + final HearingDay hearingDay = HearingDay.hearingDay() + .withHearingDate(LocalDate.now()).withIsDraft(of(true)).build(); + assertThat(hearingDay.toString().contains("isDraft"), is(true)); + } +} diff --git a/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtMappingsListTest.java b/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtMappingsListTest.java deleted file mode 100644 index 32018f189..000000000 --- a/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtMappingsListTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package uk.gov.moj.cpp.listing.domain.referencedata; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer; - -import org.junit.jupiter.api.Test; - -class CourtMappingsListTest { - - @Test - void shouldDeserializeReferenceDataCourtMappingsPayload() throws Exception { - final String json = """ - { - "cpXhibitCourtMappings": [ - { - "id": "0a43a1dd-9ba7-33d9-a306-554784e5b116", - "oucode": "C04PR00", - "crestCourtId": "448", - "crestCourtSiteId": "448", - "crestCourtSiteName": "PRESTON", - "crestCourtName": "PRESTON", - "crestCourtShortName": "PREST", - "crestCourtFullName": "PRESTON", - "crestCourtSiteCode": "C", - "courtType": "CROWN_COURT" - } - ] - } - """; - - final CourtMappingsList courtMappingsList = new ObjectMapperProducer().objectMapper() - .readValue(json, CourtMappingsList.class); - - assertNotNull(courtMappingsList); - assertEquals(1, courtMappingsList.getCpXhibitCourtMappings().size()); - assertEquals("CROWN_COURT", courtMappingsList.getCpXhibitCourtMappings().get(0).getCourtType()); - } -} diff --git a/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtRoomMappingsListTest.java b/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtRoomMappingsListTest.java deleted file mode 100644 index 7d701d6bf..000000000 --- a/listing-domain/listing-domain-common/src/test/java/uk/gov/moj/cpp/listing/domain/referencedata/CourtRoomMappingsListTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package uk.gov.moj.cpp.listing.domain.referencedata; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer; - -import org.junit.jupiter.api.Test; - -class CourtRoomMappingsListTest { - - @Test - void shouldDeserializeReferenceDataCourtRoomMappingsPayload() throws Exception { - final String json = """ - { - "cpXhibitCourtRoomMappings": [ - { - "id": "1d0199f8-8812-48a2-b13c-837e1c03ff19", - "oucode": "C14DN00", - "courtRoomId": 852, - "crestCourtId": "420", - "crestCourtSiteId": "420", - "crestCourtSiteCode": "C", - "crestCourtRoomName": "Court 2", - "courtType": "CROWN_COURT", - "courtRoomUUID": "46b45533-375f-36c0-83e9-8b806855077b", - "crestCourtSiteName": "DONCASTER", - "crestCourtFullName": "DONCASTER", - "crestCourtShortName": "DONCA", - "crestCourtSiteUUID": "6c5f4e7f-a684-33e9-8ef9-7493e033ea87" - } - ] - } - """; - - final CourtRoomMappingsList courtRoomMappingsList = new ObjectMapperProducer().objectMapper() - .readValue(json, CourtRoomMappingsList.class); - - assertNotNull(courtRoomMappingsList); - assertEquals(1, courtRoomMappingsList.getCpXhibitCourtRoomMappings().size()); - assertEquals("Court 2", courtRoomMappingsList.getCpXhibitCourtRoomMappings().get(0).getCrestCourtRoomName()); - assertEquals("46b45533-375f-36c0-83e9-8b806855077b", - courtRoomMappingsList.getCpXhibitCourtRoomMappings().get(0).getCourtRoomUUID().toString()); - } -} diff --git a/listing-domain/listing-domain-event/pom.xml b/listing-domain/listing-domain-event/pom.xml index e99b9fb8c..106d01c39 100644 --- a/listing-domain/listing-domain-event/pom.xml +++ b/listing-domain/listing-domain-event/pom.xml @@ -3,7 +3,7 @@ listing-domain uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-domain/listing-domain-xhibit/pom.xml b/listing-domain/listing-domain-xhibit/pom.xml index b8ab79f95..883e0f029 100644 --- a/listing-domain/listing-domain-xhibit/pom.xml +++ b/listing-domain/listing-domain-xhibit/pom.xml @@ -3,7 +3,7 @@ listing-domain uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-domain/pom.xml b/listing-domain/pom.xml index 7822163b6..0d72682e5 100644 --- a/listing-domain/pom.xml +++ b/listing-domain/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.listing listing-parent - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT listing-domain diff --git a/listing-event-sources/pom.xml b/listing-event-sources/pom.xml index 0933bf76a..2301e5aef 100644 --- a/listing-event-sources/pom.xml +++ b/listing-event-sources/pom.xml @@ -3,7 +3,7 @@ listing-parent uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-event/listing-event-listener/pom.xml b/listing-event/listing-event-listener/pom.xml index 8cab6d664..3ba83b0f6 100644 --- a/listing-event/listing-event-listener/pom.xml +++ b/listing-event/listing-event-listener/pom.xml @@ -3,7 +3,7 @@ listing-event uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-event/listing-event-listener/src/main/java/uk/gov/moj/cpp/listing/event/listener/RestrictCourtListEventListener.java b/listing-event/listing-event-listener/src/main/java/uk/gov/moj/cpp/listing/event/listener/RestrictCourtListEventListener.java index 33137a67c..df77ca0c2 100644 --- a/listing-event/listing-event-listener/src/main/java/uk/gov/moj/cpp/listing/event/listener/RestrictCourtListEventListener.java +++ b/listing-event/listing-event-listener/src/main/java/uk/gov/moj/cpp/listing/event/listener/RestrictCourtListEventListener.java @@ -88,10 +88,6 @@ public void hearingRestrictionForCourt(final Envelope event courtApplicationRespondentIdToBeRestricted -> jsonNodeUpdater.putSubList(COURT_APPLICATIONS_FIELD, typeRefCourtApplication, getCourtApplicationRespondentsFunction(courtApplicationRespondentIdToBeRestricted, restrictDetailsFromCourt)).save() ); - ofNullable(restrictCourtList.getCourtApplicationSubjectIds()).orElse(newArrayList()).forEach( - courtApplicationSubjectIdToBeRestricted -> jsonNodeUpdater.putSubList(COURT_APPLICATIONS_FIELD, typeRefCourtApplication, - getCourtApplicationSubjectFunction(courtApplicationSubjectIdToBeRestricted, restrictDetailsFromCourt)).save() - ); } private Function, List> getCasesFunction(UUID casesId, Boolean restrictDetailsFromCourt) { @@ -193,7 +189,6 @@ private List getAndRestrictCourtApplication(UUID courtApplicat final CourtApplication newCourtApplication = courtApplication() .withApplicant(courtApplication.getApplicant()) .withRespondents(courtApplication.getRespondents()) - .withSubject(courtApplication.getSubject()) .withApplicationType(courtApplication.getApplicationType()) .withId(courtApplication.getId()) .withParentApplicationId(courtApplication.getParentApplicationId()) @@ -222,13 +217,10 @@ private List getAndRestrictCourtApplicationApplicant(UUID cour .withRestrictFromCourtList(restrictDetailsFromCourt) .withCourtApplicationPartyType(applicantRespondent.getCourtApplicationPartyType()) .withAddress(applicantRespondent.getAddress()) - .withDateOfBirth(applicantRespondent.getDateOfBirth()) - .withMasterDefendantId(applicantRespondent.getMasterDefendantId()) .build(); final CourtApplication newCourtApplication = courtApplication() .withApplicant(newApplicantRespondent) .withRespondents(courtApplication.getRespondents()) - .withSubject(courtApplication.getSubject()) .withApplicationType(courtApplication.getApplicationType()) .withId(courtApplication.getId()) .withParentApplicationId(courtApplication.getParentApplicationId()) @@ -242,15 +234,15 @@ private List getAndRestrictCourtApplicationApplicant(UUID cour return courtApplications; } - private Function, List> getCourtApplicationRespondentsFunction(UUID courtApplicationRespondentId, Boolean restrictDetailsFromCourt) { - return courtApplications -> getAndRestrictCourtApplicationRespondents(courtApplicationRespondentId, courtApplications, restrictDetailsFromCourt); + private Function, List> getCourtApplicationRespondentsFunction(UUID courtApplicationApplicantRespondentId, Boolean restrictDetailsFromCourt) { + return courtApplications -> getAndRestrictCourtApplicationRespondents(courtApplicationApplicantRespondentId, courtApplications, restrictDetailsFromCourt); } - private List getAndRestrictCourtApplicationRespondents(UUID courtApplicationRespondentId, List courtApplications, Boolean restrictDetailsFromCourt) { + private List getAndRestrictCourtApplicationRespondents(UUID courtApplicationApplicantRespondentId, List courtApplications, Boolean restrictDetailsFromCourt) { final CourtApplication courtApplication = Iterables.find(courtApplications, ca -> ca.getRespondents().stream() - .anyMatch(res -> courtApplicationRespondentId.equals(res.getId()))); + .anyMatch(res -> res.getId().equals(courtApplicationApplicantRespondentId))); final List respondents = courtApplication.getRespondents(); - final ApplicantRespondent applicantRespondent = Iterables.find(respondents, res -> courtApplicationRespondentId.equals(res.getId())); + final ApplicantRespondent applicantRespondent = Iterables.find(respondents, res -> res.getId().equals(courtApplicationApplicantRespondentId)); final ApplicantRespondent newApplicantRespondent = applicantRespondent() .withId(applicantRespondent.getId()) .withFirstName(applicantRespondent.getFirstName()) @@ -259,45 +251,8 @@ private List getAndRestrictCourtApplicationRespondents(UUID co .withRestrictFromCourtList(restrictDetailsFromCourt) .withCourtApplicationPartyType(applicantRespondent.getCourtApplicationPartyType()) .withAddress(applicantRespondent.getAddress()) - .withDateOfBirth(applicantRespondent.getDateOfBirth()) - .withMasterDefendantId(applicantRespondent.getMasterDefendantId()) - .build(); - respondents.replaceAll(res -> courtApplicationRespondentId.equals(res.getId()) ? newApplicantRespondent : res); - return courtApplications; - } - - private Function, List> getCourtApplicationSubjectFunction(UUID courtApplicationSubjectId, Boolean restrictDetailsFromCourt) { - return courtApplications -> getAndRestrictCourtApplicationSubject(courtApplicationSubjectId, courtApplications, restrictDetailsFromCourt); - } - - private List getAndRestrictCourtApplicationSubject(UUID courtApplicationSubjectId, List courtApplications, Boolean restrictDetailsFromCourt) { - final CourtApplication courtApplication = Iterables.find(courtApplications, ca -> nonNull(ca.getSubject()) && courtApplicationSubjectId.equals(ca.getSubject().getId())); - final ApplicantRespondent subject = courtApplication.getSubject(); - final ApplicantRespondent newSubject = applicantRespondent() - .withId(subject.getId()) - .withFirstName(subject.getFirstName()) - .withLastName(subject.getLastName()) - .withIsRespondent(subject.getIsRespondent()) - .withRestrictFromCourtList(restrictDetailsFromCourt) - .withCourtApplicationPartyType(subject.getCourtApplicationPartyType()) - .withAddress(subject.getAddress()) - .withDateOfBirth(subject.getDateOfBirth()) - .withMasterDefendantId(subject.getMasterDefendantId()) - .build(); - final CourtApplication newCourtApplication = courtApplication() - .withApplicant(courtApplication.getApplicant()) - .withRespondents(courtApplication.getRespondents()) - .withSubject(newSubject) - .withApplicationType(courtApplication.getApplicationType()) - .withId(courtApplication.getId()) - .withParentApplicationId(courtApplication.getParentApplicationId()) - .withLinkedCaseIds(courtApplication.getLinkedCaseIds()) - .withRestrictFromCourtList(courtApplication.getRestrictFromCourtList()) - .withRestrictCourtApplicationType(courtApplication.getRestrictCourtApplicationType()) - .withApplicationReference(courtApplication.getApplicationReference()) - .withApplicationParticulars(courtApplication.getApplicationParticulars()) .build(); - courtApplications.replaceAll(ca -> nonNull(ca.getSubject()) && courtApplicationSubjectId.equals(ca.getSubject().getId()) ? newCourtApplication : ca); + respondents.replaceAll(res -> res.getId().equals(courtApplicationApplicantRespondentId) ? newApplicantRespondent : res); return courtApplications; } @@ -313,7 +268,6 @@ private List getAndRestrictCourtApplicationType(UUID courtAppl final CourtApplication newCourtApplication = courtApplication() .withApplicant(courtApplication.getApplicant()) .withRespondents(courtApplication.getRespondents()) - .withSubject(courtApplication.getSubject()) .withApplicationType(courtApplication.getApplicationType()) .withId(courtApplication.getId()) .withParentApplicationId(courtApplication.getParentApplicationId()) diff --git a/listing-event/listing-event-listener/src/test/java/uk/gov/moj/cpp/listing/event/listener/RestrictCourtListEventListenerTest.java b/listing-event/listing-event-listener/src/test/java/uk/gov/moj/cpp/listing/event/listener/RestrictCourtListEventListenerTest.java index b463d5595..eb9837317 100644 --- a/listing-event/listing-event-listener/src/test/java/uk/gov/moj/cpp/listing/event/listener/RestrictCourtListEventListenerTest.java +++ b/listing-event/listing-event-listener/src/test/java/uk/gov/moj/cpp/listing/event/listener/RestrictCourtListEventListenerTest.java @@ -65,10 +65,6 @@ public class RestrictCourtListEventListenerTest { private static final UUID APPLICANT_ID = randomUUID(); private static final UUID RESPONDENT_ID_1 = randomUUID(); private static final UUID RESPONDENT_ID_2 = randomUUID(); - private static final UUID RESPONDENT_MASTER_DEFENDANT_ID_1 = randomUUID(); - private static final UUID RESPONDENT_MASTER_DEFENDANT_ID_2 = randomUUID(); - private static final UUID SUBJECT_ID = randomUUID(); - private static final UUID SUBJECT_MASTER_DEFENDANT_ID = randomUUID(); private static final String EVENT_NAME = "listing.events.court-list-restricted"; private static final Address APPLICANT_ADDRESS = Address @@ -194,35 +190,6 @@ public void shouldRestrictByAppropriateRespondentForStandAloneApplications() thr verify(hearingRepository, times(4)).save(hearing); } - @Test - public void shouldRestrictSubjectByIdForStandAloneApplications() throws IOException { - final List courtApplications = createCourtApplicationsWithSubject(); - final String courtApplicationsAsString = MAPPER.writeValueAsString(courtApplications); - final JsonNode courtApplicationsProperties = MAPPER.readTree(courtApplicationsAsString); - - final CourtListRestricted payload = courtListRestricted() - .withHearingId(HEARING_ID) - .withRestrictCourtList(TRUE) - .withCourtApplicationSubjectIds(singletonList(SUBJECT_ID)) - .build(); - final Envelope restrictCourtListEnvelope = envelopeFrom(metadataWithRandomUUID(EVENT_NAME), payload); - - given(hearingRepository.findBy(HEARING_ID)).willReturn(hearing); - given(hearing.getProperties()).willReturn(properties); - given(properties.get(COURT_APPLICATIONS_FIELD)).willReturn(courtApplicationsProperties); - - target.hearingRestrictionForCourt(restrictCourtListEnvelope); - - verify(properties).replace(any(), objectNodeCaptor.capture()); - final ArrayNode applicationArrayNode = objectNodeCaptor.getValue(); - assertThat(applicationArrayNode.toString(), isJson(allOf( - withJsonPath("$[0].subject.id", equalTo(SUBJECT_ID.toString())), - withJsonPath("$[0].subject.masterDefendantId", equalTo(SUBJECT_MASTER_DEFENDANT_ID.toString())), - withJsonPath("$[0].subject.restrictFromCourtList", equalTo(true)) - ))); - verify(hearingRepository).save(hearing); - } - private Collection> getCourtApplicationMatchers(final CourtApplication courtApplication) { return newArrayList( withJsonPath("$[0].id", equalTo(courtApplication.getId().toString())), @@ -344,40 +311,6 @@ private List createCourtApplicat .build()); } - private List createCourtApplicationsWithSubject() { - return singletonList(courtApplication() - .withId(COURT_APPLICATIONS_ID) - .withApplicationType(COURT_APPLICATION_TYPE) - .withApplicationParticulars(APPLICATION_PARTICULARS) - .withApplicant(applicantRespondent() - .withFirstName(STRING.next()) - .withLastName(STRING.next()) - .withIsRespondent(false) - .withId(APPLICANT_ID) - .withAddress(APPLICANT_ADDRESS) - .withRestrictFromCourtList(FALSE) - .build()) - .withSubject(applicantRespondent() - .withFirstName(STRING.next()) - .withLastName(STRING.next()) - .withIsRespondent(false) - .withId(SUBJECT_ID) - .withMasterDefendantId(SUBJECT_MASTER_DEFENDANT_ID) - .withAddress(RESPONDENT_ADDRESS) - .withRestrictFromCourtList(FALSE) - .build()) - .withRespondents(singletonList(applicantRespondent() - .withFirstName(STRING.next()) - .withLastName(STRING.next()) - .withIsRespondent(true) - .withId(RESPONDENT_ID_1) - .withMasterDefendantId(RESPONDENT_MASTER_DEFENDANT_ID_1) - .withAddress(RESPONDENT_ADDRESS) - .withRestrictFromCourtList(FALSE) - .build())) - .build()); - } - private List createCourtApplicationsWithMultipleRespondents() { return singletonList(courtApplication() .withId(COURT_APPLICATIONS_ID) @@ -397,7 +330,6 @@ private List createCourtApplicat .withLastName(STRING.next()) .withIsRespondent(true) .withId(RESPONDENT_ID_1) - .withMasterDefendantId(RESPONDENT_MASTER_DEFENDANT_ID_1) .withAddress(RESPONDENT_ADDRESS) .withRestrictFromCourtList(FALSE) .build(), @@ -406,7 +338,6 @@ private List createCourtApplicat .withLastName(STRING.next()) .withIsRespondent(true) .withId(RESPONDENT_ID_2) - .withMasterDefendantId(RESPONDENT_MASTER_DEFENDANT_ID_2) .withAddress(RESPONDENT_ADDRESS) .withRestrictFromCourtList(FALSE) .build() diff --git a/listing-event/listing-event-listener/src/yaml/json/schema/listing.events.court-list-restricted.json b/listing-event/listing-event-listener/src/yaml/json/schema/listing.events.court-list-restricted.json index 145abbd73..a80e23481 100644 --- a/listing-event/listing-event-listener/src/yaml/json/schema/listing.events.court-list-restricted.json +++ b/listing-event/listing-event-listener/src/yaml/json/schema/listing.events.court-list-restricted.json @@ -40,13 +40,6 @@ "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" } }, - "courtApplicationSubjectIds": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" - } - }, "courtApplicationIds": { "type": "array", "minItems": 1, @@ -97,11 +90,6 @@ "required": [ "courtApplicationRespondentIds" ] - }, - { - "required": [ - "courtApplicationSubjectIds" - ] } ], "required": [ diff --git a/listing-event/listing-event-processor/pom.xml b/listing-event/listing-event-processor/pom.xml index 59edf0d36..f4af22807 100644 --- a/listing-event/listing-event-processor/pom.xml +++ b/listing-event/listing-event-processor/pom.xml @@ -3,7 +3,7 @@ listing-event uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-event/listing-event-processor/src/main/java/uk/gov/moj/cpp/listing/event/processor/service/ReferenceDataService.java b/listing-event/listing-event-processor/src/main/java/uk/gov/moj/cpp/listing/event/processor/service/ReferenceDataService.java index 4798c0fc4..f5ae4e5e0 100644 --- a/listing-event/listing-event-processor/src/main/java/uk/gov/moj/cpp/listing/event/processor/service/ReferenceDataService.java +++ b/listing-event/listing-event-processor/src/main/java/uk/gov/moj/cpp/listing/event/processor/service/ReferenceDataService.java @@ -44,7 +44,11 @@ public class ReferenceDataService { private Requester requester; public OrganisationUnit getOrganizationUnitById(final UUID courtCentreId, final JsonEnvelope event) { - return getOrganizationUnitByIdWithAdmin(courtCentreId, event); + final JsonObject payload = createObjectBuilder().add("id", courtCentreId.toString()).build(); + final JsonEnvelope request = enveloper.withMetadataFrom(event, REFERENCEDATA_QUERY_ORGANISATION_UNIT).apply(payload); + JsonEnvelope response = requester.request(request); + LOGGER.debug("'referencedata.query.organisation-unit' response with payload {}", response.payloadAsJsonObject()); + return jsonObjectConverter.convert(response.payloadAsJsonObject(), OrganisationUnit.class); } public OrganisationUnit getOrganizationUnitByIdWithAdmin(final UUID courtCentreId, final JsonEnvelope event) { diff --git a/listing-event/listing-event-processor/src/main/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/mapper/CourtServicesMapper.java b/listing-event/listing-event-processor/src/main/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/mapper/CourtServicesMapper.java index ccf39ff57..1caabd19a 100644 --- a/listing-event/listing-event-processor/src/main/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/mapper/CourtServicesMapper.java +++ b/listing-event/listing-event-processor/src/main/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/mapper/CourtServicesMapper.java @@ -109,9 +109,6 @@ public class CourtServicesMapper { private static final String DEFENDANTS = "defendants"; private static final String COMMA = ", "; public static final String SUBJECT = "subject"; - public static final String COURT_HOUSE_TYPE = "courtHouseType"; - private static final String MAGISTRATES_COURT_TYPE = "MAGISTRATES_COURT"; - private final RequestedNameMapper judicialRequestedName = new RequestedNameMapper(); @@ -385,21 +382,7 @@ private CourtHouseStructure extractCommittingCourtFromOffence(final JsonObject h final CourtHouseStructure courtHouseStructure = objectFactory.createCourtHouseStructure(); - final String courtHouseType = committingCourt.containsKey(COURT_HOUSE_TYPE) && !committingCourt.isNull(COURT_HOUSE_TYPE) - ? committingCourt.getString(COURT_HOUSE_TYPE) - : null; - // DailyList.xsd CourtHouseCodeType is [0-9]{3,4} — use CREST site id, not the single-letter courtSiteCode - final CourtLocation courtLocation = threadLocalCommonXhibitReferenceDataService.get() - .getCriminalCourtDetails(fromString(committingCourt.getString("courtCentreId")), courtHouseType); - - String crestCourtId; - - if (courtLocation.getCourtType().equalsIgnoreCase(MAGISTRATES_COURT_TYPE)) { - crestCourtId = courtLocation.getCourtSiteCode(); - - } else { - crestCourtId = courtLocation.getCrestCourtId(); - } + final String crestCourtId = threadLocalCommonXhibitReferenceDataService.get().getMagsCourtDetails(fromString(committingCourt.getString("courtCentreId"))).getCourtSiteCode(); courtHouseStructure.setCourtHouseType(CourtType.MAGISTRATES_COURT); courtHouseStructure.setCourtHouseCode(generateCourtHouseCode(crestCourtId)); diff --git a/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/ListingEventProcessorTest.java b/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/ListingEventProcessorTest.java index faf33dfda..c77b96ffb 100644 --- a/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/ListingEventProcessorTest.java +++ b/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/ListingEventProcessorTest.java @@ -2917,9 +2917,9 @@ private List prepareRotaSLJudiciaryI .map(JsonObject.class::cast) .forEach(judiciaryJsonObject -> judicialRoles.add(uk.gov.moj.cpp.listing.domain.JudicialRole.judicialRole() - .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("benchChairman"))) - .withIsDeputy(of(judiciaryJsonObject.getBoolean("deputy"))) - .withJudicialId(UUID.fromString(judiciaryJsonObject.getString("judiciaryId"))) + .withIsBenchChairman(of(judiciaryJsonObject.getBoolean("isBenchChairman"))) + .withIsDeputy(of(judiciaryJsonObject.getBoolean("isDeputy"))) + .withJudicialId(UUID.fromString(judiciaryJsonObject.getString("id"))) .withJudicialRoleType( uk.gov.moj.cpp.listing.domain.JudicialRoleType.judicialRoleType() .withJudiciaryType(judiciaryJsonObject.getString("judiciaryType")) diff --git a/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/service/ReferenceDataServiceTest.java b/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/service/ReferenceDataServiceTest.java index 39cfe43a2..6cc571a23 100644 --- a/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/service/ReferenceDataServiceTest.java +++ b/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/service/ReferenceDataServiceTest.java @@ -22,7 +22,6 @@ import uk.gov.justice.services.messaging.Envelope; import uk.gov.justice.services.messaging.JsonEnvelope; import uk.gov.justice.services.test.utils.core.random.RandomGenerator; -import uk.gov.moj.cpp.listing.common.xhibit.exception.InvalidReferenceDataException; import java.time.LocalDate; import java.time.LocalTime; @@ -36,7 +35,6 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.function.Executable; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; @@ -88,10 +86,17 @@ public void shouldGetOrganizationUnitById() { final UUID courtCentreId = randomUUID(); final UUID orgId = randomUUID(); + final JsonObject jsonObject = JsonObjects.createObjectBuilder() + .add("id", orgId.toString()) + .add("address1", orgId.toString()) + .build(); + when(enveloper.withMetadataFrom(eq(event), eq("referencedata.query.organisation-unit"))) .thenReturn(function); - when(requester.requestAsAdmin(any(), eq(OrganisationUnit.class))).thenReturn(organisationUnitEnvelope); - when(organisationUnitEnvelope.payload()).thenReturn(organisationUnit().withId(orgId.toString()).withAddress1("address1").build()); + final JsonEnvelope jsonEnvelope = envelopeFrom(metadataWithRandomUUID(REFERENCE_DATA_GET_COURTROOM), jsonObject); + when(requester.request(any())).thenReturn(jsonEnvelope); + when(jsonObjectConverter.convert(any(), eq(OrganisationUnit.class))) + .thenReturn(organisationUnit().withId(orgId.toString()).withAddress1("address1").build()); final OrganisationUnit result = referenceDataService.getOrganizationUnitById(courtCentreId, event); @@ -99,21 +104,6 @@ public void shouldGetOrganizationUnitById() { assertThat(result.getAddress1(), is("address1")); } - @Test - public void shouldThrowWhenOrganizationUnitNotFound() { - final JsonEnvelope event = hearingAllocatedEvent(); - final UUID courtCentreId = randomUUID(); - - when(enveloper.withMetadataFrom(eq(event), eq("referencedata.query.organisation-unit"))) - .thenReturn(function); - when(requester.requestAsAdmin(any(), eq(OrganisationUnit.class))).thenReturn(organisationUnitEnvelope); - when(organisationUnitEnvelope.payload()).thenReturn(null); - - final Executable executable = () -> referenceDataService.getOrganizationUnitById(courtCentreId, event); - - org.junit.jupiter.api.Assertions.assertThrows(InvalidReferenceDataException.class, executable); - } - @Test public void getOrganizationUnitByIdWithAdmin() { final JsonEnvelope event = hearingAllocatedEvent(); diff --git a/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/CourtListFileGeneratorTest.java b/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/CourtListFileGeneratorTest.java index a5cbb676c..807be1fea 100644 --- a/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/CourtListFileGeneratorTest.java +++ b/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/CourtListFileGeneratorTest.java @@ -5,7 +5,6 @@ import static java.util.UUID.fromString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.when; import static org.mockito.quality.Strictness.LENIENT; import static uk.gov.moj.cpp.listing.domain.xhibit.PublishCourtListType.DRAFT; @@ -156,8 +155,8 @@ private void mockDataSources(final PublishCourtListType publishCourtListType, fi when(commonXhibitReferenceDataService.getCrownCourtDetails(courtCentreId1)).thenReturn(courtLocation1); when(commonXhibitReferenceDataService.getCrownCourtDetails(courtCentreId2)).thenReturn(courtLocation2); - when(commonXhibitReferenceDataService.getCriminalCourtDetails(eq(courtCentreId1), nullable(String.class))).thenReturn(courtLocation1); - when(commonXhibitReferenceDataService.getCriminalCourtDetails(eq(courtCentreId2), nullable(String.class))).thenReturn(courtLocation2); + when(commonXhibitReferenceDataService.getMagsCourtDetails(courtCentreId1)).thenReturn(courtLocation1); + when(commonXhibitReferenceDataService.getMagsCourtDetails(courtCentreId2)).thenReturn(courtLocation2); when(commonXhibitReferenceDataService.getCrownCourtCentreIdsForCrestId(crestCourtId)).thenReturn(courtCentreIds); when(commonXhibitReferenceDataService.getCourtRoomNumber(courtCentreId1, UUID.fromString("7cb09222-49e1-3622-a5a6-ad253d2b3c39"))).thenReturn(10); when(commonXhibitReferenceDataService.getCourtRoomNumber(courtCentreId1, UUID.fromString("6508af42-e4d4-396d-a752-d676ebd38f6d"))).thenReturn(20); diff --git a/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/mapper/CourtServicesMapperTest.java b/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/mapper/CourtServicesMapperTest.java index 47b8fc5f5..97b791325 100644 --- a/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/mapper/CourtServicesMapperTest.java +++ b/listing-event/listing-event-processor/src/test/java/uk/gov/moj/cpp/listing/event/processor/xhibit/courtlist/mapper/CourtServicesMapperTest.java @@ -6,7 +6,6 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.when; import static uk.gov.moj.cpp.listing.event.processor.xhibit.courtlist.PublishCourtListRequestParametersBuilder.withDefaults; import static uk.gov.moj.cpp.listing.event.utils.FileUtil.givenPayload; @@ -95,7 +94,7 @@ public void mockDataSources() { "MAGISTRATES_COURT"); when(commonXhibitReferenceDataService.getCrownCourtDetails(any())).thenReturn(crownCourtLocation); - when(commonXhibitReferenceDataService.getCriminalCourtDetails(any(UUID.class), nullable(String.class))).thenReturn(magsCourtLocation); + when(commonXhibitReferenceDataService.getMagsCourtDetails(any())).thenReturn(magsCourtLocation); final JsonObject judiciary = givenPayload("/xhibit/mock-data/referencedata.query.judiciaries.json"); when(commonXhibitReferenceDataService.getJudiciary(any())).thenReturn(judiciary); diff --git a/listing-event/listing-event-processor/src/test/resources/stub-data/azure.rotasl.getHearingSlots.stub-data.json b/listing-event/listing-event-processor/src/test/resources/stub-data/azure.rotasl.getHearingSlots.stub-data.json index 286712f7b..2405666ac 100644 --- a/listing-event/listing-event-processor/src/test/resources/stub-data/azure.rotasl.getHearingSlots.stub-data.json +++ b/listing-event/listing-event-processor/src/test/resources/stub-data/azure.rotasl.getHearingSlots.stub-data.json @@ -23,28 +23,28 @@ "availableDuration": 0, "judiciaries": [ { - "judiciaryId": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", + "id": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false }, { - "judiciaryId": "d424f73e-4a19-396d-a00c-f4c38d1c864e", + "id": "d424f73e-4a19-396d-a00c-f4c38d1c864e", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": false, - "benchChairman": true + "isDeputy": false, + "isBenchChairman": true }, { - "judiciaryId": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", + "id": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false } ], "slotStartTimes": [] diff --git a/listing-event/listing-event-processor/src/yaml/json/schema/listing.events.court-list-restricted.json b/listing-event/listing-event-processor/src/yaml/json/schema/listing.events.court-list-restricted.json index 145abbd73..a80e23481 100644 --- a/listing-event/listing-event-processor/src/yaml/json/schema/listing.events.court-list-restricted.json +++ b/listing-event/listing-event-processor/src/yaml/json/schema/listing.events.court-list-restricted.json @@ -40,13 +40,6 @@ "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" } }, - "courtApplicationSubjectIds": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" - } - }, "courtApplicationIds": { "type": "array", "minItems": 1, @@ -97,11 +90,6 @@ "required": [ "courtApplicationRespondentIds" ] - }, - { - "required": [ - "courtApplicationSubjectIds" - ] } ], "required": [ diff --git a/listing-event/listing-event-processor/src/yaml/json/schema/public.listing.court-list-restricted.json b/listing-event/listing-event-processor/src/yaml/json/schema/public.listing.court-list-restricted.json index 98a8226ec..a13b0ebf0 100644 --- a/listing-event/listing-event-processor/src/yaml/json/schema/public.listing.court-list-restricted.json +++ b/listing-event/listing-event-processor/src/yaml/json/schema/public.listing.court-list-restricted.json @@ -40,13 +40,6 @@ "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" } }, - "courtApplicationSubjectIds": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "http://justice.gov.uk/core/courts/courtsDefinitions.json#/definitions/uuid" - } - }, "courtApplicationIds": { "type": "array", "minItems": 1, @@ -97,11 +90,6 @@ "courtApplicationIds", "courtApplicationType" ] - }, - { - "required": [ - "courtApplicationSubjectIds" - ] } ], "required": [ diff --git a/listing-event/pom.xml b/listing-event/pom.xml index e5dce0d3b..9e24a81ee 100644 --- a/listing-event/pom.xml +++ b/listing-event/pom.xml @@ -3,7 +3,7 @@ uk.gov.moj.cpp.listing listing-parent - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-healthchecks/pom.xml b/listing-healthchecks/pom.xml index 31de47eb9..fac0ebfe3 100644 --- a/listing-healthchecks/pom.xml +++ b/listing-healthchecks/pom.xml @@ -3,7 +3,7 @@ listing-parent uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-integration-test-persistence/pom.xml b/listing-integration-test-persistence/pom.xml index ed8c650b7..9718c2344 100644 --- a/listing-integration-test-persistence/pom.xml +++ b/listing-integration-test-persistence/pom.xml @@ -3,7 +3,7 @@ listing-parent uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-integration-test/README-PAYLOAD-BASED-TESTING.md b/listing-integration-test/README-PAYLOAD-BASED-TESTING.md index 7794222a8..fb2248c87 100644 --- a/listing-integration-test/README-PAYLOAD-BASED-TESTING.md +++ b/listing-integration-test/README-PAYLOAD-BASED-TESTING.md @@ -21,28 +21,70 @@ The new approach provides several benefits: ### Test Data Structure +Test data is split into two sections by court type: **MAGS** (Magistrates) and **CROWN**. + ``` test-data/ -├── list-court-hearing/ -│ ├── adhoc_hearing_creation.json -│ ├── spi_allocated.json -│ ├── spi_unallocated.json -│ ├── mcc_without_courtschedule_allocated.json -│ ├── sjp_without_courthscheduleid.json -│ └── spi_two_defendants_unallocated.json -├── list-next-hearings-v2/ -│ ├── adjorunment_crown_fixed_date.json -│ ├── adjournment_crown_week_commencing.json -│ └── adjournment_mags.json -├── list-unscheduled-next-hearings/ -│ ├── adjournment_crown-unscheduled.json -│ └── adjournment_crown-unscheduled_2.json -└── update-hearing-for-listing/ - ├── update-hearing-for-listing-allocated-room-update.json - ├── update-hearing-for-listing-assign-judiciary.json - ├── update-hearing-for-listing-change-to-multiday-with-nondefault-and-nonsitting.json - ├── update-hearing-for-listing-from-weekcommencing-to-multiday.json - └── update-hearing-for-listing-unallocated-to-allocated.json +├── MAGS/ +│ ├── list-court-hearing/ +│ │ ├── adhoc_hearing_creation.json +│ │ ├── spi_allocated.json +│ │ ├── spi_unallocated.json +│ │ ├── mcc_without_courtschedule_allocated.json +│ │ ├── sjp_without_courthscheduleid.json +│ │ └── spi_two_defendants_unallocated.json +│ ├── list-next-hearings-v2/ +│ │ ├── adjorunment_crown_fixed_date.json +│ │ ├── adjournment_crown_week_commencing.json +│ │ └── adjournment_mags.json +│ ├── list-unscheduled-next-hearings/ +│ │ ├── adjournment_crown-unscheduled.json +│ │ └── adjournment_crown-unscheduled_2.json +│ └── update-hearing-for-listing/ +│ ├── update-hearing-for-listing-allocated-room-update.json +│ ├── update-hearing-for-listing-assign-judiciary.json +│ ├── update-hearing-for-listing-change-to-multiday-with-nondefault-and-nonsitting.json +│ ├── update-hearing-for-listing-from-weekcommencing-to-multiday.json +│ └── update-hearing-for-listing-unallocated-to-allocated.json +├── CROWN/ +│ ├── list-court-hearing/ +│ │ ├── adhoc_hearing_creation.json +│ │ ├── adhoc_week_commencing.json +│ │ ├── mcc_crown_unscheduled.json +│ │ ├── mcc_crown_week_commencing.json +│ │ ├── mcc_without_courtschedule_allocated.json +│ │ ├── spi_two_defendants_unallocated.json +│ │ └── spi_unallocated.json +│ ├── list-next-hearings-v2/ +│ │ ├── adjorunment_crown_fixed_date.json +│ │ └── adjournment_crown_week_commencing.json +│ ├── list-unscheduled-next-hearings/ +│ │ └── adjournment_crown-unscheduled.json +│ ├── split/ +│ │ ├── unallocated-split-allocate-list-court-hearing.json +│ │ └── unallocated-split-allocate-update-hearing-for-listing.json +│ └── update-hearing-for-listing/ +│ ├── update-hearing-for-listing-allocated-room-update.json +│ ├── update-hearing-for-listing-assign-judiciary.json +│ ├── update-hearing-for-listing-change-to-multiday-with-nondefault-and-nonsitting.json +│ └── update-hearing-for-listing-from-weekcommencing-to-multiday.json +``` + +## Court Type Selection + +The `PayloadGenerator` supports loading test data for different court types via the `courtType` parameter. Two constants are provided: + +- `PayloadGenerator.MAGS` — Magistrates court data (default) +- `PayloadGenerator.CROWN` — Crown court data + +When no court type is specified, `MAGS` is used by default, preserving backward compatibility. + +```java +// Loads from test-data/MAGS/list-court-hearing/spi_allocated.json (default) +PayloadGenerator.loadPayloadWithDynamicValues("list-court-hearing", "spi_allocated"); + +// Loads from test-data/CROWN/list-court-hearing/adhoc_hearing_creation.json +PayloadGenerator.loadPayloadWithDynamicValues(PayloadGenerator.CROWN, "list-court-hearing", "adhoc_hearing_creation"); ``` ## Placeholder System @@ -79,6 +121,14 @@ PayloadGenerator.PayloadValues values = steps.whenListCourtHearingSubmittedWithS ); ``` +You can also load payloads with custom values for a specific court type: + +```java +PayloadGenerator.loadPayloadWithCustomValues( + PayloadGenerator.CROWN, "list-court-hearing", "adhoc_hearing_creation", customValues +); +``` + ## Usage Examples ### Basic List Court Hearing Test diff --git a/listing-integration-test/pom.xml b/listing-integration-test/pom.xml index 41e12ea17..562fdb23e 100644 --- a/listing-integration-test/pom.xml +++ b/listing-integration-test/pom.xml @@ -3,7 +3,7 @@ listing-parent uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 @@ -11,14 +11,6 @@ 3.3.1.Final - - @@ -202,48 +194,6 @@ true - - - de.thetaphi - forbiddenapis - 3.7 - - false - - java.time.LocalDate#now() @ Use ItClock.today() — the single anchored test clock - java.time.LocalDateTime#now() @ Use ItClock.nowLocalDateTime() - java.time.LocalTime#now() @ Derive from ItClock - java.time.ZonedDateTime#now() @ Use ItClock.nowUtc() / ItClock.nowLondon() - java.time.OffsetDateTime#now() @ Derive from ItClock - java.time.Instant#now() @ Use ItClock.nowInstant() for data dates (elapsed-time infra is excluded) - java.time.Year#now() @ Derive from ItClock - java.util.Date#<init>() @ Use ItClock — new Date() reads the host clock/zone - - - **/it/util/ItClock.class - **/TestDurationListener.class - **/ServerLogTestMarkerExtension.class - - - - - ban-direct-wall-clock-reads - process-test-classes - - testCheck - - - - @@ -279,14 +229,6 @@ - - - ${it.clock} - **/*IT.java diff --git a/listing-integration-test/src/test/java/uk/gov/justice/services/test/utils/persistence/DatabaseCleaner.java b/listing-integration-test/src/test/java/uk/gov/justice/services/test/utils/persistence/DatabaseCleaner.java index e43ad2686..f670674e3 100644 --- a/listing-integration-test/src/test/java/uk/gov/justice/services/test/utils/persistence/DatabaseCleaner.java +++ b/listing-integration-test/src/test/java/uk/gov/justice/services/test/utils/persistence/DatabaseCleaner.java @@ -91,21 +91,6 @@ public void cleanViewStoreErrorTables(final String contextName) { /** * Deletes all the data in the Event-Store tables * - *

The relay queues are re-swept at the end. An {@code AsynchronousPrePublisher} - * transaction committing between these truncates (the timers fire every 10ms on the IT - * stack) can insert a {@code publish_queue} entry whose {@code published_event} row is then - * truncated; the publisher then fails every tick with "Failed to find PublishedEvent", - * rolls back, and the orphaned head entry wedges the whole relay — nothing the next test - * appends ever publishes. Observed as {@code ExtendHearingIT} "Not Found within 30 seconds" - * with 6116 publisher errors in one 31s window. A commit landing after the re-sweep inserts - * row and queue entry atomically, so it cannot create the orphan. - * - *

Deliberately does NOT wait for the relay to drain first: truncating mid-relay is what - * suppresses the previous test's unpublished stale events. Draining first releases them onto - * the durable subscriptions, where under-filtered JMS consumers read them as instant - * wrong-payload failures (observed: {@code HearingIT} consumed a neighbouring test's - * hearing-confirmed event). - * * @param contextName the name of the context to clean the tables from */ public void cleanEventStoreTables(final String contextName) { @@ -117,11 +102,6 @@ public void cleanEventStoreTables(final String contextName) { truncateTable("pre_publish_queue", EVENT_STORE_DATABASE_NAME, connection); truncateTable("published_event", EVENT_STORE_DATABASE_NAME, connection); - // Clear any relay entries that snuck in mid-sequence and now reference truncated - // rows — the orphan that causes the wedge. - truncateTable("publish_queue", EVENT_STORE_DATABASE_NAME, connection); - truncateTable("pre_publish_queue", EVENT_STORE_DATABASE_NAME, connection); - } catch (SQLException e) { throw new DataAccessException("Failed to commit or close database connection", e); } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/helper/SearchHearingHelper.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/helper/SearchHearingHelper.java index 0fb80fa32..ec91f74f3 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/helper/SearchHearingHelper.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/helper/SearchHearingHelper.java @@ -90,14 +90,6 @@ public static String getHearingFilter(final String hearingId) { return String.format(HEARING_FILTER, hearingId); } - public static String getListedCaseFilter(final String hearingId, final String caseId) { - return getHearingFilter(hearingId) + ".listedCases[?(@.id == '" + caseId + "')]"; - } - - public static String getDefendantFilter(final String hearingId, final String caseId, final String defendantId) { - return getListedCaseFilter(hearingId, caseId) + ".defendants[?(@.id == '" + defendantId + "')]"; - } - /** * Poll for hearing with JMS delay to handle asynchronous message processing. * Use this method when the test involves JMS commands that need time to process. diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/AbstractIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/AbstractIT.java index 51c8e4799..b60a38d1d 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/AbstractIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/AbstractIT.java @@ -2,18 +2,14 @@ import static com.github.tomakehurst.wiremock.client.WireMock.reset; import static com.google.common.io.Resources.getResource; -import static javax.ws.rs.core.Response.Status.OK; -import static uk.gov.moj.cpp.listing.utils.WebDavStub.acceptCourtListXmlFile; import static java.nio.charset.Charset.defaultCharset; import static java.util.UUID.randomUUID; import static java.util.stream.Collectors.joining; -import static javax.ws.rs.core.Response.Status.OK; import static uk.gov.justice.services.common.http.HeaderConstants.USER_ID; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubCourtSchedulerCatchAll; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubDeleteAvailableHearingSlotsServiceForAnyHearing; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased; -import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataOrganisationUnitCatchAll; -import static uk.gov.moj.cpp.listing.utils.WebDavStub.acceptCourtListXmlFile; +import uk.gov.moj.cpp.listing.it.util.ArtemisQueuePurger; import static uk.gov.moj.cpp.listing.utils.WireMockStubUtils.setupAsAuthorisedUser; import static uk.gov.moj.cpp.listing.utils.WireMockStubUtils.setupProgressionNotesStubs; import static uk.gov.moj.cpp.listing.utils.WireMockStubUtils.setupProsecutionCaseByCaseUrn; @@ -59,16 +55,12 @@ public class AbstractIT { @BeforeEach void setUp() { + ArtemisQueuePurger.purgeAllListingQueues(); reset(); - // ASYNC-VULNERABLE stubs are re-armed FIRST after reset(): in-flight EVENT_PROCESSOR - // work from the previous test (court-list export PUTs, org-unit lookups for allocations) - // can land in the reset()->arm gap and fail with 404/NULL-payload errors misattributed - // to this test (court-list export then marks publish status failed and the payload - // query 500s). Mirrors the team/ccsph2n hardening. - acceptCourtListXmlFile(OK); - stubGetReferenceDataOrganisationUnitCatchAll(); + stubCourtSchedulerCatchAll(); setupAsAuthorisedUser(USER_ID_VALUE); stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(); + stubDeleteAvailableHearingSlotsServiceForAnyHearing(); setupProsecutionCaseByCaseUrn(); setupProgressionNotesStubs(); setupUsersGroupPermissionsForApplicationTypeStub(); @@ -122,6 +114,7 @@ protected Map getParams() { params.put("sessionEndDate", "2020-10-11"); params.put("pageSize", "20"); params.put("pageNumber", "1"); + params.put("jurisdiction", "MAGISTRATES"); return params; } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CacheRefDataCourtroomIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CacheRefDataCourtroomIT.java index 547f2824a..30ce74377 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CacheRefDataCourtroomIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CacheRefDataCourtroomIT.java @@ -103,12 +103,9 @@ void shouldCloseCourtRoom() throws JsonProcessingException { .withName(PUBLIC_REFERENCE_COURTROOM_CLOSED) .withUserId(randomUUID().toString()) .build()); - // The courtroom-closed event is processed asynchronously by the cache listener. Poll until the - // cache reflects the removal (count drops back to 3) instead of reading the count once and racing - // the listener — under suite load the close can land a few hundred ms after this point. - await().atMost(15, SECONDS).pollInterval(POLL_INTERVAL) - .until(() -> countCacheItemsInDb() == 3); - assertThat(checkCourtRoomIdExists(roomId), is(false)); + final boolean isRoomIdExists = checkCourtRoomIdExists(roomId); + assertThat(isRoomIdExists, is(false)); + assertThat(countCacheItemsInDb(), is(3)); } @Test diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CaseLinkedIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CaseLinkedIT.java index f30f49025..9fe711477 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CaseLinkedIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CaseLinkedIT.java @@ -13,7 +13,6 @@ import static uk.gov.justice.services.test.utils.core.matchers.ResponseStatusMatcher.status; import static uk.gov.justice.services.test.utils.core.messaging.MetadataBuilderFactory.metadataOf; import static uk.gov.justice.services.test.utils.core.random.RandomGenerator.STRING; -import static uk.gov.moj.cpp.listing.it.util.PublishRetryHelper.publishUntilReflected; import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.pollWithDefaults; import static uk.gov.moj.cpp.listing.utils.PropertyUtil.getBaseUri; import static uk.gov.moj.cpp.listing.utils.PropertyUtil.readConfig; @@ -33,24 +32,17 @@ import java.util.Arrays; import java.util.UUID; -import java.util.function.Supplier; import javax.json.JsonObject; import javax.ws.rs.core.Response; import com.fasterxml.jackson.databind.ObjectMapper; -import org.awaitility.core.ConditionTimeoutException; import org.hamcrest.Matcher; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; class CaseLinkedIT extends AbstractIT { - - private static final Logger LOGGER = LoggerFactory.getLogger(CaseLinkedIT.class); - private static final UUID CASE_TO_BE_LINKED_1 = randomUUID(); private static final String CASE_URN_TO_BE_LINKED_1 = STRING.next(); private static final UUID CASE_TO_BE_LINKED_2 = randomUUID(); @@ -78,20 +70,24 @@ void shouldUpdateLinkedCases() { final int numberOfListedCases = hearingsData.getHearingData().get(0).getListedCases().size(); //LINK EVENT + publishCasesLinkedEvent(createCaseLinkedEvent(caseId, caseUrn)); + final Matcher[] caseLinkedEventMatchers = new Matcher[]{ withJsonPath("$.id", equalTo(hearingId.toString())), withJsonPath("$.listedCases.length()", equalTo(numberOfListedCases)), anyOf(createLinkedCaseMatcher(0), createLinkedCaseMatcher(1)) }; - publishUntilHearingReflected(() -> createCaseLinkedEvent(caseId, caseUrn), hearingId, caseLinkedEventMatchers); + + verifyHearing(hearingId, caseLinkedEventMatchers); //UNLINK EVENT + publishCasesLinkedEvent(createCaseUnlinkedEvent(caseId, caseUrn)); final Matcher[] caseUnlinkedEventMatchers = new Matcher[]{ withJsonPath("$.id", equalTo(hearingId.toString())), withJsonPath("$.listedCases.length()", equalTo(numberOfListedCases)), anyOf(createUnlinkedCaseMatcher(0), createUnlinkedCaseMatcher(1)) }; - publishUntilHearingReflected(() -> createCaseUnlinkedEvent(caseId, caseUrn), hearingId, caseUnlinkedEventMatchers); + verifyHearing(hearingId, caseUnlinkedEventMatchers); } @@ -125,28 +121,6 @@ private String verifyHearing(final UUID hearingId, final Matcher[] matchers) { return response.getPayload(); } - /** - * Publishes the cases-linked event (or unlink variant) and polls until the read model reflects it, - * re-publishing up to 3 times with a fresh event each attempt. - * - *

Why re-publish? The event is processed by the {@code Case} aggregate's {@code linkCases} - * method, which silently drops the update ({@code hearingIds.isEmpty() → Stream.empty()}) when the - * aggregate does not yet know which hearing the case belongs to. The {@code add-hearing-to-case} - * command that populates {@code hearingIds} is asynchronous, so on slow CI the first publish can - * arrive before the link exists and is dropped with no JMS redelivery. Re-publishing (with a fresh - * event object and fresh metadata id each attempt) guarantees that once the link is established a - * subsequent publish lands. - */ - private void publishUntilHearingReflected(final Supplier eventSupplier, - final UUID hearingId, - final Matcher[] matchers) { - // The event is recreated inside the loop so each publish has a fresh payload AND a fresh - // metadata id (randomUUID() inside publishCasesLinkedEvent), preventing framework dedup. - publishUntilReflected(LOGGER, "case-linked-fix", "cases-linked event for hearing " + hearingId, - () -> publishCasesLinkedEvent(eventSupplier.get()), - () -> verifyHearing(hearingId, matchers)); - } - private String generateUrlForFindingAHearingById(final String rawId) { return String.format("%s/%s", getBaseUri(), format(readConfig().getProperty("listing.search.hearing"), diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CaseMarkerUpdateIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CaseMarkerUpdateIT.java index a305a5a08..d0cd29a32 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CaseMarkerUpdateIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CaseMarkerUpdateIT.java @@ -48,10 +48,8 @@ void shouldUpdateCaseMarkersForListedCase() { stubParams.put("BOOKING_ID", bookingId.toString()); stubParams.put("HEARING_START_TIME", hearingStartTime.toString()); stubProvisionalBookingWithCustomParams(stubParams); - // Re-publishes until the case<->hearing link (Case.hearingIds, populated by the async - // add-hearing-to-case command) is established and the update lands. A single publish can be - // silently dropped on slow environments — see UpdateCaseMarkersSteps#publishUntilCaseMarkersReflected. - steps.publishUntilCaseMarkersReflected(caseIdToUpdateMarkers); + steps.whenCaseMarkerUpdatedPublicEventIsPublished(); + steps.verifyCaseMarkersUpdatedThroughAPIWithJmsDelay(caseIdToUpdateMarkers); } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CourtApplicationUpdateIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CourtApplicationUpdateIT.java index 7b90dd3f0..c526da5f0 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CourtApplicationUpdateIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CourtApplicationUpdateIT.java @@ -15,6 +15,7 @@ void shouldUpdateCourtApplicationForFutureHearings() { listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); final CourtApplicationSteps courtApplicationSteps = new CourtApplicationSteps(hearingsData); - courtApplicationSteps.publishUntilCourtApplicationReflected(); + courtApplicationSteps.whenCaseCourtApplicationUpdatedPublicEventIsPublished(); + courtApplicationSteps.verifyCourtApplicationUpdatedFromAPI(); } } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CourtListIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CourtListIT.java index 35a8a7540..33d068e00 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CourtListIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/CourtListIT.java @@ -6,7 +6,6 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.Matchers.contains; import static uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData.updatedHearingDataForAllocation; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetAvailableHearingSlotsWithQueryParams; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessionsWithMultipleSchedules; @@ -141,16 +140,11 @@ public void generatePublicCourtWhenOffenceAddedToHearingWithExParte() { final DefendantData defendantWithoutExParte = listedCaseWithoutExParte.getDefendants().stream().reduce((first, second) -> second).get(); final OffenceData offenceWithoutExParte = defendantWithoutExParte.getOffences().stream().reduce((first, second) -> second).get(); final String templateName = "PublicCourtListEnglishWelsh"; - // The ExParte scenario can list more than one hearing in this timeslot, and the court-list JSON - // does not order them deterministically, so a positional hearings[0] matcher intermittently matched - // the wrong hearing (90s RestPoller ConditionTimeout). Anchor the assertions to THIS hearing by id. - final String exParteHearingPath = - "$.hearingDates[0].courtRooms[0].timeslots[0].hearings[?(@.id=='" + hearingData.getId().toString() + "')]"; final Matcher[] allocatedMatchers = { - withJsonPath(exParteHearingPath + ".id", contains(hearingData.getId().toString())), - withJsonPath(exParteHearingPath + ".caseId", contains(listedCaseWithoutExParte.getCaseId().toString())), - withJsonPath(exParteHearingPath + ".defendants[0].id", contains(defendantWithoutExParte.getDefendantId().toString())), - withJsonPath(exParteHearingPath + ".defendants[0].offences[0].id", contains(offenceWithoutExParte.getOffenceId().toString())), + withJsonPath("$.hearingDates[0].courtRooms[0].timeslots[0].hearings[0].id", equalTo(hearingData.getId().toString())), + withJsonPath("$.hearingDates[0].courtRooms[0].timeslots[0].hearings[0].caseId", equalTo(listedCaseWithoutExParte.getCaseId().toString())), + withJsonPath("$.hearingDates[0].courtRooms[0].timeslots[0].hearings[0].defendants[0].id", equalTo(defendantWithoutExParte.getDefendantId().toString())), + withJsonPath("$.hearingDates[0].courtRooms[0].timeslots[0].hearings[0].defendants[0].offences[0].id", equalTo(offenceWithoutExParte.getOffenceId().toString())), withJsonPath("$.templateName", is(templateName)) }; diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantLegalAidStatusUpdateIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantLegalAidStatusUpdateIT.java index e1974603c..2c4d6e2ab 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantLegalAidStatusUpdateIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantLegalAidStatusUpdateIT.java @@ -1,5 +1,9 @@ package uk.gov.moj.cpp.listing.it; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; +import static org.hamcrest.core.IsEqual.equalTo; +import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearingWithJmsDelay; + import uk.gov.moj.cpp.listing.steps.DefendantLegalAidStatusUpdateSteps; import uk.gov.moj.cpp.listing.steps.ListCourtHearingSteps; import uk.gov.moj.cpp.listing.steps.data.HearingData; @@ -7,6 +11,7 @@ import java.util.UUID; +import org.hamcrest.Matcher; import org.junit.jupiter.api.Test; public class DefendantLegalAidStatusUpdateIT extends AbstractIT { @@ -20,6 +25,11 @@ void shouldUpdateDefendantLegalAidStatusFollowingPublicDefendantLegalAidStatusUp final UUID caseId = hearingsData.getHearingData().get(0).getListedCases().get(0).getCaseId(); HearingData hearingData = hearingsData.getHearingData().get(0); final DefendantLegalAidStatusUpdateSteps defendantLegalAidStatusUpdateSteps = new DefendantLegalAidStatusUpdateSteps(caseId, hearingData); - defendantLegalAidStatusUpdateSteps.publishUntilLegalAidStatusReflected(); + defendantLegalAidStatusUpdateSteps.whenCaseDefendantLegalAidStatusUpdatedPublicEventIsPublished(); + // Use JMS-aware polling to handle asynchronous message processing + pollForHearingWithJmsDelay(hearingData.getCourtCentreId().toString(), false, getLoggedInUser().toString(), new Matcher[]{ + withJsonPath("$.hearings[0].listedCases[0].defendants[0].legalAidStatus", equalTo("Granted")) + }); + } } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantOffencesChangedIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantOffencesChangedIT.java index b13ced2e8..b1a6a7a5c 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantOffencesChangedIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantOffencesChangedIT.java @@ -46,12 +46,8 @@ public void shouldUpdateDefendantOffencesFollowingPublicDefendantOffencesChanged UpdatedOffenceData updatedOffenceData = updateOffenceData(offenceData); final UpdateDefendantOffencesSteps steps = new UpdateDefendantOffencesSteps(caseId, hearingData, updatedOffenceData, offenceIdToBeDeleted); - // Re-publishes until the update is REFLECTED via REST (gate). The Case aggregate silently - // drops the update when the case<->hearing link has not yet formed (async add-hearing-to-case - // race); a fresh metadata id per attempt avoids framework dedup. The gate polls REST (a read, - // not a JMS consume) so it does NOT steal the private events the verify* calls below assert — - // those run once REST proves the combined event was applied (hearing UNALLOCATED → isAllocated=false). - steps.publishUntilOffencesReflected(false); + steps.whenCaseDefendantOffencesUpdatedPublicEventIsPublished(); + steps.verifyPublicEventDefendantOffencesUpdatedInActiveMQ(); steps.verifyEventDefendantOffencesToBeUpdateInActiveMQ(); steps.verifyEventDefendantOffencesToBeAddedInActiveMQ(); steps.verifyEventDefendantOffencesToBeDeletedInActiveMQ(); @@ -74,7 +70,8 @@ public void shouldUpdateDefendantOffencesFollowingPublicDefendantOffencesChanged UpdatedOffenceData updatedOffenceData = updateOffenceData(offenceData); final UpdateDefendantOffencesSteps steps = new UpdateDefendantOffencesSteps(caseId, hearingData, updatedOffenceData, null); - steps.publishUntilOffencesUpdatedOnlyReflected(false); + steps.whenCaseDefendantOffencesUpdatedPublicEventIsPublishedUpdatedOnly(); + steps.verifyDefendentOffenceUpdatedOnlyFromAPI(false); } @Test @@ -88,7 +85,8 @@ public void shouldUpdateDefendantOffencesFollowingPublicDefendantOffencesChanged UpdatedOffenceData updatedOffenceData = updateOffenceData(offenceData); final UpdateDefendantOffencesSteps steps = new UpdateDefendantOffencesSteps(caseId, hearingData, updatedOffenceData, null); - steps.publishUntilOffencesAddedOnlyReflected(false); + steps.whenCaseDefendantOffencesUpdatedPublicEventIsPublishedAddedOnly(); + steps.verifyDefendentOffenceAddedOnlyFromAPI(false); } @Test @@ -102,9 +100,8 @@ public void shouldUpdateDefendantOffencesFollowingPublicDefendantOffencesChanged UpdatedOffenceData updatedOffenceData = updateOffenceData(offenceData); final UpdateDefendantOffencesSteps steps = new UpdateDefendantOffencesSteps(caseId, hearingData, updatedOffenceData, updatedOffenceData.getOffenceId()); - // Re-publish until reflected — closes the coverage gap left when the other offence-change - // variants were wrapped: the case<->hearing link race silently drops a single publish. - steps.publishUntilOffencesDeletedOnlyReflected(false); + steps.whenCaseDefendantOffencesUpdatedPublicEventIsPublishedDeletedOnly(); + steps.verifyDefendentOffenceDeletedOnlyFromAPI(false); } private HearingsData listCourtHearing() { diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantProceedingConcludedAndCaseStatusIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantProceedingConcludedAndCaseStatusIT.java index 8db768ff0..509283e41 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantProceedingConcludedAndCaseStatusIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantProceedingConcludedAndCaseStatusIT.java @@ -12,7 +12,6 @@ import uk.gov.moj.cpp.listing.steps.data.HearingData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; import uk.gov.moj.cpp.listing.steps.data.ListedCaseData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.time.ZoneOffset; @@ -35,17 +34,18 @@ void shouldUpdateDefendantProceedingConcludedAndCaseStatusEventFromProgression() HearingData hearingData = hearingsData.getHearingData().get(0); final CaseUpdatedAndDefendantProceedingsConcludedSteps caseUpdatedAndDefendantProceedingsConcludedSteps = new CaseUpdatedAndDefendantProceedingsConcludedSteps(caseId, hearingData); - caseUpdatedAndDefendantProceedingsConcludedSteps.publishUntilCaseStatusReflected(UNALLOCATED); + caseUpdatedAndDefendantProceedingsConcludedSteps.whenPublicEventCaseUpdatedAndHearingResultedIsPublished(); + caseUpdatedAndDefendantProceedingsConcludedSteps.verifyHearingForCaseStatusAndDefendantProceedingsConcludedFromAPIWithJmsDelay(UNALLOCATED); } @Test void shouldUpdateDefendantProceedingConcludedAndCaseStatusEventFromProgressionWhenAllocated() { final HearingsData hearingsData = HearingsData.hearingsDataWithAllocationDataAndJudiciary(); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowUtc() + ZonedDateTime.now(ZoneOffset.UTC) .withHour(9) .withMinute(0) .withSecond(0) @@ -59,7 +59,8 @@ void shouldUpdateDefendantProceedingConcludedAndCaseStatusEventFromProgressionWh HearingData hearingData = hearingsData.getHearingData().get(0); final CaseUpdatedAndDefendantProceedingsConcludedSteps caseUpdatedAndDefendantProceedingsConcludedSteps = new CaseUpdatedAndDefendantProceedingsConcludedSteps(caseId, hearingData); - caseUpdatedAndDefendantProceedingsConcludedSteps.publishUntilCaseStatusReflected(ALLOCATED); + caseUpdatedAndDefendantProceedingsConcludedSteps.whenPublicEventCaseUpdatedAndHearingResultedIsPublished(); + caseUpdatedAndDefendantProceedingsConcludedSteps.verifyHearingForCaseStatusAndDefendantProceedingsConcludedFromAPIWithJmsDelay(ALLOCATED); } @Test diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantsAddedIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantsAddedIT.java index b161e46c9..9591591a4 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantsAddedIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantsAddedIT.java @@ -10,8 +10,10 @@ import uk.gov.moj.cpp.listing.steps.ListCourtHearingSteps; import uk.gov.moj.cpp.listing.steps.data.HearingData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; -import uk.gov.moj.cpp.listing.it.util.ItClock; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.UUID; @@ -29,17 +31,18 @@ void shouldAddDefendantsFollowingPublicDefendantsAddedEventFromProgressionAndHea UUID caseId = hearingsData.getHearingData().get(0).getListedCases().get(0).getCaseId(); HearingData hearingData = hearingsData.getHearingData().get(0); final AddDefendantSteps addDefendantSteps = new AddDefendantSteps(caseId, hearingData); - addDefendantSteps.publishUntilDefendantsAddedReflected(false); + addDefendantSteps.whenCaseDefendantsAddedPublicEventIsPublished(); + addDefendantSteps.verifyHearingListedFromAPIWithJmsDelay(false); } @Test void shouldAddDefendantsFollowingPublicDefendantsAddedEventFromProgressionAndHearingIsUnallocatedEnabled() { HearingsData hearingsData = hearingsDataWithAllocationDataAndJudiciary(); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowUtc() + ZonedDateTime.now(ZoneOffset.UTC) .withHour(9) .withMinute(0) .withSecond(0) @@ -51,7 +54,8 @@ void shouldAddDefendantsFollowingPublicDefendantsAddedEventFromProgressionAndHea UUID caseId = hearingsData.getHearingData().get(0).getListedCases().get(0).getCaseId(); HearingData hearingData = hearingsData.getHearingData().get(0); final AddDefendantSteps addDefendantSteps = new AddDefendantSteps(caseId, hearingData); - addDefendantSteps.publishUntilDefendantsAddedReflected(true); + addDefendantSteps.whenCaseDefendantsAddedPublicEventIsPublished(); + addDefendantSteps.verifyHearingListedFromAPIWithJmsDelay(true); } @@ -59,10 +63,10 @@ void shouldAddDefendantsFollowingPublicDefendantsAddedEventFromProgressionAndHea void shouldAddDefendantsFollowingPublicDefendantsAddedEventFromProgressionAndHearingIsAllocated() { HearingsData hearingsData = hearingsDataWithAllocationDataAndJudiciary(); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowUtc() + ZonedDateTime.now(ZoneOffset.UTC) .withHour(9) .withMinute(0) .withSecond(0) @@ -74,17 +78,18 @@ void shouldAddDefendantsFollowingPublicDefendantsAddedEventFromProgressionAndHea UUID caseId = hearingsData.getHearingData().get(0).getListedCases().get(0).getCaseId(); HearingData hearingData = hearingsData.getHearingData().get(0); final AddDefendantSteps addDefendantSteps = new AddDefendantSteps(caseId, hearingData); - addDefendantSteps.publishUntilDefendantsAddedConsumed(); + addDefendantSteps.whenCaseDefendantsAddedPublicEventIsPublished(); + addDefendantSteps.verifyPublicEventDefendantAddedInActiveMQ(); } @Test void shouldAddDefendantsFollowingPublicDefendantsAddedEventFromProgressionAndHearingIsAllocatedHmiEnabled() { HearingsData hearingsData = hearingsDataWithAllocationDataAndJudiciary(); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowUtc() + ZonedDateTime.now(ZoneOffset.UTC) .withHour(9) .withMinute(0) .withSecond(0) @@ -96,6 +101,7 @@ void shouldAddDefendantsFollowingPublicDefendantsAddedEventFromProgressionAndHea UUID caseId = hearingsData.getHearingData().get(0).getListedCases().get(0).getCaseId(); HearingData hearingData = hearingsData.getHearingData().get(0); final AddDefendantSteps addDefendantSteps = new AddDefendantSteps(caseId, hearingData); - addDefendantSteps.publishUntilDefendantsAddedConsumed(); + addDefendantSteps.whenCaseDefendantsAddedPublicEventIsPublished(); + addDefendantSteps.verifyPublicEventDefendantAddedInActiveMQ(); } } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantsChangedIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantsChangedIT.java index f1af00309..c234103a6 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantsChangedIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/DefendantsChangedIT.java @@ -1,10 +1,9 @@ package uk.gov.moj.cpp.listing.it; +import static java.util.UUID.fromString; import static java.util.UUID.randomUUID; -import static uk.gov.moj.cpp.listing.steps.data.UpdatedDefendantData.partialDefendantUpdateWithoutIsYouth; import static uk.gov.moj.cpp.listing.steps.data.UpdatedDefendantData.updatedDefendantData; -import static uk.gov.moj.cpp.listing.steps.data.UpdatedDefendantData.updatedDefendantDataWithIsYouth; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessions; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubProvisionalBookingWithCustomParams; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.getRandomCourtCenterId; @@ -75,131 +74,4 @@ void shouldUpdateDefendantsFollowingPublicDefendantsChangedEventFromProgressionH updateDefendantSteps.whenPublicEventProgressionCaseDefendantsUpdatedIsPublished(); updateDefendantSteps.verifyHearingListedFromAPIWithJmsDelay(true); } - - /** - * {@code public.progression.case-defendant-changed} is processed into {@code listing.command.update-defendants-for-hearing}. - * Listed defendant starts as an adult ({@code isYouth} false); after the update payload marks them as youth, the view reflects {@code isYouth} true. - */ - @Test - void shouldSetDefendantAsYouthAfterUpdateDefendantsForHearingWhenInitiallyAdult() { - final HearingsData hearingsData = HearingsData.hearingsDataWithAdultDefendants(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPIWithJmsDelay(UNALLOCATED); - listCourtHearingSteps.verifyFirstListedDefendantYouthStatusWithJmsDelay(UNALLOCATED, false); - - final DefendantData defendantData = hearingsData.getHearingData().get(0).getListedCases().get(0).getDefendants().get(0); - final UUID caseId = hearingsData.getHearingData().get(0).getListedCases().get(0).getCaseId(); - final HearingData hearingData = hearingsData.getHearingData().get(0); - final UpdatedDefendantData updatedDefendantData = updatedDefendantData(defendantData); - - final UpdateDefendantSteps updateDefendantSteps = new UpdateDefendantSteps(caseId, hearingData, updatedDefendantData); - updateDefendantSteps.whenPublicEventProgressionCaseDefendantsUpdatedIsPublished(); - updateDefendantSteps.verifyHearingListedFromAPIWithJmsDelay(false, true); - } - - /** - * Covers {@code Case.updateDefendant} merge and {@code NewDomainToEventConverter.updateEventDefendant} retention: - * once {@code isYouth} is true, a later {@code public.progression.case-defendant-changed} without {@code isYouth} must not clear it. - */ - @Test - void shouldRetainIsYouthWhenSecondCaseDefendantChangedEventOmitsIsYouthFlag() { - final HearingsData hearingsData = HearingsData.hearingsDataWithAdultDefendants(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPIWithJmsDelay(UNALLOCATED); - listCourtHearingSteps.verifyFirstListedDefendantYouthStatusWithJmsDelay(UNALLOCATED, false); - - final DefendantData defendantData = hearingsData.getHearingData().get(0).getListedCases().get(0).getDefendants().get(0); - final UUID caseId = hearingsData.getHearingData().get(0).getListedCases().get(0).getCaseId(); - final HearingData hearingData = hearingsData.getHearingData().get(0); - - final UpdatedDefendantData youthUpdate = updatedDefendantDataWithIsYouth(defendantData, Boolean.TRUE); - final UpdateDefendantSteps youthFlagSet = new UpdateDefendantSteps(caseId, hearingData, youthUpdate); - youthFlagSet.whenPublicEventProgressionCaseDefendantChangedIsPublished(youthUpdate); - youthFlagSet.verifyListedDefendantIsYouthWithJmsDelay(UNALLOCATED, true); - - final UpdatedDefendantData partialUpdate = partialDefendantUpdateWithoutIsYouth( - defendantData, "YouthRetainedFirst", "YouthRetainedLast"); - final UpdateDefendantSteps partialUpdateSteps = new UpdateDefendantSteps(caseId, hearingData, partialUpdate); - partialUpdateSteps.whenPublicEventProgressionCaseDefendantChangedIsPublished(partialUpdate); - partialUpdateSteps.verifyDefendantDetailsUpdatedWithJmsDelay(UNALLOCATED); - partialUpdateSteps.verifyListedDefendantIsYouthWithJmsDelay(UNALLOCATED, true); - } - - /** - * Youth flag may arrive in the second {@code public.progression.case-defendant-changed} after a partial update without {@code isYouth}. - */ - @Test - void shouldSetIsYouthWhenYouthFlagArrivesInSecondCaseDefendantChangedEvent() { - final HearingsData hearingsData = HearingsData.hearingsDataWithAdultDefendants(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPIWithJmsDelay(UNALLOCATED); - listCourtHearingSteps.verifyFirstListedDefendantYouthStatusWithJmsDelay(UNALLOCATED, false); - - final DefendantData defendantData = hearingsData.getHearingData().get(0).getListedCases().get(0).getDefendants().get(0); - final UUID caseId = hearingsData.getHearingData().get(0).getListedCases().get(0).getCaseId(); - final HearingData hearingData = hearingsData.getHearingData().get(0); - - final UpdatedDefendantData partialUpdate = partialDefendantUpdateWithoutIsYouth( - defendantData, "BeforeYouthFirst", "BeforeYouthLast"); - final UpdateDefendantSteps partialUpdateSteps = new UpdateDefendantSteps(caseId, hearingData, partialUpdate); - partialUpdateSteps.whenPublicEventProgressionCaseDefendantChangedIsPublished(partialUpdate); - // Partial payload omits isYouth; verify name change only (API may omit isYouth when unset). - partialUpdateSteps.verifyHearingListedFromAPIWithJmsDelay(UNALLOCATED); - - final UpdatedDefendantData youthUpdate = updatedDefendantDataWithIsYouth(defendantData, Boolean.TRUE); - final UpdateDefendantSteps youthUpdateSteps = new UpdateDefendantSteps(caseId, hearingData, youthUpdate); - youthUpdateSteps.whenPublicEventProgressionCaseDefendantChangedIsPublished(youthUpdate); - youthUpdateSteps.verifyListedDefendantIsYouthWithJmsDelay(UNALLOCATED, true); - } - - /** - * Explicit {@code isYouth: false} in a later event must not downgrade a defendant who was already marked as youth. - */ - @Test - void shouldRetainIsYouthWhenSecondCaseDefendantChangedEventExplicitlySetsAdult() { - final HearingsData hearingsData = HearingsData.hearingsDataWithAdultDefendants(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPIWithJmsDelay(UNALLOCATED); - - final DefendantData defendantData = hearingsData.getHearingData().get(0).getListedCases().get(0).getDefendants().get(0); - final UUID caseId = hearingsData.getHearingData().get(0).getListedCases().get(0).getCaseId(); - final HearingData hearingData = hearingsData.getHearingData().get(0); - - final UpdatedDefendantData youthUpdate = updatedDefendantDataWithIsYouth(defendantData, Boolean.TRUE); - final UpdateDefendantSteps youthFlagSet = new UpdateDefendantSteps(caseId, hearingData, youthUpdate); - youthFlagSet.whenPublicEventProgressionCaseDefendantChangedIsPublished(youthUpdate); - youthFlagSet.verifyListedDefendantIsYouthWithJmsDelay(UNALLOCATED, true); - - final UpdatedDefendantData adultExplicitUpdate = updatedDefendantDataWithIsYouth(defendantData, Boolean.FALSE); - final UpdateDefendantSteps adultUpdateSteps = new UpdateDefendantSteps(caseId, hearingData, adultExplicitUpdate); - adultUpdateSteps.whenPublicEventProgressionCaseDefendantChangedIsPublished(adultExplicitUpdate); - adultUpdateSteps.verifyListedDefendantIsYouthWithJmsDelay(UNALLOCATED, true); - } - - /** - * Hearing listed with a young defendant; a subsequent partial {@code case-defendant-changed} without {@code isYouth} retains youth. - */ - @Test - void shouldRetainIsYouthWhenListedAsYouthAndSubsequentEventOmitsIsYouthFlag() { - final HearingsData hearingsData = HearingsData.singleHearingDataForYoungDefendants(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPIWithJmsDelay(UNALLOCATED); - listCourtHearingSteps.verifyFirstListedDefendantYouthStatusWithJmsDelay(UNALLOCATED, true); - - final DefendantData defendantData = hearingsData.getHearingData().get(0).getListedCases().get(0).getDefendants().get(0); - final UUID caseId = hearingsData.getHearingData().get(0).getListedCases().get(0).getCaseId(); - final HearingData hearingData = hearingsData.getHearingData().get(0); - - final UpdatedDefendantData partialUpdate = partialDefendantUpdateWithoutIsYouth( - defendantData, "YoungListedFirst", "YoungListedLast"); - final UpdateDefendantSteps partialUpdateSteps = new UpdateDefendantSteps(caseId, hearingData, partialUpdate); - partialUpdateSteps.whenPublicEventProgressionCaseDefendantChangedIsPublished(partialUpdate); - partialUpdateSteps.verifyDefendantDetailsUpdatedWithJmsDelay(UNALLOCATED); - partialUpdateSteps.verifyListedDefendantIsYouthWithJmsDelay(UNALLOCATED, true); - } } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/EjectCaseWithMagistrateCourtSlotsManagementIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/EjectCaseCourtSlotsManagementIT.java similarity index 66% rename from listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/EjectCaseWithMagistrateCourtSlotsManagementIT.java rename to listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/EjectCaseCourtSlotsManagementIT.java index 691cc1498..60a668aa8 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/EjectCaseWithMagistrateCourtSlotsManagementIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/EjectCaseCourtSlotsManagementIT.java @@ -1,13 +1,11 @@ package uk.gov.moj.cpp.listing.it; +import static java.time.LocalDate.now; import static java.time.LocalTime.of; import static java.time.ZonedDateTime.of; import static java.time.ZoneOffset.UTC; -import static java.util.UUID.fromString; import static java.util.UUID.randomUUID; import static uk.gov.moj.cpp.listing.steps.data.HearingsData.hearingsDataWithAllocationDataAndJudiciary; -import uk.gov.moj.cpp.listing.steps.data.CaseAndDefendantData; - import static uk.gov.moj.cpp.listing.steps.data.HearingsData.hearingsDataWithAllocationDataAndJudiciaryWithCourtCenterForMagistrate; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubDeleteAvailableHearingSlotsService; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessions; @@ -20,9 +18,8 @@ import uk.gov.moj.cpp.listing.steps.EjectCaseApplicationSteps; import uk.gov.moj.cpp.listing.steps.ListCourtHearingSteps; +import uk.gov.moj.cpp.listing.steps.data.CaseAndDefendantData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; -import uk.gov.moj.cpp.listing.it.util.ItClock; - import org.junit.jupiter.api.Test; @@ -33,110 +30,100 @@ import java.util.UUID; /** - * Integration test that verifies court scheduler slot management when a case is ejected - * from a magistrate hearing. This test: - * 1. Creates an allocated MAGISTRATE hearing with 1 case and 1 linked court application - * 2. Ejects the case - * 3. Verifies that HearingSlotsService.delete() is called at least twice + * Integration tests verifying court scheduler slot management when cases are ejected + * from allocated hearings across jurisdictions. */ -class EjectCaseWithMagistrateCourtSlotsManagementIT extends AbstractIT { - - private static final String COURT_SCHEDULER_ENDPOINT = "/listingcourtscheduler-api/rest/courtscheduler"; - private static final String HEARING_SLOTS = "/hearingslots"; +class EjectCaseCourtSlotsManagementIT extends AbstractIT { @Test void shouldNotCallHearingSlotsServiceDeleteIfHearingStillHasCasesThatAreNotEjected() { - // Given: Create hearing with 2 cases (using the simpler approach) final TestHearingData testData = createTestHearingWithTwoCases(); - - // When: Eject only one case (leaving one case in the hearing) + final EjectCaseApplicationSteps ejectSteps = new EjectCaseApplicationSteps(testData.hearingsData); ejectSteps.verifyListedCasesInHearings(true, 2); ejectSteps.buildEjectCaseData(); ejectSteps.verifyNoHearingsReturned(true); - // Then: Verify that HearingSlotsService.delete() is NOT called (0 times) verifyDeleteAvailableHearingSlotsStubCommandIsNeverInvoked(testData.hearingsData.getHearingData().get(0).getId().toString()); } - + @Test void shouldCallHearingSlotsServiceDeleteOnceForMagistrateHearingWithLinkedApplication() { - // Given: Create hearing with 1 case (using CaseAndDefendantData approach) - final TestHearingData testData = createTestHearingWithOneCase(); - - // When: Eject the only case (leaving no cases in the hearing) + final TestHearingData testData = createTestHearingWithOneCaseForJurisdiction("MAGISTRATES"); + + final EjectCaseApplicationSteps ejectSteps = new EjectCaseApplicationSteps(testData.hearingsData); + ejectSteps.verifyListedCasesInHearings(true, 1); + ejectSteps.buildEjectCaseData(); + ejectSteps.verifyNoHearingsReturned(true); + + verifyDeleteAvailableHearingSlotsStubCommandInvoked(testData.hearingsData.getHearingData().get(0).getId().toString()); + } + + @Test + void shouldCallHearingSlotsServiceDeleteOnceForCrownHearingWithLinkedApplication() { + final TestHearingData testData = createTestHearingWithOneCaseForJurisdiction("CROWN"); + final EjectCaseApplicationSteps ejectSteps = new EjectCaseApplicationSteps(testData.hearingsData); ejectSteps.verifyListedCasesInHearings(true, 1); ejectSteps.buildEjectCaseData(); ejectSteps.verifyNoHearingsReturned(true); - // Then: Verify that HearingSlotsService.delete() is called exactly once verifyDeleteAvailableHearingSlotsStubCommandInvoked(testData.hearingsData.getHearingData().get(0).getId().toString()); } - private TestHearingData createTestHearingWithTwoCases() { stubUpdateAvailableHearingSlotsService(); final UUID courtCentreId = getRandomCourtCenterId(); final UUID courtRoomUUID = getRandomCourtRoomId(); - // Create future dates for the hearing - final LocalDate futureHearingDate = ItClock.today().plusDays(7); // 7 days in the future - final ZonedDateTime futureHearingStartTime = of(futureHearingDate, of(10, 0), UTC); // 10:00 AM UTC - final LocalDate futureHearingEndDate = futureHearingDate.plusDays(1); // End date is next day + final LocalDate futureHearingDate = now().plusDays(7); + final ZonedDateTime futureHearingStartTime = of(futureHearingDate, of(10, 0), UTC); + final LocalDate futureHearingEndDate = futureHearingDate.plusDays(1); - // Create hearing data using factory with allocated magistrate hearing and future dates (simpler approach) final HearingsData hearingsData = hearingsDataWithAllocationDataAndJudiciaryWithCourtCenterForMagistrate( - courtCentreId, courtRoomUUID, futureHearingEndDate, futureHearingStartTime); + courtCentreId, courtRoomUUID, futureHearingEndDate, futureHearingStartTime); - // Setup stubs and create hearing setupStubsAndCreateHearing(hearingsData, courtCentreId, futureHearingDate, futureHearingStartTime); - return new TestHearingData(hearingsData); } - private TestHearingData createTestHearingWithOneCase() { + private TestHearingData createTestHearingWithOneCaseForJurisdiction(final String jurisdictionType) { stubUpdateAvailableHearingSlotsService(); final UUID courtCentreId = getRandomCourtCenterId(); final UUID courtRoomUUID = getRandomCourtRoomId(); - // Create future dates for the hearing - final LocalDate futureHearingDate = ItClock.today().plusDays(7); // 7 days in the future - final ZonedDateTime futureHearingStartTime = of(futureHearingDate, of(10, 0), UTC); // 10:00 AM UTC - final LocalDate futureHearingEndDate = futureHearingDate.plusDays(1); // End date is next day - - // Create CaseAndDefendantData for single case hearing + final LocalDate futureHearingDate = now().plusDays(7); + final ZonedDateTime futureHearingStartTime = of(futureHearingDate, of(10, 0), UTC); + final LocalDate futureHearingEndDate = futureHearingDate.plusDays(1); + final CaseAndDefendantData caseAndDefendantData = new CaseAndDefendantData( - randomUUID(), - "test-case-urn", - "test-case-urn", - randomUUID(), - "test-search", - "MAGISTRATES", - "MAGISTRATES", - "test-linked-case-urn", - "test-linked-case-urn" + randomUUID(), + "test-case-urn", + "test-case-urn", + randomUUID(), + "test-search", + jurisdictionType, + jurisdictionType, + "test-linked-case-urn", + "test-linked-case-urn" ); - + final HearingsData hearingsData = hearingsDataWithAllocationDataAndJudiciary( - caseAndDefendantData, courtCentreId, courtRoomUUID, futureHearingEndDate, futureHearingStartTime); + caseAndDefendantData, courtCentreId, courtRoomUUID, futureHearingEndDate, futureHearingStartTime); - // Setup stubs and create hearing setupStubsAndCreateHearing(hearingsData, courtCentreId, futureHearingDate, futureHearingStartTime); - return new TestHearingData(hearingsData); } - private void setupStubsAndCreateHearing(final HearingsData hearingsData, final UUID courtCentreId, - final LocalDate futureHearingDate, final ZonedDateTime futureHearingStartTime) { + private void setupStubsAndCreateHearing(final HearingsData hearingsData, final UUID courtCentreId, + final LocalDate futureHearingDate, final ZonedDateTime futureHearingStartTime) { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); final UUID bookingId = randomUUID(); final String courtScheduleId = "8e837de0-743a-4a2c-9db3-b2e678c48729"; - // Use the future dates for stub parameters final UUID courtroomId = hearingsData.getHearingData().get(0).getCourtRoomId(); - Map stubParams = new HashMap<>(); + final Map stubParams = new HashMap<>(); stubParams.put("SESSION_DATE", futureHearingDate.toString()); stubParams.put("COURT_CENTRE_ID", courtCentreId.toString()); stubParams.put("COURT_SCHEDULE_ID", courtScheduleId); @@ -146,8 +133,6 @@ private void setupStubsAndCreateHearing(final HearingsData hearingsData, final U stubProvisionalBookingWithCustomParams(stubParams); stubListHearingInCourtSessions(hearingsData.getHearingData().get(0).getId().toString(), courtScheduleId, futureHearingStartTime); - - // Stub the delete hearing slots endpoint to return 202 No Content stubDeleteAvailableHearingSlotsService(hearingsData.getHearingData().get(0).getId().toString()); listCourtHearingSteps.whenCaseIsSubmittedForListing(); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ExhibitScenarioIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ExhibitScenarioIT.java index e936cd70c..5942c8a69 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ExhibitScenarioIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ExhibitScenarioIT.java @@ -12,11 +12,9 @@ import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessionsWithMultipleSchedules; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubProvisionalBookingWithCustomParams; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubUpdateAvailableHearingSlotsService; -import static uk.gov.moj.cpp.listing.utils.FileUtil.getPayload; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCourtCentreById; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCourtMappings; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCpCourtRooms; -import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCpXhibitMagsCourtMappingForOucode; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataHearingTypes; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataXhibitCourtRoomMappings; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubOrganisationUnit; @@ -31,14 +29,12 @@ import uk.gov.moj.cpp.listing.steps.ListNextHearingSteps; import uk.gov.moj.cpp.listing.steps.PublishCourtListSteps; import uk.gov.moj.cpp.listing.steps.UpdateHearingSteps; -import uk.gov.moj.cpp.listing.steps.data.CommittingCourtTestDetails; import uk.gov.moj.cpp.listing.steps.data.CourtCentreData; import uk.gov.moj.cpp.listing.steps.data.DefendantData; import uk.gov.moj.cpp.listing.steps.data.HearingData; import uk.gov.moj.cpp.listing.steps.data.HearingTypeData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; import uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.io.IOException; import java.time.LocalDate; @@ -113,7 +109,7 @@ void testAdjournHearingListedForSpecificDate() throws Exception { listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); final PublishCourtListType publishCourtListType = PublishCourtListType.DRAFT; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, @@ -136,102 +132,6 @@ void testAdjournHearingListedForSpecificDate() throws Exception { publishCourtListSteps.verifySentPublishedCourtListHearingDataForDraft(true, "RestrictionApplied"); } - /** - * Crown venue (Preston): two allocated hearings on the draft list where offences carry different committing courts — - * one referencing a magistrates court centre (Liverpool, mags Xhibit mapping) and one referencing the crown court - * centre (Preston, empty mags then crown cp-xhibit-court-mappings). Exhibit export should complete successfully. - */ - @Test - void testDraftCourtListExportWithMixedMagistratesAndCrownCommittingCourts() throws Exception { - stubUpdateAvailableHearingSlotsService(); - final UUID prestonCourtCentreId = fromString("b52f805c-2821-4904-a0e0-26f7fda6dd08"); - final UUID liverpoolCourtCentreId = fromString("9b583616-049b-30f9-a14f-028a53b7cfe8"); - final UUID courtRoomUUID = fromString("1d0199f8-8812-48a2-b13c-837e1c03ff19"); - final UUID courtListId = randomUUID(); - final int courtRoomId = 231; - final String courtScheduleId = "8e837de0-743a-4a2c-9db3-b2e678c48729"; - - final HearingsData magsCommittingHearingData = HearingsData.singleHearingDataSingleCaseWithSingleOffence( - prestonCourtCentreId, courtRoomUUID, "DISTRICT_JUDGE", "Norwich Crown court", 1); - magsCommittingHearingData.getHearingData().get(0).getListedCases().get(0).getDefendants().get(0).getOffences().get(0) - .setCommittingCourtTestDetails(new CommittingCourtTestDetails( - liverpoolCourtCentreId, "Liverpool Magistrates' Court", "MAGISTRATES")); - - final HearingsData crownCommittingHearingData = HearingsData.singleHearingDataSingleCaseWithSingleOffence( - prestonCourtCentreId, courtRoomUUID, "DISTRICT_JUDGE", "Norwich Crown court", 1); - crownCommittingHearingData.getHearingData().get(0).getListedCases().get(0).getDefendants().get(0).getOffences().get(0) - .setCommittingCourtTestDetails(new CommittingCourtTestDetails( - prestonCourtCentreId, "Preston Crown Court", "CROWN")); - - final ListCourtHearingSteps listSteps1 = new ListCourtHearingSteps(magsCommittingHearingData); - final ZonedDateTime hearingStartTime1 = listSteps1.getHearingsData().getHearingData().get(0).getHearingStartTime(); - final LocalDate hearingDate1 = hearingStartTime1.toLocalDate(); - final UUID courtroomId1 = listSteps1.getHearingsData().getHearingData().get(0).getCourtRoomId(); - final UUID bookingId1 = randomUUID(); - final Map stubParams1 = new HashMap<>(); - stubParams1.put("SESSION_DATE", hearingDate1.toString()); - stubParams1.put("COURT_CENTRE_ID", prestonCourtCentreId.toString()); - stubParams1.put("COURT_SCHEDULE_ID", courtScheduleId); - stubParams1.put("COURT_ROOM_ID", courtroomId1.toString()); - stubParams1.put("BOOKING_ID", bookingId1.toString()); - stubParams1.put("HEARING_START_TIME", hearingStartTime1.toString()); - stubProvisionalBookingWithCustomParams(stubParams1); - stubListHearingInCourtSessions( - listSteps1.getHearingsData().getHearingData().get(0).getId().toString(), - courtScheduleId, - hearingStartTime1); - listSteps1.whenCaseIsSubmittedForListing(); - listSteps1.verifyHearingListedFromAPI(ALLOCATED); - - final ListCourtHearingSteps listSteps2 = new ListCourtHearingSteps(crownCommittingHearingData); - final ZonedDateTime hearingStartTime2 = listSteps2.getHearingsData().getHearingData().get(0).getHearingStartTime(); - final LocalDate hearingDate2 = hearingStartTime2.toLocalDate(); - final UUID courtroomId2 = listSteps2.getHearingsData().getHearingData().get(0).getCourtRoomId(); - final UUID bookingId2 = randomUUID(); - final Map stubParams2 = new HashMap<>(); - stubParams2.put("SESSION_DATE", hearingDate2.toString()); - stubParams2.put("COURT_CENTRE_ID", prestonCourtCentreId.toString()); - stubParams2.put("COURT_SCHEDULE_ID", courtScheduleId); - stubParams2.put("COURT_ROOM_ID", courtroomId2.toString()); - stubParams2.put("BOOKING_ID", bookingId2.toString()); - stubParams2.put("HEARING_START_TIME", hearingStartTime2.toString()); - stubProvisionalBookingWithCustomParams(stubParams2); - stubListHearingInCourtSessions( - listSteps2.getHearingsData().getHearingData().get(0).getId().toString(), - courtScheduleId, - hearingStartTime2); - listSteps2.whenCaseIsSubmittedForListing(); - listSteps2.verifyHearingListedFromAPI(ALLOCATED); - - final PublishCourtListType publishCourtListType = PublishCourtListType.DRAFT; - final LocalDate startDate = ItClock.today(); - final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( - prestonCourtCentreId, - publishCourtListType, - startDate); - - stubGetReferenceDataCourtCentreById(prestonCourtCentreId); - stubIdMapperReturningExistingAssociation(courtListId); - stubOrganisationUnit(prestonCourtCentreId); - stubGetReferenceDataCpXhibitMagsCourtMappingForOucode("C04PR00", "{}"); - stubGetReferenceDataCpXhibitMagsCourtMappingForOucode( - "C05LV00", - getPayload("stub-data/referencedata.query.cp-xhibit-mags-court-mapping-c05lv00.json")); - stubGetReferenceDataCourtMappings(new CourtCentreData( - prestonCourtCentreId, DEFAULT_START_TIME, DEFAULT_DURATION_HOURS_MINS, DEFAULT_COURT_ROOM_ID, DEFAULT_COURT_CENTRE_NAME)); - stubGetReferenceDataCourtMappings(new CourtCentreData( - liverpoolCourtCentreId, DEFAULT_START_TIME, DEFAULT_DURATION_HOURS_MINS, DEFAULT_COURT_ROOM_ID, DEFAULT_COURT_CENTRE_NAME)); - stubGetReferenceDataCpCourtRooms(magsCommittingHearingData.getHearingData().get(0).getCourtRoomId(), courtRoomId); - stubGetReferenceDataXhibitCourtRoomMappings(magsCommittingHearingData.getHearingData().get(0).getCourtRoomId()); - - final PublishCourtListSteps publishCourtListSteps = new PublishCourtListSteps(magsCommittingHearingData, publishCourtListCommandPayload); - publishCourtListSteps.createMessageConsumer(); - publishCourtListSteps.acceptCourtListXmlFiles(); - publishCourtListSteps.sendPublishCourtListCommand(); - publishCourtListSteps.verifyCourtListPublishStatus(EXPORT_SUCCESSFUL, "false"); - publishCourtListSteps.verifySentPublishedCourtListHearingDataForDraft(true, "RestrictionApplied"); - } - /** * Scenarios 2:Two cases on same hearing on Same offence and Same defendant with Related hearing in Specific case. *

@@ -295,7 +195,7 @@ void testTwoCasesWithLinkedHearingProducesFinalListOnAFixedDate() throws Excepti listNextHearingSteps.verifyUpdateRelatedHearingRequestedInActiveMQ(anotherHearing.getHearingData().get(0).getId()); final PublishCourtListType publishCourtListType = PublishCourtListType.FINAL; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, publishCourtListType, @@ -377,7 +277,7 @@ void testRelatedHearingSpecificCasesForFixedDate() throws Exception { listNextHearingSteps.verifyUpdateRelatedHearingRequestedInActiveMQ(anotherHearing.getHearingData().get(0).getId()); final PublishCourtListType publishCourtListType = PublishCourtListType.FINAL; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, publishCourtListType, @@ -459,7 +359,7 @@ void testRelatedHearingSpecificCasesForFixedDateWithTwoDefendantsAndTwoOffences( listNextHearingSteps.verifyUpdateRelatedHearingRequestedInActiveMQ(anotherHearing.getHearingData().get(0).getId()); final PublishCourtListType publishCourtListType = PublishCourtListType.FINAL; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, publishCourtListType, @@ -502,20 +402,20 @@ void testWeekendCommencingWithTwoCases() throws Exception { final UUID courtListId = randomUUID(); final int courtRoomId = 231; - final HearingsData hearingsData1 = hearingsDataForWeekCommencing(ItClock.today(), 1, courtCentreId, courtRoomUUID, "DISTRICT_JUDGE"); + final HearingsData hearingsData1 = hearingsDataForWeekCommencing(LocalDate.now(), 1, courtCentreId, courtRoomUUID, "DISTRICT_JUDGE"); hearingsData1.getHearingData().get(0).setName("Nottingham crown court"); final ListCourtHearingSteps listCourtHearingSteps1 = new ListCourtHearingSteps(hearingsData1); listCourtHearingSteps1.whenCaseIsSubmittedForListing(); listCourtHearingSteps1.verifyHearingListedFromAPI(ALLOCATED); - final HearingsData hearingsData2 = hearingsDataForWeekCommencing(ItClock.today(), 1, courtCentreId, courtRoomUUID, "DISTRICT_JUDGE"); + final HearingsData hearingsData2 = hearingsDataForWeekCommencing(LocalDate.now(), 1, courtCentreId, courtRoomUUID, "DISTRICT_JUDGE"); hearingsData2.getHearingData().get(0).setName("Nottingham crown court"); final ListCourtHearingSteps listCourtHearingSteps2 = new ListCourtHearingSteps(hearingsData2); listCourtHearingSteps2.whenCaseIsSubmittedForListing(); listCourtHearingSteps2.verifyHearingListedFromAPI(ALLOCATED); final PublishCourtListType publishCourtListType = PublishCourtListType.WARN; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, @@ -569,20 +469,20 @@ void testTwoWeekendCommencingWithTwoCases() throws Exception { final UUID courtListId = randomUUID(); final int courtRoomId = 231; - final HearingsData hearingsData1 = hearingsDataForWeekCommencing(ItClock.today(), 2, courtCentreId, courtRoomUUID, "DISTRICT_JUDGE"); + final HearingsData hearingsData1 = hearingsDataForWeekCommencing(LocalDate.now(), 2, courtCentreId, courtRoomUUID, "DISTRICT_JUDGE"); hearingsData1.getHearingData().get(0).setName("Nottingham crown court"); final ListCourtHearingSteps listCourtHearingSteps1 = new ListCourtHearingSteps(hearingsData1); listCourtHearingSteps1.whenCaseIsSubmittedForListing(); listCourtHearingSteps1.verifyHearingListedFromAPI(ALLOCATED); - final HearingsData hearingsData2 = hearingsDataForWeekCommencing(ItClock.today(), 2, courtCentreId, courtRoomUUID, "DISTRICT_JUDGE"); + final HearingsData hearingsData2 = hearingsDataForWeekCommencing(LocalDate.now(), 2, courtCentreId, courtRoomUUID, "DISTRICT_JUDGE"); hearingsData2.getHearingData().get(0).setName("Nottingham crown court"); final ListCourtHearingSteps listCourtHearingSteps2 = new ListCourtHearingSteps(hearingsData2); listCourtHearingSteps2.whenCaseIsSubmittedForListing(); listCourtHearingSteps2.verifyHearingListedFromAPI(ALLOCATED); final PublishCourtListType publishCourtListType = PublishCourtListType.FIRM; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, @@ -641,7 +541,7 @@ void testWeekendCommencingWhereSittingIsNull() throws Exception { listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); final PublishCourtListType publishCourtListType = PublishCourtListType.FIRM; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, @@ -722,7 +622,6 @@ void testUshersCourtList() throws IOException { * The hearing should have one court application with a subject but no prosecution cases or listed cases */ @Test - @ExpectedServerErrors("court application hearings without a prosecution case -> WARN 'Hearing does not contain caseIdentifier' from the court-list export (application-only hearings are valid)") void testCrownAllocatedHearingWithSentenceTypeAndCourtApplicationWithSubject() throws Exception { stubUpdateAvailableHearingSlotsService(); @@ -746,8 +645,8 @@ void testCrownAllocatedHearingWithSentenceTypeAndCourtApplicationWithSubject() t stubGetReferenceDataCourtMappings(new CourtCentreData(crownCourtCentreId, DEFAULT_START_TIME, DEFAULT_DURATION_HOURS_MINS, crownCourtRoomUUID, "Leeds Crown Court")); // Set hearing data using reflection - final LocalDate hearingDate = ItClock.today(); - final ZonedDateTime hearingStartTime = ItClock.nowUtc().withHour(10).withMinute(0).withSecond(0).withNano(0); + final LocalDate hearingDate = LocalDate.now(); + final ZonedDateTime hearingStartTime = ZonedDateTime.now().withHour(10).withMinute(0).withSecond(0).withNano(0); setHearingDataFields(standaloneHearing, hearingTypeData, crownCourtCentreId, crownCourtRoomUUID, hearingDate, hearingStartTime); // Setup listing steps and stubs @@ -837,7 +736,7 @@ private void publishAndVerifyCourtList(final HearingsData hearingsData, final UU final PublishCourtListType publishType, final String weekCommencing, final String subjectFirstName, final String subjectLastName, final CourtListVerifier verifier) throws Exception { - final JsonObject commandPayload = buildPublishCourtListCommandPayload(courtCentreId, publishType, ItClock.today()); + final JsonObject commandPayload = buildPublishCourtListCommandPayload(courtCentreId, publishType, LocalDate.now()); final PublishCourtListSteps steps = new PublishCourtListSteps(hearingsData, commandPayload); steps.createMessageConsumer(); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ExpectedServerErrors.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ExpectedServerErrors.java deleted file mode 100644 index c125a88e4..000000000 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ExpectedServerErrors.java +++ /dev/null @@ -1,33 +0,0 @@ -package uk.gov.moj.cpp.listing.it; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a negative-scenario test (or a whole test class) that DELIBERATELY provokes - * server-side ERROR/WARN lines in the Wildfly server.log (e.g. by stubbing a downstream - * service to return 422/500 and asserting the fail-safe behaviour). - * - *

{@link ServerLogTestMarkerExtension} reads this annotation and stamps the test's - * [TEST-START]/[TEST-END] markers in server.log with an extra [EXPECTED-ERRORS] tag plus - * the reason text, so anyone triaging the log knows errors inside that window are by - * design and need no investigation.

- * - *

Triage rule this enables: any ERROR/WARN between a [TEST-START] marker WITHOUT - * [EXPECTED-ERRORS] and its [TEST-END] is a genuine problem. Happy-path tests must - * keep the log clean; only annotated negative scenarios may emit errors.

- */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface ExpectedServerErrors { - - /** - * Short description of the exact ERROR/WARN lines this scenario is expected to produce, - * so a log reader can match them without opening the test source. - */ - String value(); -} diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ExtendHearingIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ExtendHearingIT.java index d9e6839b1..47c3d90fe 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ExtendHearingIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ExtendHearingIT.java @@ -48,10 +48,6 @@ public void shouldExtendHearingForCase() { LOGGER.info("UnAllocated HearingID : {} - Allocated HearingId : {} ", UNALLOCATED_HEARING_ID, ALLOCATED_HEARING_ID); listCourtHearingSteps2.verifyHearingIsCreated(ALLOCATED_HEARING_ID, 1); - // The extend handler fetches BOTH hearings from the query view (ListingCommandHandler.extendHearingForHearing); - // without this await the extend races the unallocated hearing's projection and rollback-redelivers with - // "There is no Hearing for this ID" until hearing-listed lands. - listCourtHearingSteps2.verifyHearingIsCreated(UNALLOCATED_HEARING_ID, 2); listCourtHearingSteps2.extendHearing(UNALLOCATED_HEARING_ID, ALLOCATED_HEARING_ID); listCourtHearingSteps2.verifyPublicEventHearingConfirmedAndExtendHearingFromProgression(ALLOCATED_HEARING_ID, UNALLOCATED_HEARING_ID); listCourtHearingSteps2.verifyHearingUpdatedToCaseInActiveMQ(ALLOCATED_HEARING_ID, UNALLOCATED_HEARING_ID, 2); @@ -79,8 +75,6 @@ public void shouldExtendHearingPartially() { LOGGER.info("UnAllocated HearingID : {} - Allocated HearingId : {} ", unallocatedHearingId, ALLOCATED_HEARING_ID); listCourtHearingSteps2.verifyHearingIsCreated(ALLOCATED_HEARING_ID, 1); - // Await the unallocated hearing too — see shouldExtendHearingForCase for why. - listCourtHearingSteps2.verifyHearingIsCreated(unallocatedHearingId, 2); listCourtHearingSteps2.extendHearingPartially(unallocatedHearingId, ALLOCATED_HEARING_ID, listedCaseData); listCourtHearingSteps2.verifyPublicEventHearingConfirmedEventAndExtendPartialHearingFromProgression(ALLOCATED_HEARING_ID, unallocatedHearingId); listCourtHearingSteps2.verifyHearingUpdatedToCaseInActiveMQ(ALLOCATED_HEARING_ID, unallocatedHearingId, 1); @@ -114,8 +108,6 @@ public void shouldExtendHearingWhole() { LOGGER.info("UnAllocated HearingID : {} - Allocated HearingId : {} ", unallocatedHearingId, ALLOCATED_HEARING_ID); listCourtHearingSteps2.verifyHearingIsCreated(ALLOCATED_HEARING_ID, 1); - // Await the unallocated hearing too — see shouldExtendHearingForCase for why. - listCourtHearingSteps2.verifyHearingIsCreated(unallocatedHearingId, 2); listCourtHearingSteps2.extendWholeHearing(unallocatedHearingId, ALLOCATED_HEARING_ID, listedCaseDataList); listCourtHearingSteps2.verifyPublicEventHearingConfirmedAndExtendHearingFromProgression(ALLOCATED_HEARING_ID, unallocatedHearingId); listCourtHearingSteps2.verifyHearingUpdatedToCaseInActiveMQ(ALLOCATED_HEARING_ID, unallocatedHearingId, 2); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/GroupCasesIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/GroupCasesIT.java index e5b323c9e..be238769a 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/GroupCasesIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/GroupCasesIT.java @@ -26,7 +26,6 @@ import uk.gov.moj.cpp.listing.steps.ListCourtHearingSteps; import uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub; import uk.gov.moj.cpp.listing.utils.QueueUtil; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.io.IOException; import java.time.LocalDate; @@ -62,7 +61,7 @@ public class GroupCasesIT extends AbstractIT { private final UUID groupId = randomUUID(); private final UUID hearingTypeId = randomUUID(); private final UUID courtCentreId = getRandomCourtCenterId(); - private final LocalDate startDate = ItClock.today(); + private final LocalDate startDate = LocalDate.now(); private LocalTime defaultStartTime = LocalTime.parse("10:00"); private final ZonedDateTime hearingStartTime = ZonedDateTime.of(startDate, defaultStartTime, UTC); private long defaultDuration = 20; diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingCsvReportIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingCsvReportIT.java index 460185c5b..2b95806b1 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingCsvReportIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingCsvReportIT.java @@ -3,9 +3,6 @@ import static java.text.MessageFormat.format; import static java.util.Collections.emptyList; import static javax.ws.rs.core.Response.Status.OK; -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.awaitility.Awaitility.await; -import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.POLL_INTERVAL; import static org.codehaus.groovy.runtime.InvokerHelper.asList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; @@ -22,7 +19,6 @@ import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCourtCentreById; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCourtMappings; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCpCourtRooms; -import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataJudiciaries; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataXhibitCourtRoomMappings; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubOrganisationUnit; @@ -30,7 +26,6 @@ import uk.gov.moj.cpp.listing.steps.UpdateHearingSteps; import uk.gov.moj.cpp.listing.steps.data.CourtCentreData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.time.LocalTime; @@ -38,7 +33,6 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; import javax.ws.rs.core.Response; @@ -75,21 +69,12 @@ public void initialize() { data = loadHearingDataWithJudiciary(courtCentreId, courtRoomUUID); - // The CSV report resolves judiciary names via referencedata.query.judiciaries; without - // this stub the response payload is null -> NPE -> WARN "Failed to resolve judiciary name". - data.getHearingData().get(0).getJudiciary() - .forEach(j -> stubGetReferenceDataJudiciaries(j.getJudicialId())); - stubOrganisationUnit(courtCentreId); stubGetReferenceDataCourtMappings(new CourtCentreData(courtCentreId, LocalTime.of(10, 30), "6:30", null, STRING.next())); stubGetReferenceDataCpCourtRooms(data.getHearingData().get(0).getCourtRoomId(), courtRoomId); stubGetReferenceDataXhibitCourtRoomMappings(data.getHearingData().get(0).getCourtRoomId()); var first = data.getHearingData().get(0); - // The update below replaces the hearing's judiciary with a fresh random judge — that is - // the id the CSV report will resolve, so it needs its own judiciaries stub. - var updatedJudicialRole = randomJudicialRole("DISTRICT_JUDGE"); - stubGetReferenceDataJudiciaries(updatedJudicialRole.getJudicialId()); var updatedHearingDataWithoutNonDefaultDaysShouldPreservePrevRoomChange = new uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData( first.getId(), first.getCourtCentreId(), @@ -101,7 +86,7 @@ public void initialize() { emptyList(), emptyList(), "ENGLISH", - asList(updatedJudicialRole), + asList(randomJudicialRole("DISTRICT_JUDGE")), first.getJurisdictionType(), null, null, @@ -128,40 +113,21 @@ void shouldDownloadHearingCsvReport() { final UUID courtCentreId = data.getHearingData().get(0).getCourtCentreId(); final Integer numberOfWeeks = 2; - final LocalDate now = ItClock.today(); + final LocalDate now = LocalDate.now(); final String expectedCsvFileName = "hearing_report_%s.csv".formatted(now.toString()); // When final String url = getDownloadUrl(courtCentreId, now, numberOfWeeks); - // The CSV report is built from the hearing read-model, which updates asynchronously after the - // multi-day update in @BeforeEach. Poll the download until the report reflects the enriched - // 4-day hearing (duration "360" and the "1 of 4".."4 of 4" day markers) so we never assert on - // a pre-enrichment snapshot (which intermittently showed duration "20" / "1 of 1"). - // getLoggedInHeader() reads a ThreadLocal user context. Awaitility evaluates the condition on a - // SEPARATE polling thread where that ThreadLocal is unset, which would make userId null and 500 the - // endpoint on every poll. Capture the header here on the test thread and reuse it inside the lambda. - final javax.ws.rs.core.MultivaluedMap loggedInHeader = getLoggedInHeader(); - final AtomicReference responseRef = new AtomicReference<>(); - final AtomicReference csvRef = new AtomicReference<>(); - await().atMost(15, SECONDS).pollInterval(POLL_INTERVAL).until(() -> { - final Response polled = restClient.query(url, "text/csv", loggedInHeader); - if (polled.getStatus() != OK.getStatusCode()) { - return false; - } - final String csv = polled.readEntity(String.class); - responseRef.set(polled); - csvRef.set(csv); - return csv.contains("360") && csv.contains("4 of 4"); - }); - - final Response response = responseRef.get(); - final String csvContent = csvRef.get(); + + final Response response = restClient.query(url, "text/csv", getLoggedInHeader()); // Then assertThat(response.getStatus(), is(OK.getStatusCode())); assertThat(response.getHeaderString("Content-Type"), containsString("text/csv")); assertThat(response.getHeaderString("Content-Disposition"), containsString("attachment")); assertThat(response.getHeaderString("Content-Disposition"), containsString(expectedCsvFileName)); + final String csvContent = response.readEntity(String.class); + assertThat(csvContent, is(not(emptyString()))); assertThat(csvContent, containsString("Date of hearing")); assertThat(csvContent, containsString("Courtroom")); @@ -171,7 +137,7 @@ void shouldDownloadHearingCsvReport() { // The WireMock stubs should return test notes that get included in the CSV assertThat(csvContent, containsString("PTP")); assertThat(csvContent, containsString("Fixed")); - assertThat(csvContent, containsString("360")); + assertThat(csvContent, containsString("30")); // HEARING_ESTIMATE_MINUTES = 30 assertThat(csvContent, containsString("Youth")); assertThat(csvContent, containsString("ENGLISH")); assertThat(csvContent, containsString("Custody")); @@ -179,7 +145,7 @@ void shouldDownloadHearingCsvReport() { assertThat(csvContent, containsString("RestrictionApplied")); assertThat(csvContent, containsString("C - Description")); assertThat(csvContent, Matchers.stringContainsInOrder("1 of 4","2 of 4","3 of 4","4 of 4")); - final LocalTime utcTime = ZonedDateTime.of(ItClock.today(), LocalTime.of(10, 30), ZoneId.of("Europe/London")) + final LocalTime utcTime = ZonedDateTime.of(LocalDate.now(), LocalTime.of(10, 30), ZoneId.of("Europe/London")) .withZoneSameInstant(ZoneOffset.UTC).toLocalTime(); final String expectedUtcTime = String.format("T%02d:%02d:00Z", utcTime.getHour(), utcTime.getMinute()); assertThat(csvContent, Matchers.stringContainsInOrder(expectedUtcTime)); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingDaysIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingDaysIT.java index fd2c85be0..ea073c27a 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingDaysIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingDaysIT.java @@ -35,7 +35,6 @@ import uk.gov.moj.cpp.listing.steps.UpdateHearingSteps; import uk.gov.moj.cpp.listing.steps.data.HearingData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.io.IOException; import java.time.LocalDate; @@ -90,8 +89,8 @@ public class HearingDaysIT extends AbstractIT { void testHearingDaysWithCourtCentreForSplit() throws IOException { stubGetAvailableHearingSlots(); - startDate = ItClock.today(); - endDate = ItClock.today().plusDays(1); + startDate = LocalDate.now(); + endDate = LocalDate.now().plusDays(1); hearingStartTime = ZonedDateTime.of(startDate, defaultStartTime, UTC); hearingId = randomUUID(); caseId = randomUUID(); @@ -128,19 +127,11 @@ void testHearingDaysWithCourtCentreForSplit() throws IOException { UpdateHearingSteps updateHearingStepsSplit = new UpdateHearingSteps(); - // Anchor the split day span on working days. The split derives new hearing days from the base - // hearing date; built with raw plusDays(1)/plusDays(2) the span straddles a weekend whenever the - // suite runs Wed-Sat. Courts do not sit at weekends, so no hearing-requested-for-listing is emitted - // for the weekend day and verifyHearingRequestedForListingEvent(2) times out. plusWorkingDays keeps - // the span on Mon-Fri and is identical to plusDays on a weekday run with no weekend in range. - final LocalDate splitStartDate = ItClock.plusWorkingDays(hearingData.getHearingStartDate(), 1); - final LocalDate splitEndDate = ItClock.plusWorkingDays(splitStartDate, 2); - final JsonObject updateHearingJsonObjectSplit = updateHearingStepsSplit.preparePayloadToUpdateHearing(UPDATE_HEARING_FOR_LISTING_SPLIT_JSON, getSplitPayloadValues(hearingData.getId().toString(), hearingData.getListedCases().get(0).getCaseId().toString(), hearingData.getCourtCentreId().toString(), hearingData.getCourtRoomId().toString(), - splitStartDate.toString(), splitEndDate.toString(), + hearingData.getHearingStartDate().plusDays(1).toString(), hearingData.getHearingEndDate().plusDays(2).toString(), hearingData.getListedCases().get(0).getDefendants().get(0).getDefendantId().toString(), hearingData.getListedCases().get(0).getDefendants().get(0).getOffences().get(1).getOffenceId().toString(), courtScheduleId)); @@ -178,7 +169,7 @@ private Map getSplitPayloadValues(final String hearingId, @Test void testHearingDaysCorrectedWithCourtCentre() throws IOException { - startDate = ItClock.today(); + startDate = LocalDate.now(); hearingId = randomUUID(); caseId = randomUUID(); courtCentreId = getRandomCourtCenterId(); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingIT.java index bb5c0cb85..42995161f 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/HearingIT.java @@ -32,6 +32,8 @@ import static uk.gov.moj.cpp.listing.steps.data.factory.HearingsDataFactory.MAGISTRATES_JURISDICTION; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetAvailableHearingSlots; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetAvailableHearingSlotsWithQueryParams; +import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetCourtSchedulesByIdWithJudiciary; +import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetCourtSchedulesByIdWithDraftStatus; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessions; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessionsWithJudiciary; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessionsWithMultipleSchedules; @@ -53,7 +55,6 @@ import uk.gov.moj.cpp.listing.steps.data.JudicialRoleTypeData; import uk.gov.moj.cpp.listing.steps.data.SequenceHearingData; import uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.io.IOException; import java.time.LocalDate; @@ -536,6 +537,8 @@ void updateAllocatedHearingWithNoCourtRoomResultsInUnallocatedListing() throws I stubParams.put("BOOKING_ID", bookingId.toString()); stubParams.put("HEARING_START_TIME", hearingStartTime.toString()); stubProvisionalBookingWithCustomParams(stubParams); + stubGetCourtSchedulesByIdWithDraftStatus(java.util.Collections.singletonList(courtScheduleId), false); + stubListHearingInCourtSessions(hearinId.toString(), courtScheduleId, hearingStartTime); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); @@ -644,12 +647,12 @@ void sequenceHearingDays() { @Test void shouldUpdateWeekCommencing() { - final HearingsData hearingsData = HearingsData.hearingsDataForWeekCommencing(ItClock.today(), 1); + final HearingsData hearingsData = HearingsData.hearingsDataForWeekCommencing(LocalDate.now(), 1); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); - listCourtHearingSteps.verifyHearingListedWithWeekCommencingFromAPI(UNALLOCATED, ItClock.today(), 1); + listCourtHearingSteps.verifyHearingListedWithWeekCommencingFromAPI(UNALLOCATED, LocalDate.now(), 1); } @Test @@ -713,9 +716,9 @@ private List getJudicialRoleDataFromRotaSLHearingSlots(final J .map(JsonObject.class::cast) .forEach(rotaSlJudiciaryJsonObject -> judicialRoleDataList.add( - new JudicialRoleData(Optional.of(rotaSlJudiciaryJsonObject.getBoolean("benchChairman")), - Optional.of(rotaSlJudiciaryJsonObject.getBoolean("deputy")), - UUID.fromString(rotaSlJudiciaryJsonObject.getString("judiciaryId")), + new JudicialRoleData(Optional.of(rotaSlJudiciaryJsonObject.getBoolean("isBenchChairman")), + Optional.of(rotaSlJudiciaryJsonObject.getBoolean("isDeputy")), + UUID.fromString(rotaSlJudiciaryJsonObject.getString("id")), null, new JudicialRoleTypeData(Optional.empty(), "MAGISTRATE")) ) @@ -813,4 +816,90 @@ void shouldPublishHearingResultedEventAndUpdateHearingWithResultedStatus() throw updateHearingSteps.verifyHearingResultedInDatabase(); } + @Test + void searchHearingsStampsJudiciarySourceHearingWhenHearingHasOwnJudiciary() { + final HearingsData hearingsData = hearingsDataWithAllocationDataAndJudiciary(); + final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); + final ZonedDateTime hearingStartTime = listCourtHearingSteps.getHearingsData().getHearingData().get(0).getHearingStartTime(); + final UUID courtroomId = listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId(); + final UUID bookingId = randomUUID(); + final String courtScheduleId = "8e837de0-743a-4a2c-9db3-b2e678c48729"; + final UUID courtCentreId = listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtCentreId(); + + final Map stubParams = new HashMap<>(); + stubParams.put("SESSION_DATE", hearingStartTime.toLocalDate().toString()); + stubParams.put("COURT_CENTRE_ID", courtCentreId.toString()); + stubParams.put("COURT_SCHEDULE_ID", courtScheduleId); + stubParams.put("COURT_ROOM_ID", courtroomId.toString()); + stubParams.put("BOOKING_ID", bookingId.toString()); + stubParams.put("HEARING_START_TIME", hearingStartTime.toString()); + stubProvisionalBookingWithCustomParams(stubParams); + stubListHearingInCourtSessions( + listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), + courtScheduleId, + hearingStartTime); + + listCourtHearingSteps.whenCaseIsSubmittedForListing(); + + final String hearingJudicialId = hearingsData.getHearingData().get(0).getJudiciary().get(0).getJudicialId().toString(); + listCourtHearingSteps.verifyHearingListedWithJudiciarySourceAndJudicialId(ALLOCATED, "HEARING", hearingJudicialId); + } + + @Test + void searchHearingsInjectsSessionJudiciaryAndStampsSessionSourceWhenHearingHasNoJudiciary() throws IOException { + final HearingsData hearingsData = hearingsData(); + final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); + + listCourtHearingSteps.whenCaseIsSubmittedForListing(); + listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); + + final UpdatedHearingData updatedHearingDataForAllocation = + updatedHearingDataForAllocationWithoutJudiciary(hearingsData.getHearingData().get(0).getId()); + final UpdateHearingSteps updateHearingSteps = new UpdateHearingSteps(hearingsData, updatedHearingDataForAllocation); + + final String courtScheduleId = updatedHearingDataForAllocation.getNonDefaultDays().get(0) + .getCourtScheduleId().orElse("8e837de0-743a-4a2c-9db3-b2e678c48729"); + + final String sessionJudiciaryId = randomUUID().toString(); + stubGetCourtSchedulesByIdWithJudiciary(courtScheduleId, sessionJudiciaryId, "RECORDER", true, false, + 143117, "His Honour", "His Honour Judge", "Ei Anrhydedd y Barnwr", + "131172", List.of("ATTEMPTED_MURDER", "MURDER"), "HIS HONOUR JUDGE MARK AINSWORTH", + "Ainsworth", "Mark J", "mark.ainsworth@ejudiciary.net"); + + stubGetAvailableHearingSlotsWithQueryParams(updateHearingSteps.getUpdatedHearingData()); + stubListHearingInCourtSessionsWithMultipleSchedules(updateHearingSteps.getUpdatedHearingData()); + updateHearingSteps.whenHearingIsUpdatedForListing(); + + // The update changes the hearing's courtCentreId, so poll at the NEW courtCentreId + // rather than the original one from hearingsData(). + listCourtHearingSteps.verifySessionJudiciaryAllFields( + updatedHearingDataForAllocation.getCourtCentreId(), + ALLOCATED, "SESSION", sessionJudiciaryId, + 143117, + "His Honour", + "His Honour Judge", + "Ei Anrhydedd y Barnwr", + "131172", + "HIS HONOUR JUDGE MARK AINSWORTH", + "Ainsworth", + "Mark J", + "mark.ainsworth@ejudiciary.net", + "RECORDER", + List.of("ATTEMPTED_MURDER", "MURDER")); + } + + @Test + void shouldNotAllocateCrownHearingWhenCourtScheduleSessionIsDraft() { + final HearingsData hearingsData = HearingsData.hearingsDataForBookedSlot(); + final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); + + // Stub fetchCourtSchedulesByIds to return isDraft=true + final String courtScheduleId = hearingsData.getHearingData().get(0).getBookedSlots().get(0).getCourtScheduleId(); + stubGetCourtSchedulesByIdWithDraftStatus(java.util.Collections.singletonList(courtScheduleId), true); + + listCourtHearingSteps.whenCaseIsSubmittedForListing(); + // isDraft=true session → aggregate noneHasDraftSession fails → UNALLOCATED + listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); + } + } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListCourtHearingIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListCourtHearingIT.java index ac8c23818..affaf4373 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListCourtHearingIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListCourtHearingIT.java @@ -27,7 +27,6 @@ import uk.gov.moj.cpp.listing.steps.data.HearingData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; import uk.gov.moj.cpp.listing.steps.data.JudicialRoleData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.time.ZoneId; @@ -58,10 +57,10 @@ public void shouldListHearingWithUnallocatedData() { @Test public void shouldListHearingWithAdjournedDateSingleCountBasedSlot() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithAllocationDataAndJudiciaryWithAdjournmentFromDate(1)); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); } @@ -69,10 +68,10 @@ public void shouldListHearingWithAdjournedDateSingleCountBasedSlot() { @Test public void shouldListHearingWithAdjournedDateSingleCountBasedSlot_CivilCase() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithAllocationDataAndJudiciaryWithAdjournmentFromDate_CivilCase(1)); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); stubProgressionServiceCivilCase(); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); @@ -86,10 +85,10 @@ public void shouldListHearingWithAdjournedDateSingleCountBasedSlot_CivilCase() { @Test public void shouldListHearingWithAdjournedDateSingleCountBasedSlot_CivilSummons() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithAllocationDataAndJudiciaryWithAdjournmentFromDate_CivilCase(1)); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); stubProgressionServiceCivilCaseSummons(); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); @@ -103,10 +102,10 @@ public void shouldListHearingWithAdjournedDateSingleCountBasedSlot_CivilSummons( @Test public void shouldListHearingWithAdjournedDateSingleCountBasedSlotHmiEnabled() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithAllocationDataAndJudiciaryWithAdjournmentFromDate(1)); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); } @@ -124,10 +123,10 @@ public void shouldListHearingWithAdjournedDateMultipleCountBasedSlots() { stubGetProvisionalBookedSlotsMultipleCourtSchedulesCountBased(); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithAllocationDataAndJudiciaryWithAdjournmentFromDate(1)); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); } @@ -138,10 +137,10 @@ public void shouldListHearingWithAdjournedDateMultipleCountBasedSlotsHmiEnabled( stubGetProvisionalBookedSlotsMultipleCourtSchedulesCountBased(); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithAllocationDataAndJudiciaryWithAdjournmentFromDate(1)); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); } @@ -150,10 +149,10 @@ public void shouldListHearingWithAdjournedDateMultipleCountBasedSlotsHmiEnabled( public void shouldListHearingWithAdjournedDateSingleDurationBasedSlot() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithAllocationDataAndJudiciaryWithAdjournmentFromDate(1)); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); } @@ -162,10 +161,10 @@ public void shouldListHearingWithAdjournedDateSingleDurationBasedSlot() { public void shouldListHearingWithAdjournedDateSingleDurationBasedSlotHmiEnabled() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithAllocationDataAndJudiciaryWithAdjournmentFromDate(1)); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); } @@ -235,10 +234,10 @@ public void shouldListHearingWithAdjournedDateMultipleDurationBasedSlotsSummerTi @Test public void shouldListHearingWithAllocatedData() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithAllocationDataAndJudiciary()); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); } @@ -259,10 +258,10 @@ public void shouldListHearingAsUnallocatedAndSendDummyCourtroomToHmi() { public void shouldListHearingWithAllocatedDataHmiEnabled() { final HearingsData hearingsData = hearingsDataWithAllocationDataAndJudiciary(); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); verifyJudiciaryAssignedToAllocatedHearingFromAPI(hearingsData); } @@ -305,10 +304,10 @@ public void shouldListHearingByIdWhenIdIsInvalid() { @Test public void shouldListHearingWithShadowListedFlag() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithShadowListedOffences()); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); verifyShadowListingFlagAndReportingRestrictions(listCourtHearingSteps); @@ -317,10 +316,10 @@ public void shouldListHearingWithShadowListedFlag() { @Test public void shouldListHearingWithShadowListedFlagHmiEnabled() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithShadowListedOffences()); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); verifyShadowListingFlagAndReportingRestrictions(listCourtHearingSteps); } @@ -328,10 +327,10 @@ public void shouldListHearingWithShadowListedFlagHmiEnabled() { @Test public void shouldExtendHearingWithShadowListedFlag() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithShadowListedOffences()); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); listCourtHearingSteps.whenProgressionHearingExtended(); @@ -341,10 +340,10 @@ public void shouldExtendHearingWithShadowListedFlag() { @Test public void shouldExtendHearingWithShadowListedFlagHmiEnabled() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithShadowListedOffences()); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); listCourtHearingSteps.whenProgressionHearingExtended(); @@ -354,10 +353,10 @@ public void shouldExtendHearingWithShadowListedFlagHmiEnabled() { @Test public void shouldRetrieveCasesByDefendantAndHearingDateForAllocatedHearing() { final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsDataWithAllocationDataAndJudiciary()); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); listCourtHearingSteps.verifyQueryAPIFindCaseByPersonDefendantAndHearingDate(); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListCourtWeekCommencingHearingIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListCourtWeekCommencingHearingIT.java index c5631bb0c..b26826c27 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListCourtWeekCommencingHearingIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListCourtWeekCommencingHearingIT.java @@ -1,6 +1,7 @@ package uk.gov.moj.cpp.listing.it; import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; +import static java.time.LocalDate.now; import static java.util.Arrays.asList; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; @@ -21,7 +22,6 @@ import uk.gov.moj.cpp.listing.steps.ListCourtHearingSteps; import uk.gov.moj.cpp.listing.steps.data.HearingsData; import uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.util.ArrayList; @@ -35,8 +35,8 @@ class ListCourtWeekCommencingHearingIT extends AbstractIT { - private final static String WEEK_COMMENCING_END_DATE_FOR_ONE_WEEK = ItClock.today().plusDays(7L).toString(); - private final static String WEEK_COMMENCING_END_DATE_FOR_TWO_WEEKS = ItClock.today().plusDays(14L).toString(); + private final static String WEEK_COMMENCING_END_DATE_FOR_ONE_WEEK = LocalDate.now().plusDays(7L).toString(); + private final static String WEEK_COMMENCING_END_DATE_FOR_TWO_WEEKS = LocalDate.now().plusDays(14L).toString(); private static final DatabaseCleaner databaseCleaner = new DatabaseCleaner(); private static final ViewStoreCleaner viewStoreCleaner = new ViewStoreCleaner(); @@ -63,8 +63,8 @@ public void initialize() { @Test @Disabled("will be addressed with SPRDT-184") public void shouldListHearingsWithinWeekCommencingDateRangeByRelevance() { - final String weekCommencingSearchStartDate = ItClock.today().minusDays(7).toString(); - final String weekCommencingSearchEndDate = ItClock.today().plusDays(22).toString(); + final String weekCommencingSearchStartDate = now().minusDays(7).toString(); + final String weekCommencingSearchEndDate = now().plusDays(22).toString(); final HearingsData hearingsData1 = hearingsData.get(0); final HearingsData hearingsData2 = hearingsData.get(1); @@ -120,8 +120,8 @@ public void shouldListHearingsWithinWeekCommencingDateRangeByRelevance() { @Test @Disabled("will be addressed with SPRDT-184") public void shouldListHearingsWithEndDateOrWeekCommencingDatesWithinWeekCommencingDateRangeByRelevance() { - final String weekCommencingSearchStartDate = ItClock.today().plusDays(4).toString(); - final String weekCommencingSearchEndDate = ItClock.today().plusDays(11).toString(); + final String weekCommencingSearchStartDate = now().plusDays(4).toString(); + final String weekCommencingSearchEndDate = now().plusDays(11).toString(); final HearingsData hearingsData1 = hearingsData.get(2); @@ -149,8 +149,8 @@ public void shouldListHearingsWithStartDateOrWeekCommencingDatesWithinWeekCommen // for (var courtCenterId : getCourtCenterIds()) { // stubGetReferenceDataCourtCentreById(courtCenterId); // } - final String weekCommencingSearchStartDate = ItClock.today().plusDays(14).toString(); - final String weekCommencingSearchEndDate = ItClock.today().plusDays(22).toString(); + final String weekCommencingSearchStartDate = now().plusDays(14).toString(); + final String weekCommencingSearchEndDate = now().plusDays(22).toString(); final UpdatedHearingData updatedHearingData = updatedHearingDataList.get(0); @@ -175,7 +175,7 @@ public void shouldListHearingsWithStartDateOrWeekCommencingDatesWithinWeekCommen @Test public void shouldListUnallocatedHearingsWithinWeekCommencingDateRange() { - final String weekCommencingSearchStartDate = ItClock.today().toString(); + final String weekCommencingSearchStartDate = now().toString(); final HearingsData hearingsData5 = hearingsData.get(4); final UpdatedHearingData firstUpdatedHearingDataWithWeekCommencingDate = updatedHearingDataList.get(1); @@ -204,8 +204,8 @@ public void shouldListUnallocatedHearingsWithinWeekCommencingDateRange() { @Test public void shouldReturnEmptyListWhenNoHearingWithingWeekCommencingDateRangeByRelevance() { - final String weekCommencingSearchStartDate = ItClock.today().minusDays(14).toString(); - final String weekCommencingSearchEndDate = ItClock.today().minusDays(7).toString(); + final String weekCommencingSearchStartDate = now().minusDays(14).toString(); + final String weekCommencingSearchEndDate = now().minusDays(7).toString(); final Matcher[] matchers = { withJsonPath("$.hearings", hasSize(0)), @@ -217,7 +217,7 @@ public void shouldReturnEmptyListWhenNoHearingWithingWeekCommencingDateRangeByRe @Test public void shouldRetrieveCasesByDefendantAndHearingDateForUnallocatedHearing() { - final String weekCommencingSearchStartDate = ItClock.today().toString(); + final String weekCommencingSearchStartDate = now().toString(); final UpdatedHearingData firstUpdatedHearingDataWithWeekCommencingDate = updatedHearingDataList.get(1); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListNextHearingIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListNextHearingIT.java index a0f1c1e71..6ed6918bf 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListNextHearingIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListNextHearingIT.java @@ -10,7 +10,6 @@ import static uk.gov.moj.cpp.listing.steps.data.HearingsData.notHmiEnabledHearingsData; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessions; -import static uk.gov.moj.cpp.listing.utils.HmiAllocationStubHelper.stubForAllocatedListing; import com.google.common.collect.ImmutableMap; import uk.gov.justice.core.courts.JurisdictionType; @@ -25,7 +24,6 @@ import uk.gov.moj.cpp.listing.steps.data.HearingsData; import uk.gov.moj.cpp.listing.steps.data.OffenceData; import uk.gov.moj.cpp.listing.steps.data.UpdatedOffenceData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.time.ZoneOffset; @@ -178,6 +176,7 @@ void shouldAddCasetoExistingHearingforAdHocHearing() { } + @Test void shouldDeleteOldAllocatedRelatedHearingsAndUpdateRelatedHearings() { @@ -187,10 +186,10 @@ void shouldDeleteOldAllocatedRelatedHearingsAndUpdateRelatedHearings() { final HearingsData existedHearings = hearingsDataWithAllocationDataAndJudiciary(); final ListCourtHearingSteps listCourtHearingSteps1 = new ListCourtHearingSteps(firstHearings); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps1.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps1.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps1.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowUtc() + ZonedDateTime.now(ZoneOffset.UTC) .withHour(9) .withMinute(0) .withSecond(0) @@ -200,10 +199,10 @@ void shouldDeleteOldAllocatedRelatedHearingsAndUpdateRelatedHearings() { listCourtHearingSteps1.verifyHearingListedFromAPI(ALLOCATED); final ListCourtHearingSteps listCourtHearingSteps2 = new ListCourtHearingSteps(existedHearings); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps2.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps2.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps2.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowUtc() + ZonedDateTime.now(ZoneOffset.UTC) .withHour(9) .withMinute(0) .withSecond(0) @@ -243,10 +242,10 @@ void shouldRemoveOffencesFromNextHearingWhenFirstSeededHearingAmended(){ // First hearing created final HearingsData firstHearings = HearingsData.hearingsDataWithAllocationDataAndJudiciary(); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(firstHearings); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowUtc() + ZonedDateTime.now(ZoneOffset.UTC) .withHour(9) .withMinute(0) .withSecond(0) @@ -269,9 +268,6 @@ void shouldRemoveOffencesFromNextHearingWhenFirstSeededHearingAmended(){ listNextHearingSteps1.verifyCasesAddedToAllocatedHearingFromApi(nextHearings, secondHearings); // Amend Reshare First Hearing - // Drain the offences-removed events produced by the earlier update-related-hearing step so the - // verify below reads the DELETE's event, not the UPDATE's (same hearingId, so id-filter alone can't disambiguate). - listNextHearingSteps1.clearStaleAllocatedHearingMessages(); listNextHearingSteps1.whenDeleteNextHearingSubmittedForListing(); listNextHearingSteps1.verifyCasesAreInAllocatedHearingFromApi(nextHearings, secondHearings); listNextHearingSteps1.verifyPublicOffencesRemovedFromExistingAllocatedHearingInActiveMQ(nextHearings.getHearingData().get(0).getId(), nextHearings); @@ -282,24 +278,23 @@ void shouldRemoveOffencesFromNextHearingWhenNextHearingIsExistingHearing(){ HearingsData secondHearings = HearingsData.hearingsDataWithAllocationDataAndJudiciary(); ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(secondHearings); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", listCourtHearingSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(listCourtHearingSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowUtc() + ZonedDateTime.now(ZoneOffset.UTC) .withHour(9) .withMinute(0) .withSecond(0) .withNano(0) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"))); listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPIWithJmsDelay(ALLOCATED); + listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); // First hearing created final HearingsData firstHearings = HearingsData.hearingsDataWithAllocationDataAndJudiciary(); listCourtHearingSteps = new ListCourtHearingSteps(firstHearings); - stubForAllocatedListing(listCourtHearingSteps.getHearingsData().getHearingData().get(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPIWithJmsDelay(ALLOCATED); + listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); // Second hearing share and extend to first hearing final UUID nextHearingId = firstHearings.getHearingData().get(0).getId(); @@ -320,9 +315,7 @@ void shouldRemoveOffencesFromNextHearingWhenNextHearingIsExistingHearing(){ final UpdateDefendantOffencesSteps steps = new UpdateDefendantOffencesSteps(caseId, hearingData, updatedOffenceData, null, false); final OffencesForDefendantUpdated newOffences = steps.whenCaseDefendantOffencesUpdatedPublicEventIsPublishedAddedOnly(); final String newOffenceId = newOffences.getAddedOffences().stream().flatMap(a -> a.getOffences().stream()).map(Offence::getId).map(UUID::toString).toList().get(0); - // Re-publish if the first publish was silently dropped due to the cross-aggregate - // case<->hearing link not yet being established (same race as CaseMarkerUpdateIT on vld). - listNextHearingSteps.verifyOrRepublishUntilOffenceReflected(steps, firstHearings, newOffenceId); + listNextHearingSteps.verifyOffenceAddedToAllocatedHearingFromApi(firstHearings, newOffenceId); // Amend Reshare second Hearing listNextHearingSteps.clearStaleAllocatedHearingMessages(); @@ -343,10 +336,10 @@ void ShouldRemoveOffencesIfNextHearingWasExtended(){ final HearingsData firstHearings = HearingsData.hearingsDataWithAllocationDataAndJudiciary(caseAndDefendantData); final ListCourtHearingSteps firstHearingsSteps = new ListCourtHearingSteps(firstHearings); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", firstHearingsSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", firstHearingsSteps.getHearingsData().getHearingData().get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(firstHearingsSteps.getHearingsData().getHearingData().get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowUtc() + ZonedDateTime.now(ZoneOffset.UTC) .withHour(9) .withMinute(0) .withSecond(0) diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListingNoteIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListingNoteIT.java index 0fd79e1d1..c19c5a752 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListingNoteIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ListingNoteIT.java @@ -4,6 +4,7 @@ import static com.jayway.jsonpath.Filter.filter; import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; import static java.text.MessageFormat.format; +import static java.time.LocalDate.now; import static java.util.UUID.fromString; import static java.util.UUID.randomUUID; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -35,7 +36,6 @@ import uk.gov.moj.cpp.listing.steps.NotesSteps; import uk.gov.moj.cpp.listing.steps.data.HearingData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.sql.Connection; import java.sql.Date; @@ -90,7 +90,7 @@ void tearDownAll() throws SQLException { @Test void shouldCreateNoteForListing() { givenAUserHasLoggedInAsAListingOfficer(USER_ID_VALUE); - final LocalDate date = ItClock.today(); + final LocalDate date = now(); final UUID courtRoomId = getRandomCourtRoomId(); final JmsMessageConsumerClient messageConsumerClientPublicForCreateNote = newPublicJmsMessageConsumerClientProvider() .withEventNames( PUBLIC_LISTING_CREATED_LISTING_NOTE).getMessageConsumerClient(); @@ -106,7 +106,7 @@ void shouldEditNote() { givenAUserHasLoggedInAsAListingOfficer(USER_ID_VALUE); //Given 1 : A note created using create note command - UUID noteId = createRandomNote(ItClock.today()); + UUID noteId = createRandomNote(now()); //Given 2 : consumer topic with selector "public.listing.note-edited" to capture public event raised final JmsMessageConsumerClient messageConsumerClientPublicForEditNote = newPublicJmsMessageConsumerClientProvider() @@ -130,7 +130,7 @@ void shouldEditNote() { void shouldDeleteListingNote() { givenAUserHasLoggedInAsAListingOfficer(USER_ID_VALUE); //Given : A note created using create note command - final LocalDate hearingDate = ItClock.today(); + final LocalDate hearingDate = now(); final UUID noteId = createRandomNote(hearingDate); final JmsMessageConsumerClient messageConsumerClientPublicForDeleteNote = newPublicJmsMessageConsumerClientProvider() .withEventNames( PUBLIC_LISTING_DELETED_LISTING_NOTE).getMessageConsumerClient(); @@ -150,10 +150,10 @@ void shouldReturnNotesForAllocatedHearingOnSearchQuery() { //Given 1 : Hearing data final HearingsData hearingsData = hearingsDataWithAllocationDataAndJudiciaryWithAdjournmentFromDate(); List hearingData = hearingsData.getHearingData(); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", hearingData.get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", hearingData.get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(hearingData.get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPIWithJmsDelay(ALLOCATED); @@ -175,10 +175,10 @@ void shouldNotReturnNotesForAllocatedHearingIfNoteNotExistForThatHearingOnSearch //Given 1 : Hearing data and no Note data for this hearing final HearingsData hearingsData = HearingsData.hearingsDataWithAllocationDataAndJudiciary(); List hearingData = hearingsData.getHearingData(); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", hearingData.get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", hearingData.get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(hearingData.get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPIWithJmsDelay(ALLOCATED); @@ -195,7 +195,7 @@ void shouldReturnNotesGivenNoHearingExistForCourtRoomIdAndDateOnSearchQuery() { //Given 1 : No Hearing data but Note data exist for given courtRoom and date UUID courtRoomId = getRandomCourtRoomId(); - LocalDate startDate = ItClock.today(); + LocalDate startDate = now(); notesSteps.createNoteForListing(courtRoomId, startDate.toString(), NOTE_DESCRIPTION); UUID noteId = verifyNoteExists(courtRoomId, startDate); @@ -210,23 +210,19 @@ void shouldNotReturnNotesAndHearingDataOnSearchQueryWithUnallocatedQueryParam() final HearingsData hearingsData = HearingsData.hearingsDataWithAllocationDataAndJudiciary(); List hearingData = hearingsData.getHearingData(); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(ItClock.today(), ImmutableMap.of("courtRoomId", hearingData.get(0).getCourtRoomId().toString())); + stubGetProvisionalBookedSlotsSingleCourtScheduleCountBased(LocalDate.now(), ImmutableMap.of("courtRoomId", hearingData.get(0).getCourtRoomId().toString())); stubListHearingInCourtSessions(hearingData.get(0).getId().toString(), "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowLondon().withHour(10).withMinute(0).withSecond(0).withNano(0)); + ZonedDateTime.now(ZoneId.of("Europe/London")).withHour(10).withMinute(0).withSecond(0).withNano(0)); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPIWithJmsDelay(ALLOCATED); - //Given 2 : Note data using courtRoomId and date from hearing data. noteId is derived - //server-side from (courtRoomId, hearingDate) -> de-duplicate, or the second create for a - //shared courtroom/date logs ERROR "Note already exists" in the aggregate and no-ops. + //Given 2 : Note data using courtRoomId and date from hearing data List noteIds = new ArrayList<>(); - hearingData.stream() - .map(hearingData1 -> java.util.Map.entry(hearingData1.getCourtRoomId(), hearingData1.getHearingStartDate())) - .distinct() - .forEach(roomAndDate -> { - notesSteps.createNoteForListing(roomAndDate.getKey(), roomAndDate.getValue().toString(), NOTE_DESCRIPTION); - noteIds.add(verifyNoteExists(roomAndDate.getKey(), roomAndDate.getValue())); + hearingData + .forEach(hearingData1 -> { + notesSteps.createNoteForListing(hearingData1.getCourtRoomId(), hearingData1.getHearingStartDate().toString(), NOTE_DESCRIPTION); + noteIds.add(verifyNoteExists(hearingData1.getCourtRoomId(), hearingData1.getHearingStartDate())); } ); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/PayloadBasedListNextHearingIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/PayloadBasedListNextHearingIT.java index c6b0c2a98..af995e0e5 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/PayloadBasedListNextHearingIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/PayloadBasedListNextHearingIT.java @@ -11,7 +11,6 @@ import uk.gov.moj.cpp.listing.steps.PayloadBasedListNextHearingSteps; import uk.gov.moj.cpp.listing.steps.PayloadBasedUpdateHearingSteps; import uk.gov.moj.cpp.listing.steps.PayloadGenerator; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.time.ZoneOffset; @@ -113,7 +112,7 @@ void shouldCreateNextHearingForMagistratesJurisdiction() { stubListHearingInCourtSessions(firstHearingValues.hearingId, "8e837de0-743a-4a2c-9db3-b2e678c48729", - ItClock.nowUtc() + ZonedDateTime.now(ZoneOffset.UTC) .withHour(9) .withMinute(0) .withSecond(0) diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/PublishCourtListIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/PublishCourtListIT.java index 5ab2fc2a8..7b1c2d57b 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/PublishCourtListIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/PublishCourtListIT.java @@ -37,8 +37,6 @@ import uk.gov.moj.cpp.listing.steps.data.CourtCentreData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; import uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData; -import uk.gov.moj.cpp.listing.it.util.ItClock; - import java.time.LocalDate; import java.time.LocalTime; @@ -74,7 +72,7 @@ public void shouldPublishCourtListWithNoHearings() throws Exception { final UUID hearingTypeId = fromString("52edf232-3c09-4c74-a6ad-737985c2e662"); final UUID courtListId = randomUUID(); final PublishCourtListType publishCourtListType = PublishCourtListType.FIRM; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, publishCourtListType, @@ -100,7 +98,7 @@ public void shouldPublishCourtListWithHearings() { final UUID courtListId = randomUUID(); final int courtRoomId = 231; final PublishCourtListType publishCourtListType = PublishCourtListType.FIRM; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, @@ -132,7 +130,7 @@ public void publishFinalCourtListsForAllCrownCourts() { final UUID courtCentreIdOne = getRandomCourtCenterId(); final UUID courtCentreIdTwo = getRandomCourtCenterId(asList(courtCentreIdOne)); //fromString("b52f805c-2821-4904-a0e0-26f7fda6dd08"); final PublishCourtListType publishCourtListType = PublishCourtListType.FIRM; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); stubGetReferenceDataCourtMappings(new CourtCentreData(courtCentreIdOne, DEFAULT_START_TIME, DEFAULT_DURATION_HOURS_MINS, DEFAULT_COURT_ROOM_ID, DEFAULT_COURT_CENTRE_NAME)); stubGetAllCrownCourtCentres(courtCentreIdOne, courtCentreIdTwo); stubOrganisationUnit(courtCentreIdOne); @@ -151,7 +149,7 @@ public void publishFinalCourtListsForAllCrownCourts() { publishCourtListSteps.verifyHearingListedFromAPI(true); publishCourtListSteps.acceptCourtListXmlFiles(); - final LocalDate expectedPublishDate = getNextWorkingDay(ItClock.today()); + final LocalDate expectedPublishDate = getNextWorkingDay(LocalDate.now()); sendPublishFinalCourtListsForAllCrownCourtsCommand(commandAsJson); @@ -167,7 +165,7 @@ public void shouldPublishCourtListWithHearingsWithPublicListNoteForFirmPublishTy final UUID courtListId = randomUUID(); final int courtRoomId = 231; final PublishCourtListType publishCourtListType = PublishCourtListType.FIRM; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, @@ -207,7 +205,7 @@ public void shouldPublishCourtListWithHearingsWithOutPublicListNoteForFirmPublis final UUID courtListId = randomUUID(); final int courtRoomId = 231; final PublishCourtListType publishCourtListType = PublishCourtListType.FIRM; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, @@ -246,7 +244,7 @@ public void shouldPublishCourtListWithHearingsWithVideoLinkForDraftPublishTypeAn final UUID courtListId = randomUUID(); final int courtRoomId = 231; final PublishCourtListType publishCourtListType = PublishCourtListType.DRAFT; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, @@ -287,7 +285,7 @@ public void shouldPublishCourtListWithHearingsWithPublicListNoteForFinalPublishT final UUID courtListId = randomUUID(); final int courtRoomId = 231; final PublishCourtListType publishCourtListType = PublishCourtListType.FINAL; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, @@ -327,7 +325,7 @@ public void shouldPublishCourtListWithHearingsWithOutListNoteWhenRRNotPresentFor final UUID courtListId = randomUUID(); final int courtRoomId = 231; final PublishCourtListType publishCourtListType = PublishCourtListType.FIRM; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, @@ -367,7 +365,7 @@ public void shouldRestrictListingCaseFromCourtForHearingId() throws Exception { final UUID courtListId = randomUUID(); final int courtRoomId = 231; final PublishCourtListType publishCourtListType = PublishCourtListType.FIRM; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); final JsonObject publishCourtListCommandPayload = buildPublishCourtListCommandPayload( courtCentreId, diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java index cbe83d4f6..1bab6e2f9 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForCourtCalendarIT.java @@ -20,7 +20,6 @@ import uk.gov.moj.cpp.listing.steps.data.CaseAndDefendantData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; import uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.time.ZonedDateTime; @@ -67,9 +66,9 @@ public void hearingCanBeSearchedForUsingDifferentCombinationsOfParametersForMags { final UUID magestirateCourtRoomId = new ArrayList<>(COURT_ROOMS.keySet()).get(i % COURT_ROOMS.size()); final int dayFromToday = i % 3; - final LocalDate hearingEndDate = ItClock.today().plusDays(dayFromToday); + final LocalDate hearingEndDate = LocalDate.now().plusDays(dayFromToday); //This was failing due to startDate/enDate new adjustment/shrinking - final ZonedDateTime hearingStartTime = ItClock.nowUtc().plusDays(dayFromToday); + final ZonedDateTime hearingStartTime = ZonedDateTime.now().plusDays(dayFromToday); final HearingsData hearingsData = hearingsDataWithAllocationDataAndJudiciaryWithCourtCenterForMagistrate(magsCourtCenterId, magestirateCourtRoomId, hearingEndDate, hearingStartTime); final ListCourtHearingSteps listCourtHearingSteps = getListCourtHearingStepsWithStubbedBookingRef(hearingsData, hearingStartTime); @@ -111,8 +110,8 @@ public void shouldRangeSearchCourtCalendarForCrown() throws JsonProcessingExcept final CaseAndDefendantData caseAndDefendantData = new CaseAndDefendantData(hearingId, caseUrn, caseUrn, masterDefendantId, CASE_IN_HEARING, jurisdictionType, jurisdictionType, null, null); final int dayFromToday = i % 3 ; - final LocalDate hearingEndDate = ItClock.today().plusDays(dayFromToday + 1); - final ZonedDateTime hearingStartTime = ItClock.nowUtc().plusDays(dayFromToday); + final LocalDate hearingEndDate = LocalDate.now().plusDays(dayFromToday + 1); + final ZonedDateTime hearingStartTime = ZonedDateTime.now().plusDays(dayFromToday); ListCourtHearingSteps listCourtHearingSteps1 = new ListCourtHearingSteps(HearingsData.hearingsDataWithAllocationDataAndJudiciary(caseAndDefendantData, crownCourtCenterId, crownCourtRoomId, hearingEndDate, hearingStartTime)); testDataList.add(new TestData(hearingStartTime.toLocalDate(), crownCourtRoomId, COURT_ROOMS.get(crownCourtRoomId), hearingStartTime)); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForMagistratesIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForMagistratesIT.java index 80017a2d0..56f557dfc 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForMagistratesIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RangeSearchQueryForMagistratesIT.java @@ -30,7 +30,6 @@ import uk.gov.justice.services.test.utils.core.http.ResponseData; import uk.gov.moj.cpp.listing.steps.ListCourtHearingSteps; import uk.gov.moj.cpp.listing.steps.data.CaseAndDefendantData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.io.IOException; import java.time.Instant; @@ -127,7 +126,7 @@ void shouldReturnNotesAndHearingsForMagistratesRangeSearchIfHearingInCourtSchedu JsonArray hearingDaysJsonArr = hearingsJsonArr.getJsonObject(0).getJsonArray("hearingDays"); assertThat(hearingDaysJsonArr.getJsonObject(0).getString("courtScheduleId"), is(bookedSlotsJsonArr.getJsonObject(0).getString("courtScheduleId"))); - final Instant instant = ItClock.nowInstant(); + final Instant instant = Instant.now(); stubGetHearingIds(instant); params.put(EXACT_HEARING_START_DATETIME, instant.toString()); final String queryString2 = getQueryString(params); @@ -162,7 +161,7 @@ void shouldReturnNothingForMagistrateRangeSearchIfNoHearingInCourtScheduler() { caseUrnForLinkedCases, caseUrnForLinkedCases); ListCourtHearingSteps listCourtHearingSteps1 = new ListCourtHearingSteps(hearingsDataWithAllocationDataAndJudiciary(caseAndDefendantData1)); - listCourtHearingSteps1.createListingNotes(ItClock.today().plusDays(1), "note 1"); + listCourtHearingSteps1.createListingNotes(LocalDate.now().plusDays(1), "note 1"); final ZonedDateTime hearingStartTime = listCourtHearingSteps1.getHearingsData().getHearingData().get(0).getHearingStartTime(); final LocalDate hearingDate = hearingStartTime.toLocalDate(); final UUID courtroomId = listCourtHearingSteps1.getHearingsData().getHearingData().get(0).getCourtRoomId(); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RestrictListFromCourtIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RestrictListFromCourtIT.java index 8a2e6f1ac..289317b95 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RestrictListFromCourtIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/RestrictListFromCourtIT.java @@ -19,7 +19,6 @@ import uk.gov.moj.cpp.listing.steps.RestrictCourtListSteps; import uk.gov.moj.cpp.listing.steps.data.CourtCentreData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.time.LocalTime; @@ -92,73 +91,6 @@ void shouldRestrictCourtApplicationTypeFromCourtForHearingId() { restrictCourtListSteps.verifyCourtApplicationOrApplicantOrRespondentListingRestrictedInHearing(false, false, false, true); } - @Test - void shouldRestrictCourtApplicationRespondentFromCourtForHearingId() { - HearingsData hearingsData = HearingsData.hearingsData(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); - - final RestrictCourtListSteps restrictCourtListSteps = new RestrictCourtListSteps(hearingsData); - restrictCourtListSteps.whenRestrictingCaseOrStandaloneApplicationForCourtListing(restrictCourtListSteps.getCourtApplicationRespondentDataToBeRestricted(hearingsData)); - restrictCourtListSteps.verifyCourtApplicationOrApplicantOrRespondentListingRestrictedInHearing(false, false, true, false); - } - - @Test - void shouldUnRestrictCourtApplicationApplicantFromCourtForHearingId() { - HearingsData hearingsData = HearingsData.hearingsData(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); - - final RestrictCourtListSteps restrictCourtListSteps = new RestrictCourtListSteps(hearingsData); - restrictCourtListSteps.whenRestrictingCaseOrStandaloneApplicationForCourtListing(restrictCourtListSteps.getCourtApplicationDataToBeRestricted(hearingsData)); - restrictCourtListSteps.verifyCourtApplicationOrApplicantOrRespondentListingRestrictedInHearing(true, true, false, false); - - restrictCourtListSteps.whenRestrictingCaseOrStandaloneApplicationForCourtListing(restrictCourtListSteps.getCourtApplicationDataToBeUnrestricted(hearingsData)); - restrictCourtListSteps.verifyCourtApplicationOrApplicantOrRespondentListingRestrictedInHearing(false, false, false, false); - restrictCourtListSteps.verifyPublicCourtListRestrictedEvent(false); - } - - @Test - void shouldRestrictBothApplicantAndRespondentOfCourtApplicationFromCourtForHearingId() { - HearingsData hearingsData = HearingsData.hearingsData(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); - - final RestrictCourtListSteps restrictCourtListSteps = new RestrictCourtListSteps(hearingsData); - restrictCourtListSteps.whenRestrictingCaseOrStandaloneApplicationForCourtListing(restrictCourtListSteps.getCourtApplicationApplicantAndRespondentDataToBeRestricted(hearingsData)); - restrictCourtListSteps.verifyCourtApplicationOrApplicantOrRespondentListingRestrictedInHearing(true, true, true, false); - restrictCourtListSteps.verifyPublicCourtListRestrictedEventWithApplicantAndRespondent(true); - } - - @Test - void shouldRestrictStandaloneApplicationApplicantFromCourtForHearingId() { - HearingsData hearingsData = HearingsData.hearingsDataStandaloneApplication(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListingStandaloneApplication(); - listCourtHearingSteps.verifyHearingListedFromAPIForStandaloneApplication(UNALLOCATED); - - final RestrictCourtListSteps restrictCourtListSteps = new RestrictCourtListSteps(hearingsData); - restrictCourtListSteps.whenRestrictingCaseOrStandaloneApplicationForCourtListing(restrictCourtListSteps.getCourtApplicationDataToBeRestricted(hearingsData)); - restrictCourtListSteps.verifyCourtApplicationOrApplicantOrRespondentListingRestrictedInHearing(true, true, false, false); - restrictCourtListSteps.verifyPublicCourtListRestrictedEventWithApplicant(true); - } - - @Test - void shouldRestrictCourtApplicationSubjectFromCourtForHearingId() { - HearingsData hearingsData = HearingsData.hearingsDataStandaloneApplicationWithSubject(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListingStandaloneApplication(); - listCourtHearingSteps.verifyHearingListedFromAPIForStandaloneApplication(UNALLOCATED); - - final RestrictCourtListSteps restrictCourtListSteps = new RestrictCourtListSteps(hearingsData); - restrictCourtListSteps.whenRestrictingCaseOrStandaloneApplicationForCourtListing(restrictCourtListSteps.getCourtApplicationSubjectDataToBeRestricted(hearingsData)); - restrictCourtListSteps.verifyCourtApplicationSubjectListingRestrictedInHearing(true); - restrictCourtListSteps.verifyPublicCourtListRestrictedEventWithSubject(true); - } - @Test void shouldPublishCourtListWithHearingsWithDefendantNameMasking() throws Exception { final ViewStoreCleaner viewStoreCleaner = new ViewStoreCleaner(); @@ -169,7 +101,7 @@ void shouldPublishCourtListWithHearingsWithDefendantNameMasking() throws Excepti final UUID courtListId = randomUUID(); final int courtRoomId = 231; final PublishCourtListType publishCourtListType = PublishCourtListType.FIRM; - final LocalDate startDate = ItClock.today(); + final LocalDate startDate = LocalDate.now(); stubGetReferenceDataCourtCentreById(courtCentreId); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/SearchAvailableJudiciariesIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/SearchAvailableJudiciariesIT.java new file mode 100644 index 000000000..a23a610d3 --- /dev/null +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/SearchAvailableJudiciariesIT.java @@ -0,0 +1,81 @@ +package uk.gov.moj.cpp.listing.it; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; +import static java.lang.String.format; +import static javax.ws.rs.core.Response.Status.OK; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static uk.gov.justice.services.test.utils.core.http.BaseUriProvider.getBaseUri; +import static uk.gov.justice.services.test.utils.core.http.RequestParamsBuilder.requestParams; +import static uk.gov.justice.services.test.utils.core.matchers.ResponsePayloadMatcher.payload; +import static uk.gov.justice.services.test.utils.core.matchers.ResponseStatusMatcher.status; +import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.pollWithDefaults; +import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubSearchAvailableJudiciaries; +import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubSearchAvailableJudiciariesBadRequest; + +import uk.gov.justice.services.test.utils.core.http.RequestParams; +import uk.gov.justice.services.test.utils.core.http.ResponseData; +import uk.gov.justice.services.test.utils.core.rest.RestClient; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.Response; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +class SearchAvailableJudiciariesIT extends AbstractIT { + + private static final String QUERY_API_PATH = "/listing-query-api/query/api/rest/listing/judiciaries/search-available"; + private static final String QUERY_MEDIA_TYPE = "application/vnd.listing.search.available.judiciaries+json"; + + @Test + void shouldProxySearchAvailableJudiciariesFromCourtScheduler() { + final Map params = new HashMap<>(); + params.put("search", "ai"); + final String queryString = getQueryString(params); + + stubSearchAvailableJudiciaries(); + + final RequestParams requestParams = getRequestParams(queryString); + final ResponseData responseData = pollWithDefaults(requestParams).until(status().is(OK), + payload().isJson(allOf( + withJsonPath("$.judiciaries.size()", is(1)), + withJsonPath("$.judiciaries[0].id", is("9f39f876-3ff6-32b5-926e-c588e36a87b8")), + withJsonPath("$.judiciaries[0].surname", is("Ainsworth")), + withJsonPath("$.judiciaries[0].specialisms.size()", is(2)) + )) + ); + + assertThat(responseData.getStatus().getStatusCode(), is(HttpStatus.SC_OK)); + } + + @Test + void shouldPropagateBadRequestFromCourtScheduler() { + final Map params = new HashMap<>(); + params.put("search", "x"); + final String queryString = getQueryString(params); + + stubSearchAvailableJudiciariesBadRequest(); + + final RequestParams requestParams = getRequestParams(queryString); + final ResponseData responseData = queryService(requestParams); + + assertThat(responseData.getStatus().getStatusCode(), is(HttpStatus.SC_BAD_REQUEST)); + assertThat(responseData.getPayload(), is("search text too short")); + } + + private RequestParams getRequestParams(final String queryString) { + final String url = format("%s%s%s%s", getBaseUri(), QUERY_API_PATH, "?", queryString); + return requestParams(url, QUERY_MEDIA_TYPE) + .withHeader(CPP_UID_HEADER.getName(), CPP_UID_HEADER.getValue()) + .build(); + } + + private ResponseData queryService(final RequestParams requestParams) { + final Response response = new RestClient().query(requestParams.getUrl(), requestParams.getMediaType(), requestParams.getHeaders()); + return new ResponseData(Response.Status.fromStatusCode(response.getStatus()), response.readEntity(String.class), response.getHeaders()); + } +} diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ServerLogTestMarkerExtension.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ServerLogTestMarkerExtension.java deleted file mode 100644 index 032464591..000000000 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ServerLogTestMarkerExtension.java +++ /dev/null @@ -1,274 +0,0 @@ -package uk.gov.moj.cpp.listing.it; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.nio.file.StandardOpenOption.APPEND; -import static java.nio.file.StandardOpenOption.CREATE; -import static java.nio.file.StandardOpenOption.WRITE; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Locale; -import java.util.Optional; - -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.TestWatcher; -import org.junit.platform.commons.support.AnnotationSupport; - -/** - * Appends test lifecycle marker lines directly to the Wildfly server.log so that every - * ERROR/WARN in the log can be attributed to the exact test that was executing. - * - *

The test JVM and Wildfly are separate processes, but Wildfly's log directory is - * bind-mounted on the host ({@code $CPP_DOCKER_DIR/containers/wildfly/log/server.log}), - * so this extension simply appends to the same file. Appends are single {@code write()} - * calls in {@code O_APPEND} mode, which Linux interleaves safely with Wildfly's own writes.

- * - *

Markers produced (timestamp format matches Wildfly's own, UTC):

- *
- * ==== [IT-CLASS-START] CrownUpdateHearingMultidayIT ====
- * ---- [TEST-START] CrownUpdateHearingMultidayIT.shouldExtendHearing ----
- * ---- [TEST-END][SUCCESS] CrownUpdateHearingMultidayIT.shouldExtendHearing ----
- * ---- [TEST-START][EXPECTED-ERRORS] CrownUpdateHearingMultidayIT.shouldReturn422... -- expects: courtscheduler 422 NO_AVAILABILITY ----
- * ---- [TEST-END][SUCCESS][EXPECTED-ERRORS] CrownUpdateHearingMultidayIT.shouldReturn422... ----
- * ==== [IT-CLASS-END] CrownUpdateHearingMultidayIT ====
- * 
- * - *

Triage rules:

- *
    - *
  • ERROR/WARN between a [TEST-START] without [EXPECTED-ERRORS] and its - * [TEST-END] = genuine problem in that test's window — investigate.
  • - *
  • ERROR/WARN inside an [EXPECTED-ERRORS] window = deliberate negative scenario - * (see the {@link ExpectedServerErrors} reason text on the marker) — ignore.
  • - *
  • ERROR/WARN between a [TEST-END] and the next [TEST-START] = async straggler - * (in-flight JMS/event-processor work outlasting its test) — a missing await/drain - * in the test that just ended, or the known teardown race.
  • - *
- * - *

The [TEST-START] marker is written before {@code AbstractIT.setUp()} (extension - * callbacks precede {@code @BeforeEach} methods) and [TEST-END] after {@code @AfterEach}, - * so setup/teardown errors are attributed to the right test as well.

- * - *

Resolution of the log path: {@code -Dserver.log.path} override, then - * {@code $CPP_DOCKER_DIR}, then the conventional {@code ~/gitrepos/cpp-developers-docker} - * location. If the directory does not exist (e.g. tests running against a remote stack), - * the extension is a silent no-op — it must never fail a test.

- */ -public class ServerLogTestMarkerExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, TestWatcher { - - private static final DateTimeFormatter LOG_TIMESTAMP = - DateTimeFormatter.ofPattern("dd/MMM/yyyy:HH:mm:ss 'UTC'", Locale.ENGLISH).withZone(ZoneOffset.UTC); - - private static final Path SERVER_LOG = resolveServerLogPath(); - - @Override - public void beforeAll(final ExtensionContext context) { - writeMarker("==== [IT-CLASS-START] " + context.getRequiredTestClass().getSimpleName() + " ===="); - } - - @Override - public void afterAll(final ExtensionContext context) { - writeMarker("==== [IT-CLASS-END] " + context.getRequiredTestClass().getSimpleName() + " ===="); - } - - @Override - public void beforeEach(final ExtensionContext context) { - final Optional expected = findExpectedServerErrors(context); - final String tag = expected.isPresent() ? "[TEST-START][EXPECTED-ERRORS]" : "[TEST-START]"; - final String reason = expected.map(annotation -> " -- expects: " + annotation.value()).orElse(""); - writeMarker("---- " + tag + " " + testName(context) + reason + " ----"); - rememberWindowStartOffset(context); - } - - /** - * Scans this test's server.log window for unexpected ERROR/WARN lines: anything that is not - * (a) sanctioned by {@link ExpectedServerErrors} or (b) a documented framework-race floor - * signature. Findings are reported in the end-of-run summary - * ({@link ServerLogUnexpectedErrorSummary}); with {@code -Dserver.log.failOnUnexpectedErrors=true} - * the owning test fails instead (off by default — the floor races land in arbitrary windows - * and would make runs flaky). - */ - @Override - public void afterEach(final ExtensionContext context) { - if (findExpectedServerErrors(context).isPresent()) { - return; // sanctioned negative-scenario window - } - final long startOffset = windowStartOffset(context); - if (startOffset < 0) { - return; // marker file unavailable on this machine - } - final String window = readLogWindow(startOffset); - final List unexpected = findUnexpectedErrorLines(window); - if (!unexpected.isEmpty()) { - ServerLogUnexpectedErrorSummary.record(testName(context), unexpected); - if (FAIL_ON_UNEXPECTED_ERRORS) { - throw new AssertionError("Unexpected server.log ERROR/WARN lines during this test " - + "(see markers in server.log; annotate with @ExpectedServerErrors only if deliberate):\n " - + String.join("\n ", unexpected)); - } - } - } - - @Override - public void testSuccessful(final ExtensionContext context) { - writeTestEnd(context, "SUCCESS"); - } - - @Override - public void testFailed(final ExtensionContext context, final Throwable cause) { - writeTestEnd(context, "FAILED"); - } - - @Override - public void testAborted(final ExtensionContext context, final Throwable cause) { - writeTestEnd(context, "ABORTED"); - } - - @Override - public void testDisabled(final ExtensionContext context, final Optional reason) { - writeMarker("---- [TEST-DISABLED] " + testName(context) + " ----"); - } - - private void writeTestEnd(final ExtensionContext context, final String outcome) { - final String expectedTag = findExpectedServerErrors(context).isPresent() ? "[EXPECTED-ERRORS]" : ""; - writeMarker("---- [TEST-END][" + outcome + "]" + expectedTag + " " + testName(context) + " ----"); - } - - // ---- unexpected-error scanning ------------------------------------------------------------ - - private static final boolean FAIL_ON_UNEXPECTED_ERRORS = Boolean.getBoolean("server.log.failOnUnexpectedErrors"); - - /** Extra allowlist regex appended via -Dserver.log.error.allowlist= (optional). */ - private static final String CUSTOM_ALLOWLIST = System.getProperty("server.log.error.allowlist", ""); - - private static final ExtensionContext.Namespace NAMESPACE = - ExtensionContext.Namespace.create(ServerLogTestMarkerExtension.class); - - /** - * Documented framework-race floor (see memory/triage doc): async tail of test N crossing - * test N+1's reset()/truncate. Self-heals on redelivery, no DLQ, also present on main. - */ - private static final java.util.regex.Pattern FLOOR_NOISE = java.util.regex.Pattern.compile( - "No stream not found" // EVENT_LISTENER stream_status select-for-update race - + "|Failed to find Event with id" // AsynchronousPrePublisher vs event-store truncate - + "|AMQ212009: resetting session"); // Artemis companion WARN of any rolled-back delivery - - private void rememberWindowStartOffset(final ExtensionContext context) { - long offset = -1; - try { - if (Files.isRegularFile(SERVER_LOG)) { - offset = Files.size(SERVER_LOG); - } - } catch (final IOException ignored) { - // scanning is best-effort; never fail a test over it - } - context.getStore(NAMESPACE).put("windowStart", offset); - } - - private long windowStartOffset(final ExtensionContext context) { - final Long offset = context.getStore(NAMESPACE).get("windowStart", Long.class); - return offset == null ? -1 : offset; - } - - private static String readLogWindow(final long startOffset) { - try (java.io.RandomAccessFile raf = new java.io.RandomAccessFile(SERVER_LOG.toFile(), "r")) { - final long end = raf.length(); - if (end <= startOffset) { - return ""; - } - final byte[] bytes = new byte[(int) Math.min(end - startOffset, 64L * 1024 * 1024)]; - raf.seek(startOffset); - raf.readFully(bytes); - return new String(bytes, UTF_8); - } catch (final IOException e) { - return ""; - } - } - - private static List findUnexpectedErrorLines(final String window) { - // Companion suppression: a stream-status race manifests as a WFLYEJB0034+AMQ154004 pair - // whose MAIN line only says "Failed to process event..." — the discriminating cause - // ("No stream not found") is in the stack lines below. Suppress those companions only - // when the race signature is actually present in this window, so a genuinely new - // listener failure still surfaces. - final boolean streamRaceInWindow = window.contains("No stream not found"); - final List unexpected = new java.util.ArrayList<>(); - for (String line : window.split("\n")) { - line = line.replaceAll("\\[[0-9;]*m", ""); - if (!line.contains(" ERROR ") && !line.contains(" WARN ")) { - continue; - } - if (line.contains("[TEST-") || line.contains("[IT-CLASS")) { - continue; // our own markers (reason text may contain the words ERROR/WARN) - } - if (FLOOR_NOISE.matcher(line).find()) { - continue; - } - if (streamRaceInWindow && line.contains("Failed to process event")) { - continue; - } - if (!CUSTOM_ALLOWLIST.isBlank() && line.matches(".*(" + CUSTOM_ALLOWLIST + ").*")) { - continue; - } - unexpected.add(line); - } - return unexpected; - } - - private static Optional findExpectedServerErrors(final ExtensionContext context) { - final Optional onMethod = context.getTestMethod() - .flatMap(method -> AnnotationSupport.findAnnotation(method, ExpectedServerErrors.class)); - if (onMethod.isPresent()) { - return onMethod; - } - return context.getTestClass() - .flatMap(testClass -> AnnotationSupport.findAnnotation(testClass, ExpectedServerErrors.class)); - } - - private static String testName(final ExtensionContext context) { - final String className = context.getTestClass().map(Class::getSimpleName).orElse("?"); - final String methodName = context.getTestMethod().map(java.lang.reflect.Method::getName).orElse("?"); - final String displayName = context.getDisplayName(); - // Parameterized/display-named tests: include the display name for disambiguation - if (!displayName.equals(methodName + "()") && !displayName.equals(methodName)) { - return className + "." + methodName + " (" + displayName + ")"; - } - return className + "." + methodName; - } - - private static void writeMarker(final String text) { - if (!Files.isDirectory(SERVER_LOG.getParent())) { - return; // wildfly log dir not mounted on this machine — markers are a local-stack aid only - } - // Leading newline guards against landing mid-line if Wildfly is mid-write; single - // write() call keeps the marker atomic w.r.t. Wildfly's own appends. - final String line = "\n" + LOG_TIMESTAMP.format(Instant.now()) + " " + text + "\n"; - try { - Files.write(SERVER_LOG, line.getBytes(UTF_8), CREATE, WRITE, APPEND); - } catch (final IOException ignored) { - // never fail a test because a log marker could not be written - } - } - - private static Path resolveServerLogPath() { - final String override = System.getProperty("server.log.path"); - if (override != null && !override.isBlank()) { - return Paths.get(override); - } - final String dockerDir = System.getenv("CPP_DOCKER_DIR"); - if (dockerDir != null && !dockerDir.isBlank()) { - return Paths.get(dockerDir, "containers", "wildfly", "log", "server.log"); - } - return Paths.get(System.getProperty("user.home"), - "gitrepos", "cpp-developers-docker", "containers", "wildfly", "log", "server.log"); - } -} diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ServerLogUnexpectedErrorSummary.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ServerLogUnexpectedErrorSummary.java deleted file mode 100644 index ea3e49c35..000000000 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/ServerLogUnexpectedErrorSummary.java +++ /dev/null @@ -1,86 +0,0 @@ -package uk.gov.moj.cpp.listing.it; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.junit.platform.launcher.TestExecutionListener; -import org.junit.platform.launcher.TestPlan; - -/** - * Collects unexpected server.log ERROR/WARN lines found by {@link ServerLogTestMarkerExtension} - * during each test's window and prints a consolidated summary at the END of the test run - * (registered via META-INF/services/org.junit.platform.launcher.TestExecutionListener, so the - * summary lands in the failsafe console output, after the last test class). - * - *

The summary is also written to {@code target/unexpected-server-errors.txt} so CI archives it - * even when console output is truncated.

- * - *

An "unexpected" line is any ERROR/WARN inside a test's [TEST-START]→test-end window that is - * NOT (a) inside an {@link ExpectedServerErrors} window, and NOT (b) one of the documented - * framework-race floor signatures (see {@link ServerLogTestMarkerExtension#isFloorNoise}).

- */ -public class ServerLogUnexpectedErrorSummary implements TestExecutionListener { - - /** One finding = one test window that contained unexpected ERROR/WARN lines. */ - public static final class Finding { - final String testName; - final List lines; - - Finding(final String testName, final List lines) { - this.testName = testName; - this.lines = lines; - } - } - - private static final List FINDINGS = Collections.synchronizedList(new ArrayList<>()); - private static final int MAX_LINE_LENGTH_IN_SUMMARY = 300; - - public static void record(final String testName, final List unexpectedLines) { - FINDINGS.add(new Finding(testName, unexpectedLines)); - } - - @Override - public void testPlanExecutionFinished(final TestPlan testPlan) { - final StringBuilder summary = new StringBuilder(); - summary.append("\n=================== SERVER.LOG UNEXPECTED ERROR SUMMARY ===================\n"); - if (FINDINGS.isEmpty()) { - summary.append("server.log CLEAN: no unexpected ERROR/WARN lines outside [EXPECTED-ERRORS] windows.\n"); - } else { - summary.append(FINDINGS.size()).append(" test window(s) contained unexpected server.log ERROR/WARN lines\n") - .append("(framework-race floor signatures and @ExpectedServerErrors windows already filtered):\n"); - synchronized (FINDINGS) { - for (final Finding finding : FINDINGS) { - summary.append("\n ").append(finding.testName).append("\n"); - for (final String line : finding.lines) { - summary.append(" ") - .append(line, 0, Math.min(line.length(), MAX_LINE_LENGTH_IN_SUMMARY)) - .append("\n"); - } - } - } - summary.append("\nInvestigate these before merging; happy-path tests must keep server.log clean.\n") - .append("Re-run with -Dserver.log.failOnUnexpectedErrors=true to fail the owning test instead.\n"); - } - summary.append("============================================================================\n"); - - System.out.println(summary); - writeReportFile(summary.toString()); - } - - private void writeReportFile(final String summary) { - try { - final Path reportFile = Paths.get("target", "unexpected-server-errors.txt"); - Files.createDirectories(reportFile.getParent()); - Files.write(reportFile, summary.getBytes(UTF_8)); - } catch (final IOException ignored) { - // the console summary is the primary output; never fail the run over the report file - } - } -} diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/SessionAvailabilityValidationIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/SessionAvailabilityValidationIT.java new file mode 100644 index 000000000..3570d0abb --- /dev/null +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/SessionAvailabilityValidationIT.java @@ -0,0 +1,54 @@ +package uk.gov.moj.cpp.listing.it; + +import static java.text.MessageFormat.format; +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.OK; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubValidateSessionAvailability; +import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubValidateSessionAvailabilityFailure; +import static uk.gov.moj.cpp.listing.utils.PropertyUtil.getBaseUri; +import static uk.gov.moj.cpp.listing.utils.PropertyUtil.readConfig; + +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.junit.jupiter.api.Test; + +class SessionAvailabilityValidationIT extends AbstractIT { + + private static final String VALIDATE_SESSION_AVAILABILITY_URL = "listing.query.validate-session-availability"; + private static final String CONTENT_TYPE = "application/vnd.listing.validate.session.availability+json"; + + private static final String VALID_PAYLOAD = + "{\"courtScheduleIdList\":[{\"courtScheduleId\":\"f8254db1-1683-483e-afb3-b87fde5a0a26\"}],\"duration\":30}"; + private static final String PAYLOAD_WITHOUT_DURATION = + "{\"courtScheduleIdList\":[{\"courtScheduleId\":\"f8254db1-1683-483e-afb3-b87fde5a0a26\"}]}"; + + @Test + void shouldReturnOkWhenCourtSchedulerValidatesSuccessfully() { + stubValidateSessionAvailability(); + + final Response response = postToValidateSessionAvailability(VALID_PAYLOAD); + + assertThat(response.getStatus(), is(OK.getStatusCode())); + } + + @Test + void shouldPassThroughCourtSchedulerErrorResponse() { + stubValidateSessionAvailabilityFailure(); + + final Response response = postToValidateSessionAvailability(PAYLOAD_WITHOUT_DURATION); + + assertThat(response.getStatus(), is(BAD_REQUEST.getStatusCode())); + } + + private Response postToValidateSessionAvailability(final String payload) { + final String url = String.format("%s/%s", getBaseUri(), + format(readConfig().getProperty(VALIDATE_SESSION_AVAILABILITY_URL))); + final MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(CPP_UID_HEADER.getName(), CPP_UID_HEADER.getValue()); + return restClient.postCommand(url, CONTENT_TYPE, payload, headers); + } +} diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/VacateHearingIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/VacateHearingIT.java index 81995e278..b20a49d0e 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/VacateHearingIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/VacateHearingIT.java @@ -7,6 +7,7 @@ import static uk.gov.moj.cpp.listing.steps.data.factory.HearingsDataFactory.CROWN_JURISDICTION; import static uk.gov.moj.cpp.listing.steps.data.factory.HearingsDataFactory.MAGISTRATES_JURISDICTION; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubDeleteAvailableHearingSlotsService; +import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetCourtSchedulesByIdWithDraftStatus; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessions; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubProvisionalBookingWithCustomParams; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.verifyDeleteAvailableHearingSlotsStubCommandInvoked; @@ -136,15 +137,23 @@ void shouldNotVacateMagistrateCourtHearingAndNotAttemptToFreeHearingSlotsWhenRea } @Test - void shouldVacateCrownCourtHearingAndNotAttemptToFreeHearingSlots() { + void shouldVacateCrownCourtHearingAndFreeHearingSlots() { final HearingsData hearingsData = hearingsDataWithAllocationDataAndJudiciary(CROWN_JURISDICTION); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); + final String hearingId = hearingsData.getHearingData().get(0).getId().toString(); + final String courtScheduleId = "8e837de0-743a-4a2c-9db3-b2e678c48729"; + + stubGetCourtSchedulesByIdWithDraftStatus(java.util.Collections.singletonList(courtScheduleId), false); + stubListHearingInCourtSessions(hearingId, courtScheduleId, + hearingsData.getHearingData().get(0).getHearingStartTime()); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(ALLOCATED); + stubDeleteAvailableHearingSlotsService(hearingId); final VacatingTrialSteps vacatingTrialSteps = new VacatingTrialSteps(hearingsData); vacatingTrialSteps.whenPublicEventHearingTrialVacatedIsPublished(); vacatingTrialSteps.verifyVacatedTrialWhenQueryingFromAPI(); + verifyDeleteAvailableHearingSlotsStubCommandInvoked(hearingId); } @ParameterizedTest diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/WeekCommencingHearingIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/WeekCommencingHearingIT.java index 032f21fe9..5a78d82dc 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/WeekCommencingHearingIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/WeekCommencingHearingIT.java @@ -1,5 +1,6 @@ package uk.gov.moj.cpp.listing.it; +import static java.time.LocalDate.now; import static uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData.updatedHearingData; import static uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData.updatedHearingDataWithWeekCommencingDate; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetCourtSchedulesByIdWithDraftStatus; @@ -10,10 +11,8 @@ import uk.gov.moj.cpp.listing.steps.WeekCommencingHearingSteps; import uk.gov.moj.cpp.listing.steps.data.HearingsData; import uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; -import java.time.ZoneOffset; import java.util.Collections; import org.junit.jupiter.api.Disabled; @@ -24,12 +23,12 @@ public class WeekCommencingHearingIT extends AbstractIT { @Test public void shouldUpdateHearingWithWeekCommencingDatesAndKeepItUnallocated() { - final HearingsData hearingsData = HearingsData.hearingsDataForWeekCommencing(ItClock.today(), 1); + final HearingsData hearingsData = HearingsData.hearingsDataForWeekCommencing(LocalDate.now(), 1); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); - final UpdatedHearingData updatedHearingDataWithWeekCommencingDate = updatedHearingDataWithWeekCommencingDate(hearingsData.getHearingData().get(0), ItClock.today().plusDays(1).toString(), ItClock.today().plusDays(7l).toString(), 1); + final UpdatedHearingData updatedHearingDataWithWeekCommencingDate = updatedHearingDataWithWeekCommencingDate(hearingsData.getHearingData().get(0), now().plusDays(1).toString(), now().plusDays(7l).toString(), 1); final WeekCommencingHearingSteps weekCommencingHearingSteps = new WeekCommencingHearingSteps(updatedHearingDataWithWeekCommencingDate); weekCommencingHearingSteps.whenHearingIsUpdatedForListingForWeekCommencingDate(); @@ -39,13 +38,13 @@ public void shouldUpdateHearingWithWeekCommencingDatesAndKeepItUnallocated() { @Test public void shouldUpdateUpdateHearingWithWeekCommencingDatesToFixedDatesAndAllocateHearing() { - final HearingsData hearingsData = HearingsData.hearingsDataForWeekCommencing(ItClock.today(), 1); + final HearingsData hearingsData = HearingsData.hearingsDataForWeekCommencing(LocalDate.now(), 1); final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); listCourtHearingSteps.whenCaseIsSubmittedForListing(); listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); - final UpdatedHearingData updatedHearingDataWithWeekCommencingDate = updatedHearingDataWithWeekCommencingDate(hearingsData.getHearingData().get(0), ItClock.today().plusDays(1).toString(), ItClock.today().plusDays(7l).toString(), 1); + final UpdatedHearingData updatedHearingDataWithWeekCommencingDate = updatedHearingDataWithWeekCommencingDate(hearingsData.getHearingData().get(0), now().plusDays(1).toString(), now().plusDays(7l).toString(), 1); final WeekCommencingHearingSteps weekCommencingHearingSteps = new WeekCommencingHearingSteps(updatedHearingDataWithWeekCommencingDate); weekCommencingHearingSteps.whenHearingIsUpdatedForListingForWeekCommencingDate(); @@ -55,20 +54,10 @@ public void shouldUpdateUpdateHearingWithWeekCommencingDatesToFixedDatesAndAlloc final UpdatedHearingData updatedHearingDataForUnallocation = updatedHearingData(hearingsData.getHearingData().get(0)); final String courtScheduleId = updatedHearingDataForUnallocation.getNonDefaultDays().get(0).getCourtScheduleId().orElseThrow(); - // Rich overload: the single-day UPDATE enrichment sanity-checks hearingDate against the - // session's sessionDate, so the stub must agree with the update payload's nonDefaultDay. - final LocalDate updatedStartDate = LocalDate.parse(updatedHearingDataForUnallocation.getStartDate()); - final java.time.ZonedDateTime updatedStartTime = updatedStartDate.atTime(10, 0).atZone(ZoneOffset.UTC); - stubGetCourtSchedulesByIdWithDraftStatus(Collections.singletonList(courtScheduleId), false, - updatedStartDate, - updatedHearingDataForUnallocation.getCourtCentreId(), - updatedHearingDataForUnallocation.getCourtRoomId(), - updatedStartTime); - // Sessions listing must also reflect the UPDATED start time — the enrichment rebuilds - // the hearing day from this response (stale seed-time values would revert the date). + stubGetCourtSchedulesByIdWithDraftStatus(Collections.singletonList(courtScheduleId), false); stubListHearingInCourtSessions(hearingsData.getHearingData().get(0).getId().toString(), courtScheduleId, - updatedStartTime); + hearingsData.getHearingData().get(0).getHearingStartTime()); final UpdateHearingSteps updateHearingSteps = new UpdateHearingSteps(hearingsData, updatedHearingDataForUnallocation); updateHearingSteps.whenHearingIsUpdatedForListing(); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/YouthCourtListRestrictionIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/YouthCourtListRestrictionIT.java deleted file mode 100644 index 27486a3b7..000000000 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/YouthCourtListRestrictionIT.java +++ /dev/null @@ -1,222 +0,0 @@ -package uk.gov.moj.cpp.listing.it; - -import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; -import static org.hamcrest.CoreMatchers.equalTo; -import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearing; -import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearingByWeekCommencing; -import static uk.gov.moj.cpp.listing.steps.data.HearingsData.hearingsDataForYoungCourtApplicationRespondent; -import static uk.gov.moj.cpp.listing.steps.data.HearingsData.hearingsDataForYoungCourtApplicationSubject; -import static uk.gov.moj.cpp.listing.steps.data.HearingsData.hearingsDataForYoungDefendants; -import static uk.gov.moj.cpp.listing.steps.data.HearingsData.hearingsDataForWeekCommencingWithYoungDefendants; -import static uk.gov.moj.cpp.listing.steps.data.HearingsData.hearingsDataWithAdultDefendants; -import static uk.gov.moj.cpp.listing.steps.data.UpdatedDefendantData.updatedDefendantDataWithUnder18DateOfBirth; -import static uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData.updatedHearingDataForAllocation; -import static uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData.updatedHearingDataWithWeekCommencingDate; -import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetAvailableHearingSlotsWithQueryParams; -import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessionsWithMultipleSchedules; -import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCourtRoom; - -import uk.gov.moj.cpp.listing.steps.ListCourtHearingSteps; -import uk.gov.moj.cpp.listing.steps.UpdateDefendantSteps; -import uk.gov.moj.cpp.listing.steps.UpdateHearingSteps; -import uk.gov.moj.cpp.listing.steps.WeekCommencingHearingSteps; -import uk.gov.moj.cpp.listing.steps.data.CourtApplicationData; -import uk.gov.moj.cpp.listing.steps.data.DefendantData; -import uk.gov.moj.cpp.listing.steps.data.HearingData; -import uk.gov.moj.cpp.listing.steps.data.HearingsData; -import uk.gov.moj.cpp.listing.steps.data.UpdatedDefendantData; -import uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData; -import uk.gov.moj.cpp.listing.it.util.ItClock; - -import java.io.IOException; -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.UUID; - -import org.hamcrest.Matcher; -import org.junit.jupiter.api.Test; - -class YouthCourtListRestrictionIT extends AbstractIT { - - private static final LocalTime DEFAULT_START_TIME = LocalTime.of(10, 30); - private static final String DEFAULT_DURATION_HOURS_MINS = "6:30"; - - @Test - void shouldRestrictUnder18DefendantFromCourtListWhenHearingIsAllocated() throws IOException { - final HearingsData hearingsData = hearingsDataForYoungDefendants(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); - - final HearingData hearingData = hearingsData.getHearingData().get(0); - final UpdatedHearingData updatedHearingDataForAllocation = updatedHearingDataForAllocation(hearingData.getId()); - - stubGetReferenceDataCourtRoom(updatedHearingDataForAllocation.getCourtCentreId(), DEFAULT_START_TIME, DEFAULT_DURATION_HOURS_MINS, updatedHearingDataForAllocation.getCourtRoomId()); - - final UpdateHearingSteps updateHearingSteps = new UpdateHearingSteps(hearingsData, updatedHearingDataForAllocation); - stubGetAvailableHearingSlotsWithQueryParams(updateHearingSteps.getUpdatedHearingData()); - stubListHearingInCourtSessionsWithMultipleSchedules(updateHearingSteps.getUpdatedHearingData()); - updateHearingSteps.whenHearingIsUpdatedForListing(); - updateHearingSteps.verifyHearingAllocatedWhenQueryingFromAPI(); - - final String defendantId = hearingData.getListedCases().get(0).getDefendants().get(0).getDefendantId().toString(); - - pollForHearing(updatedHearingDataForAllocation.getCourtCentreId().toString(), ALLOCATED, getLoggedInUser().toString(), new Matcher[]{ - withJsonPath("$.hearings[0].id", equalTo(hearingData.getId().toString())), - withJsonPath("$.hearings[0].listedCases[0].defendants[0].id", equalTo(defendantId)), - withJsonPath("$.hearings[0].listedCases[0].defendants[0].restrictFromCourtList", equalTo(true)) - }); - } - - @Test - void shouldRestrictUnder18DefendantFromCourtListWhenWeekCommencingDateIsSet() throws IOException { - final LocalDate initialWeekCommencingStart = ItClock.today(); - final HearingsData hearingsData = hearingsDataForWeekCommencingWithYoungDefendants(initialWeekCommencingStart, 1); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); - - final HearingData hearingData = hearingsData.getHearingData().get(0); - final String defendantId = hearingData.getListedCases().get(0).getDefendants().get(0).getDefendantId().toString(); - - final LocalDate changedWeekCommencingStart = initialWeekCommencingStart.plusWeeks(3); - final LocalDate changedWeekCommencingEnd = changedWeekCommencingStart.plusWeeks(1).minusDays(1); - final UpdatedHearingData updatedWeekCommencingData = updatedHearingDataWithWeekCommencingDate( - hearingData, changedWeekCommencingStart, changedWeekCommencingEnd, 1); - - final WeekCommencingHearingSteps weekCommencingHearingSteps = new WeekCommencingHearingSteps(updatedWeekCommencingData); - weekCommencingHearingSteps.whenHearingIsUpdatedForListingForWeekCommencingDate(); - - pollForHearingByWeekCommencing( - updatedWeekCommencingData.getCourtCentreId().toString(), - UNALLOCATED, - updatedWeekCommencingData.getWeekCommencingStartDate(), - updatedWeekCommencingData.getWeekCommencingEndDate(), - getLoggedInUser().toString(), - new Matcher[]{ - withJsonPath("$.hearings[0].id", equalTo(hearingData.getId().toString())), - withJsonPath("$.hearings[0].weekCommencingStartDate", equalTo(updatedWeekCommencingData.getWeekCommencingStartDate())), - withJsonPath("$.hearings[0].weekCommencingEndDate", equalTo(updatedWeekCommencingData.getWeekCommencingEndDate())), - withJsonPath("$.hearings[0].listedCases[0].defendants[0].id", equalTo(defendantId)), - withJsonPath("$.hearings[0].listedCases[0].defendants[0].restrictFromCourtList", equalTo(true)) - }); - - final UpdatedHearingData updatedHearingDataForAllocation = updatedHearingDataForAllocation(hearingData.getId()); - - stubGetReferenceDataCourtRoom(updatedHearingDataForAllocation.getCourtCentreId(), DEFAULT_START_TIME, DEFAULT_DURATION_HOURS_MINS, updatedHearingDataForAllocation.getCourtRoomId()); - - final UpdateHearingSteps updateHearingSteps = new UpdateHearingSteps(hearingsData, updatedHearingDataForAllocation); - stubGetAvailableHearingSlotsWithQueryParams(updateHearingSteps.getUpdatedHearingData()); - stubListHearingInCourtSessionsWithMultipleSchedules(updateHearingSteps.getUpdatedHearingData()); - updateHearingSteps.whenHearingIsUpdatedForListing(); - updateHearingSteps.verifyHearingAllocatedWhenQueryingFromAPI(); - - pollForHearingByWeekCommencing( - updatedHearingDataForAllocation.getCourtCentreId().toString(), - ALLOCATED, - "1970-01-01", - "2100-12-31", - getLoggedInUser().toString(), - new Matcher[]{ - withJsonPath("$.hearings[0].id", equalTo(hearingData.getId().toString())), - withJsonPath("$.hearings[0].listedCases[0].defendants[0].id", equalTo(defendantId)), - withJsonPath("$.hearings[0].listedCases[0].defendants[0].restrictFromCourtList", equalTo(true)) - }); - } - - @Test - void shouldRestrictUnder18CourtApplicationRespondentFromCourtListWhenHearingIsAllocated() throws IOException { - final HearingsData hearingsData = hearingsDataForYoungCourtApplicationRespondent(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); - - final HearingData hearingData = hearingsData.getHearingData().get(0); - final CourtApplicationData courtApplicationData = hearingData.getCourtApplications().get(0); - final String respondentId = courtApplicationData.getRespondent().getId().toString(); - - final UpdatedHearingData updatedHearingDataForAllocation = updatedHearingDataForAllocation(hearingData.getId()); - stubGetReferenceDataCourtRoom(updatedHearingDataForAllocation.getCourtCentreId(), DEFAULT_START_TIME, DEFAULT_DURATION_HOURS_MINS, updatedHearingDataForAllocation.getCourtRoomId()); - - final UpdateHearingSteps updateHearingSteps = new UpdateHearingSteps(hearingsData, updatedHearingDataForAllocation); - stubGetAvailableHearingSlotsWithQueryParams(updateHearingSteps.getUpdatedHearingData()); - stubListHearingInCourtSessionsWithMultipleSchedules(updateHearingSteps.getUpdatedHearingData()); - updateHearingSteps.whenHearingIsUpdatedForListing(); - updateHearingSteps.verifyHearingAllocatedWhenQueryingFromAPI(); - - pollForHearing(updatedHearingDataForAllocation.getCourtCentreId().toString(), ALLOCATED, getLoggedInUser().toString(), new Matcher[]{ - withJsonPath("$.hearings[0].id", equalTo(hearingData.getId().toString())), - withJsonPath("$.hearings[0].courtApplications[0].id", equalTo(courtApplicationData.getId().toString())), - withJsonPath("$.hearings[0].courtApplications[0].respondents[0].id", equalTo(respondentId)), - withJsonPath("$.hearings[0].courtApplications[0].respondents[0].restrictFromCourtList", equalTo(true)) - }); - } - - @Test - void shouldRestrictUnder18CourtApplicationSubjectFromCourtListWhenHearingIsAllocated() throws IOException { - final HearingsData hearingsData = hearingsDataForYoungCourtApplicationSubject(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); - - final HearingData hearingData = hearingsData.getHearingData().get(0); - final CourtApplicationData courtApplicationData = hearingData.getCourtApplications().get(0); - final String subjectId = courtApplicationData.getSubject().getId().toString(); - - final UpdatedHearingData updatedHearingDataForAllocation = updatedHearingDataForAllocation(hearingData.getId()); - stubGetReferenceDataCourtRoom(updatedHearingDataForAllocation.getCourtCentreId(), DEFAULT_START_TIME, DEFAULT_DURATION_HOURS_MINS, updatedHearingDataForAllocation.getCourtRoomId()); - - final UpdateHearingSteps updateHearingSteps = new UpdateHearingSteps(hearingsData, updatedHearingDataForAllocation); - stubGetAvailableHearingSlotsWithQueryParams(updateHearingSteps.getUpdatedHearingData()); - stubListHearingInCourtSessionsWithMultipleSchedules(updateHearingSteps.getUpdatedHearingData()); - updateHearingSteps.whenHearingIsUpdatedForListing(); - updateHearingSteps.verifyHearingAllocatedWhenQueryingFromAPI(); - - pollForHearing(updatedHearingDataForAllocation.getCourtCentreId().toString(), ALLOCATED, getLoggedInUser().toString(), new Matcher[]{ - withJsonPath("$.hearings[0].id", equalTo(hearingData.getId().toString())), - withJsonPath("$.hearings[0].courtApplications[0].id", equalTo(courtApplicationData.getId().toString())), - withJsonPath("$.hearings[0].courtApplications[0].subject.id", equalTo(subjectId)), - withJsonPath("$.hearings[0].courtApplications[0].subject.restrictFromCourtList", equalTo(true)) - }); - } - - @Test - void shouldRestrictDefendantFromCourtListWhenAgeUpdatedToUnder18AndHearingIsAllocated() throws IOException { - final HearingsData hearingsData = hearingsDataWithAdultDefendants(); - final ListCourtHearingSteps listCourtHearingSteps = new ListCourtHearingSteps(hearingsData); - listCourtHearingSteps.whenCaseIsSubmittedForListing(); - listCourtHearingSteps.verifyHearingListedFromAPI(UNALLOCATED); - - final HearingData hearingData = hearingsData.getHearingData().get(0); - final DefendantData defendantData = hearingData.getListedCases().get(0).getDefendants().get(0); - final String defendantId = defendantData.getDefendantId().toString(); - final UUID caseId = hearingData.getListedCases().get(0).getCaseId(); - - pollForHearing(hearingData.getCourtCentreId().toString(), UNALLOCATED, getLoggedInUser().toString(), new Matcher[]{ - withJsonPath("$.hearings[0].id", equalTo(hearingData.getId().toString())), - withJsonPath("$.hearings[0].listedCases[0].defendants[0].id", equalTo(defendantId)), - withJsonPath("$.hearings[0].listedCases[0].defendants[0].restrictFromCourtList", equalTo(false)) - }); - - final UpdatedDefendantData updatedDefendantData = updatedDefendantDataWithUnder18DateOfBirth(defendantData); - final UpdateDefendantSteps updateDefendantSteps = new UpdateDefendantSteps(caseId, hearingData, updatedDefendantData); - updateDefendantSteps.whenPublicEventProgressionCaseDefendantsUpdatedIsPublished(); - updateDefendantSteps.verifyHearingListedFromAPIWithJmsDelay(UNALLOCATED, true); - - final UpdatedHearingData updatedHearingDataForAllocation = updatedHearingDataForAllocation(hearingData.getId()); - - stubGetReferenceDataCourtRoom(updatedHearingDataForAllocation.getCourtCentreId(), DEFAULT_START_TIME, DEFAULT_DURATION_HOURS_MINS, updatedHearingDataForAllocation.getCourtRoomId()); - - final UpdateHearingSteps updateHearingSteps = new UpdateHearingSteps(hearingsData, updatedHearingDataForAllocation); - stubGetAvailableHearingSlotsWithQueryParams(updateHearingSteps.getUpdatedHearingData()); - stubListHearingInCourtSessionsWithMultipleSchedules(updateHearingSteps.getUpdatedHearingData()); - updateHearingSteps.whenHearingIsUpdatedForListing(); - updateHearingSteps.verifyHearingAllocatedWhenQueryingFromAPI(); - - pollForHearing(updatedHearingDataForAllocation.getCourtCentreId().toString(), ALLOCATED, getLoggedInUser().toString(), new Matcher[]{ - withJsonPath("$.hearings[0].id", equalTo(hearingData.getId().toString())), - withJsonPath("$.hearings[0].listedCases[0].defendants[0].id", equalTo(defendantId)), - withJsonPath("$.hearings[0].listedCases[0].defendants[0].restrictFromCourtList", equalTo(true)) - }); - } -} diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/util/ItClock.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/util/ItClock.java deleted file mode 100644 index f37c79057..000000000 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/util/ItClock.java +++ /dev/null @@ -1,140 +0,0 @@ -package uk.gov.moj.cpp.listing.it.util; - -import java.time.Clock; -import java.time.DayOfWeek; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; - -/** - * The single time authority for the listing integration-test suite. - * - *

Every test that needs "today" or "now" must read it from here rather than calling - * {@code LocalDate.now()} / {@code ZonedDateTime.now()} directly. Two problems are solved:

- * - *
    - *
  • Midnight safety. {@link #today()} is captured once per JVM, so a test can - * never straddle midnight between the moment it builds a hearing date and the moment it asserts - * on it. The historic "00:00-01:00 BST band" flakes came from two independent {@code now()} - * reads landing on different calendar days during that hour.
  • - *
  • Host independence. The deployed Wildfly/Postgres run in UTC. With the failsafe - * {@code -Duser.timezone=UTC} pin in place, the canonical zone here is UTC too, so the test JVM - * and the server agree on "today" regardless of whether the host is a UK laptop (Europe/London) - * or a UTC CI agent.
  • - *
- * - *

Simulation. Pass {@code -Dit.clock=2026-06-15T00:30:00+01:00[Europe/London]} (forwarded - * to the forked JVM by the {@code listing-integration-test} failsafe profile) to freeze the clock at - * an arbitrary instant. This is what makes the 00:00-01:00 band testable on demand without waiting - * for real midnight (see {@code run-it-midnight.sh}). When the property is absent or blank the clock - * is the live system UTC clock.

- * - *

Elapsed-time measurement (e.g. {@code System.currentTimeMillis()} in {@code QueueUtil} or - * {@code Instant.now()} inside {@code TestDurationListener}/{@code ServerLogTestMarkerExtension}) is - * deliberately NOT routed through here — those measure durations, not calendar dates.

- */ -public final class ItClock { - - public static final ZoneId LONDON = ZoneId.of("Europe/London"); - public static final ZoneId UTC = ZoneOffset.UTC; - - /** Override key, e.g. {@code -Dit.clock=2026-06-15T00:30:00+01:00[Europe/London]}. */ - public static final String CLOCK_OVERRIDE_PROPERTY = "it.clock"; - - private static final Clock CLOCK = resolveClock(); - - /** Captured ONCE per JVM/run, in UTC — no test can straddle midnight between build and assert. */ - private static final LocalDate TODAY = LocalDate.now(CLOCK); - - private ItClock() { - } - - /** The single anchored "today", in UTC. Stable for the whole run. */ - public static LocalDate today() { - return TODAY; - } - - /** {@code today()} shifted by {@code days} (negative = past). */ - public static LocalDate todayPlusDays(final long days) { - return TODAY.plusDays(days); - } - - /** {@code today()} if it falls Mon-Fri, else rolled forward to the next working day. */ - public static LocalDate nextWorkingDay() { - return nextWorkingDay(TODAY); - } - - /** - * {@code date} if it falls Mon-Fri, else rolled forward to the next working day (skips Sat/Sun). - * - *

Courts do not sit at weekends, so any test that asserts per-day listing/scheduling events must - * anchor its dates on working days. {@link #today()} alone only removes time-of-day - * non-determinism (the midnight band); a span built with raw {@code plusDays} still silently - * straddles a weekend on some days of the week, leaving the suite non-deterministic by - * day-of-week. Use this (and {@link #plusWorkingDays(LocalDate, int)}) to close that gap.

- */ - public static LocalDate nextWorkingDay(final LocalDate date) { - LocalDate d = date; - while (d.getDayOfWeek() == DayOfWeek.SATURDAY || d.getDayOfWeek() == DayOfWeek.SUNDAY) { - d = d.plusDays(1); - } - return d; - } - - /** - * {@code date} advanced by {@code workingDays} working days, skipping weekends; the result is - * guaranteed to be a working day. {@code plusWorkingDays(d, 0)} equals {@link #nextWorkingDay(LocalDate)}. - * - *

Use this instead of {@code plusDays} when building multi-day / split / extend hearing spans so the - * span endpoints never land on a Sat/Sun whichever day the suite runs. On a run with no weekend in the - * range it is identical to {@code plusDays}, so it does not perturb the passing weekday case.

- */ - public static LocalDate plusWorkingDays(final LocalDate date, final int workingDays) { - LocalDate d = nextWorkingDay(date); - for (int i = 0; i < workingDays; i++) { - d = nextWorkingDay(d.plusDays(1)); - } - return d; - } - - /** "Now" as a UTC {@link ZonedDateTime}; replaces {@code ZonedDateTime.now()} / {@code now(ZoneOffset.UTC)}. */ - public static ZonedDateTime nowUtc() { - return ZonedDateTime.now(CLOCK); - } - - /** "Now" as a Europe/London {@link ZonedDateTime}; replaces {@code ZonedDateTime.now(ZoneId.of("Europe/London"))}. */ - public static ZonedDateTime nowLondon() { - return ZonedDateTime.now(CLOCK.withZone(LONDON)); - } - - /** "Now" as an {@link Instant}; replaces a data-bearing {@code Instant.now()} (NOT elapsed-time measurement). */ - public static Instant nowInstant() { - return CLOCK.instant(); - } - - /** "Now" as a UTC {@link LocalDateTime}; replaces a data-bearing {@code LocalDateTime.now()}. */ - public static LocalDateTime nowLocalDateTime() { - return LocalDateTime.now(CLOCK); - } - - /** - * The ONE court-calendar-date-and-time to UTC-instant-string conversion rule. - * A court "calendar date" at a given London wall-clock time, expressed as the equivalent UTC instant — - * mirrors what {@code CourtSchedulerServiceStub} does and what the server stores. - */ - public static String utc(final LocalDate date, final LocalTime londonTime) { - return date.atTime(londonTime).atZone(LONDON).withZoneSameInstant(UTC).toString(); - } - - private static Clock resolveClock() { - final String override = System.getProperty(CLOCK_OVERRIDE_PROPERTY); - if (override == null || override.isBlank()) { - return Clock.system(UTC); - } - return Clock.fixed(ZonedDateTime.parse(override).toInstant(), UTC); - } -} diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/util/PublishRetryHelper.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/util/PublishRetryHelper.java deleted file mode 100644 index 794499418..000000000 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/util/PublishRetryHelper.java +++ /dev/null @@ -1,88 +0,0 @@ -package uk.gov.moj.cpp.listing.it.util; - -import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.RETRY_ATTEMPT_TIMEOUT_IN_MILLIS; -import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.TIMEOUT_IN_MILLIS; -import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.withRetryAttemptPollBudget; - -import org.awaitility.core.ConditionTimeoutException; -import org.slf4j.Logger; - -/** - * Shared re-publish loop for public events that race the asynchronous case<->hearing link: - * the {@code Case} aggregate silently drops the update ({@code hearingIds.isEmpty() → - * Stream.empty()}) until the async {@code add-hearing-to-case} command has run, and a dropped - * publish emits nothing — no JMS redelivery, no observable — so the only recovery is publishing - * again with a fresh metadata id (the framework dedupes by metadata id). - * - *

Adaptive poll budget. A dropped publish can never satisfy the poll, so burning the - * full {@link RestPollerHelper#TIMEOUT_IN_MILLIS} budget before re-publishing is pure waste — - * on vld this class of loop cost ~375s per run in full-budget first attempts. Non-final attempts - * therefore poll with the short {@link RestPollerHelper#RETRY_ATTEMPT_TIMEOUT_IN_MILLIS} budget; - * only the final attempt gets the full budget (preserving the original tail behaviour for a - * genuinely slow, but not dropped, projection). - * - *

The attempt count is derived so the short-probe phase spans the same wall clock as the - * previous 3 × full-budget loops' first two attempts — the last re-publish therefore - * happens no earlier than it used to, preserving coverage of slow link formation (measured at - * ~90s on vld), and total worst-case wall clock stays at 3 × the full budget. What changes - * is granularity: a re-publish lands within ~5s of the link forming instead of up to a full - * budget later. - */ -public final class PublishRetryHelper { - - private static final int MAX_PUBLISH_ATTEMPTS = - (int) (2 * RestPollerHelper.TIMEOUT_IN_MILLIS / RETRY_ATTEMPT_TIMEOUT_IN_MILLIS) + 1; - - private PublishRetryHelper() { - } - - /** - * Publishes via {@code publish} and verifies via {@code verify}, re-publishing until the - * read model reflects the update or the attempt budget is exhausted. {@code verify} must - * observe through a {@link RestPollerHelper}-built poll so the adaptive budget applies. - */ - public static void publishUntilReflected(final Logger logger, final String tag, final String description, - final Runnable publish, final Runnable verify) { - retryLoop(logger, tag, description, publish, verify, true); - } - - /** - * Variant for callers that have already done the initial publish: attempt 1 only verifies; - * {@code republish} runs before each subsequent attempt. - */ - public static void verifyOrRepublishUntilReflected(final Logger logger, final String tag, final String description, - final Runnable republish, final Runnable verify) { - retryLoop(logger, tag, description, republish, verify, false); - } - - private static void retryLoop(final Logger logger, final String tag, final String description, - final Runnable publish, final Runnable verify, - final boolean publishOnFirstAttempt) { - for (int attempt = 1; attempt <= MAX_PUBLISH_ATTEMPTS; attempt++) { - final boolean finalAttempt = attempt == MAX_PUBLISH_ATTEMPTS; - if (attempt > 1 || publishOnFirstAttempt) { - logger.info("[{}] publishing {} (attempt {}/{})", tag, description, attempt, MAX_PUBLISH_ATTEMPTS); - publish.run(); - } - try { - if (finalAttempt) { - verify.run(); - } else { - withRetryAttemptPollBudget(verify); - } - logger.info("[{}] read model reflected {} after {} attempt(s)", tag, description, attempt); - return; - } catch (final ConditionTimeoutException notYetReflected) { - if (finalAttempt) { - logger.error("[{}] {} still not reflected after {} attempts ({}ms short / {}ms final poll budgets) — failing", - tag, description, MAX_PUBLISH_ATTEMPTS, RETRY_ATTEMPT_TIMEOUT_IN_MILLIS, TIMEOUT_IN_MILLIS); - throw notYetReflected; - } - // The case<->hearing link was not established when this publish was processed, so the - // update was dropped. Re-publish with a fresh metadata id and poll again. - logger.warn("[{}] attempt {} did not land within {}ms (case<->hearing link likely not yet established); re-publishing", - tag, attempt, RETRY_ATTEMPT_TIMEOUT_IN_MILLIS); - } - } - } -} diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/util/RestPollerHelper.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/util/RestPollerHelper.java index 0840c9bce..81c6dec77 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/util/RestPollerHelper.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/util/RestPollerHelper.java @@ -14,45 +14,18 @@ public class RestPollerHelper { public static final long DELAY_IN_MILLIS = 300L; public static final long INTERVAL_IN_MILLIS = 20L; - public static final long TIMEOUT_IN_MILLIS = 30000L; - public static final long RETRY_ATTEMPT_TIMEOUT_IN_MILLIS = 5000L; + public static final long TIMEOUT_IN_MILLIS = 90000L; public static final FibonacciPollWithStartAndMax POLL_INTERVAL = new FibonacciPollWithStartAndMax(Duration.ofMillis(INTERVAL_IN_MILLIS), Duration.ofMillis(DELAY_IN_MILLIS)); - private static final ThreadLocal POLL_BUDGET_OVERRIDE = new ThreadLocal<>(); - public static RestPoller pollWithDefaults(final RequestParams requestParams) { - return poll(requestParams, POLL_INTERVAL, Duration.ofMillis(currentPollBudgetInMillis())); + return poll(requestParams, POLL_INTERVAL, Duration.ofMillis(TIMEOUT_IN_MILLIS)); } public static RestPoller pollWithDefaults(final RequestParamsBuilder requestParams) { - return poll(requestParams.build(), POLL_INTERVAL, Duration.ofMillis(currentPollBudgetInMillis())); + return poll(requestParams.build(), POLL_INTERVAL, Duration.ofMillis(TIMEOUT_IN_MILLIS)); } public static RestPoller pollWithDelayForJms(final RequestParams requestParams) { - return poll(requestParams, POLL_INTERVAL, Duration.ofMillis(currentPollBudgetInMillis())) + return poll(requestParams, POLL_INTERVAL, Duration.ofMillis(TIMEOUT_IN_MILLIS)) .pollDelay(DELAY_IN_MILLIS, MILLISECONDS); // Add delay for JMS processing; } - - /** - * Runs {@code verification} with the poll budget reduced to - * {@link #RETRY_ATTEMPT_TIMEOUT_IN_MILLIS} for every poll constructed on this thread inside it. - * - *

For re-publish retry loops (see {@code PublishRetryHelper}): when a publish was silently - * dropped by the aggregate, the polled observable can never appear, so non-final attempts - * should fail fast and re-publish instead of burning the full {@link #TIMEOUT_IN_MILLIS} - * budget. The override is read when the {@code RestPoller} is built (on the calling thread), - * so it applies to all polls the verification starts, and is always restored afterwards. - */ - public static void withRetryAttemptPollBudget(final Runnable verification) { - POLL_BUDGET_OVERRIDE.set(RETRY_ATTEMPT_TIMEOUT_IN_MILLIS); - try { - verification.run(); - } finally { - POLL_BUDGET_OVERRIDE.remove(); - } - } - - private static long currentPollBudgetInMillis() { - final Long override = POLL_BUDGET_OVERRIDE.get(); - return override != null ? override : TIMEOUT_IN_MILLIS; - } } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/scenario/ListCourtHearingStepsSpi.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/scenario/ListCourtHearingStepsSpi.java index 559df6c61..a7b6fe3d3 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/scenario/ListCourtHearingStepsSpi.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/scenario/ListCourtHearingStepsSpi.java @@ -4,8 +4,6 @@ import uk.gov.moj.cpp.listing.it.AbstractIT; import uk.gov.moj.cpp.listing.steps.data.CourtCentreData; -import org.hamcrest.Matcher; - import javax.json.JsonArray; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; @@ -15,19 +13,22 @@ import java.util.Map; import java.util.UUID; -import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; import static java.text.MessageFormat.format; import static java.util.UUID.fromString; import static uk.gov.justice.services.messaging.JsonObjects.createArrayBuilder; import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; +import static javax.ws.rs.core.Response.Status.OK; import static org.apache.http.HttpStatus.SC_ACCEPTED; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.jupiter.api.Assertions.*; +import static uk.gov.justice.services.common.http.HeaderConstants.USER_ID; +import static uk.gov.justice.services.test.utils.core.http.RequestParamsBuilder.requestParams; +import static uk.gov.justice.services.test.utils.core.matchers.ResponseStatusMatcher.status; import static uk.gov.justice.services.test.utils.core.messaging.JsonObjects.getJsonObject; import static uk.gov.justice.services.test.utils.core.messaging.JsonObjects.getUUID; -import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearing; +import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.pollWithDefaults; import static uk.gov.moj.cpp.listing.utils.PropertyUtil.getBaseUri; import static uk.gov.moj.cpp.listing.utils.PropertyUtil.readConfig; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCourtCentreById; @@ -37,6 +38,7 @@ public class ListCourtHearingStepsSpi extends AbstractIT { private static final String LISTING_COMMAND_LIST_COURT_HEARING = "listing.command.list-court-hearing"; private static final String MEDIA_TYPE_LIST_COURT_HEARING = "application/vnd.listing.command.list-court-hearing+json"; + private static final String MEDIA_TYPE_SEARCH_HEARINGS_JSON = "application/vnd.listing.search.hearings+json"; private static final String DEFAULT_DURATION_HOURS_MINS = "6:30"; private static final LocalTime DEFAULT_START_TIME = LocalTime.of(10, 30); @@ -98,12 +100,10 @@ public void verifyAllocatedHearingFound(final Map payload) { final String searchHearingUrl = String.format("%s/%s", getBaseUri(), format(readConfig().getProperty("listing.search.hearings.by.allocated"), true)); - // Poll on content (not just HTTP 200): the by.allocated read model lags the async - // list-court-hearing command, so wait until the hearing actually appears. - final String response = pollForHearing(searchHearingUrl, getLoggedInUser().toString(), - new Matcher[]{withJsonPath("$.hearings[0].id", equalTo(payload.get("hearingId")))}); - final JsonObject jsonObject = stringToJsonObjectConverter.convert(response); - final JsonObject hearingJsonObject = jsonObject.getJsonArray("hearings").getJsonObject(0); + final String response = pollWithDefaults(requestParams(searchHearingUrl, MEDIA_TYPE_SEARCH_HEARINGS_JSON).withHeader(USER_ID, getLoggedInUser()).build()) + .until(status().is(OK)).getPayload(); + JsonObject jsonObject = stringToJsonObjectConverter.convert(response); + final JsonObject hearingJsonObject = (JsonObject)jsonObject.getJsonArray("hearings").get(0); assertThat(hearingJsonObject.getString("id"), is(payload.get("hearingId"))); assertThat(hearingJsonObject.getString("courtCentreId"), is(payload.get("courtCentreId"))); @@ -121,10 +121,9 @@ public void verifyUnallocatedHearingFound(final Map payload) { final String searchHearingUrl = String.format("%s/%s", getBaseUri(), format(readConfig().getProperty("listing.search.hearings.by.allocated"), false)); - final String response = pollForHearing(searchHearingUrl, getLoggedInUser().toString(), - new Matcher[]{withJsonPath("$.hearings[0].id", equalTo(payload.get("hearingId")))}); - final JsonObject jsonObject = stringToJsonObjectConverter.convert(response); - final JsonObject hearingJsonObject = jsonObject.getJsonArray("hearings").getJsonObject(0); + final String response = pollWithDefaults(requestParams(searchHearingUrl, MEDIA_TYPE_SEARCH_HEARINGS_JSON).withHeader(USER_ID, getLoggedInUser())).until(status().is(OK)).getPayload(); + JsonObject jsonObject = stringToJsonObjectConverter.convert(response); + final JsonObject hearingJsonObject = (JsonObject)jsonObject.getJsonArray("hearings").get(0); assertThat(hearingJsonObject.getString("id"), is(payload.get("hearingId"))); assertThat(hearingJsonObject.getString("courtCentreId"), is(payload.get("courtCentreId"))); @@ -141,18 +140,18 @@ public void verifyUnallocatedTwoDefendantsHearingFound(final Map final String searchHearingUrl = String.format("%s/%s", getBaseUri(), format(readConfig().getProperty("listing.search.hearings.by.allocated"), false)); - final String response = pollForHearing(searchHearingUrl, getLoggedInUser().toString(), - new Matcher[]{withJsonPath("$.hearings[0].id", equalTo(payload.get("hearingId")))}); - final JsonObject jsonObject = stringToJsonObjectConverter.convert(response); - final JsonObject hearingJsonObject = jsonObject.getJsonArray("hearings").getJsonObject(0); - final JsonArray listedCases = hearingJsonObject.getJsonArray("listedCases"); + final String response = pollWithDefaults(requestParams(searchHearingUrl, MEDIA_TYPE_SEARCH_HEARINGS_JSON).withHeader(USER_ID, getLoggedInUser())).until(status().is(OK)).getPayload(); + JsonObject jsonObject = stringToJsonObjectConverter.convert(response); + final JsonObject hearingJsonObject = (JsonObject)jsonObject.getJsonArray("hearings").get(0); + JsonArray listedCases = hearingJsonObject.getJsonArray("listedCases"); assertEquals(1, listedCases.size()); - final JsonObject firstCase = listedCases.getJsonObject(0); - final JsonArray defendants = firstCase.getJsonArray("defendants"); + JsonObject firstCase = listedCases.getJsonObject(0); + JsonArray defendants = firstCase.getJsonArray("defendants"); assertEquals(2, defendants.size()); assertThat(hearingJsonObject.getString("id"), is(payload.get("hearingId"))); assertThat(hearingJsonObject.getString("courtCentreId"), is(payload.get("courtCentreId"))); + assertThat(hearingJsonObject.getString("courtCentreId"), is(payload.get("courtCentreId"))); assertThat(hearingJsonObject.getString("jurisdictionType"), is( payload.get("jurisdictionType"))); assertThat(hearingJsonObject.getJsonObject("type").getString("id"), is(payload.get("hearingTypeId"))); assertThat(hearingJsonObject.getInt("estimatedMinutes"), is(Integer.parseInt(payload.get("estimatedMinutes")))); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/scenario/TestSpiScenario.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/scenario/TestSpiScenario.java index f09d7cf1d..9e06cc8c6 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/scenario/TestSpiScenario.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/scenario/TestSpiScenario.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import uk.gov.moj.cpp.listing.it.AbstractIT; -import uk.gov.moj.cpp.listing.it.util.ItClock; import javax.json.JsonObject; import java.io.IOException; @@ -17,7 +16,6 @@ import static java.util.Collections.emptyMap; import static java.util.UUID.randomUUID; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.*; -import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.getRandomCourtCenterId; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetProsecutorPoliceFlag; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCourtCentreById; @@ -48,12 +46,12 @@ class TestSpiScenario extends AbstractIT { @Test void testHearingDaysWithCourtCentreScenario() throws IOException { stubGetAvailableHearingSlots(); - startDate = ItClock.today(); - endDate = ItClock.today().plusDays(1); + startDate = LocalDate.now(); + endDate = LocalDate.now().plusDays(1); hearingStartTime = ZonedDateTime.of(startDate, defaultStartTime, UTC); hearingId = randomUUID(); caseId = randomUUID(); - courtCentreId = getRandomCourtCenterId(); + courtCentreId = randomUUID(); hearingTypeId = randomUUID(); stubGetReferenceDataCourtCentreById(courtCentreId); @@ -77,12 +75,12 @@ void testHearingDaysWithCourtCentreScenario() throws IOException { @Test void shouldListHearingWithUnallocatedData() throws IOException { - startDate = ItClock.today(); - endDate = ItClock.today().plusDays(1); + startDate = LocalDate.now(); + endDate = LocalDate.now().plusDays(1); hearingStartTime = ZonedDateTime.of(startDate, defaultStartTime, UTC); hearingId = randomUUID(); caseId = randomUUID(); - courtCentreId = getRandomCourtCenterId(); + courtCentreId = randomUUID(); hearingTypeId = randomUUID(); stubGetReferenceDataCourtCentreById(courtCentreId); @@ -104,12 +102,12 @@ void shouldListHearingWithUnallocatedData() throws IOException { @Test void shouldListHearingWithTwoDefendantsUnallocated() throws IOException { stubGetAvailableHearingSlots(); - startDate = ItClock.today(); - endDate = ItClock.today().plusDays(1); + startDate = LocalDate.now(); + endDate = LocalDate.now().plusDays(1); hearingStartTime = ZonedDateTime.of(startDate, defaultStartTime, UTC); hearingId = randomUUID(); caseId = randomUUID(); - courtCentreId = getRandomCourtCenterId(); + courtCentreId = randomUUID(); hearingTypeId = randomUUID(); stubGetReferenceDataCourtCentreById(courtCentreId); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/AddDefendantSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/AddDefendantSteps.java index 6b39cbd95..edb459743 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/AddDefendantSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/AddDefendantSteps.java @@ -6,17 +6,14 @@ import static java.util.UUID.randomUUID; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static uk.gov.justice.core.courts.Organisation.organisation; import static uk.gov.justice.services.integrationtest.utils.jms.JmsMessageConsumerClientProvider.newPublicJmsMessageConsumerClientProvider; import static uk.gov.justice.services.integrationtest.utils.jms.JmsMessageProducerClientProvider.newPublicJmsMessageProducerClientProvider; import static uk.gov.justice.services.test.utils.core.messaging.MetadataBuilderFactory.metadataOf; import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearing; import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearingWithJmsDelay; -import static uk.gov.moj.cpp.listing.it.util.PublishRetryHelper.publishUntilReflected; import static uk.gov.moj.cpp.listing.utils.QueueUtil.retrieveMessage; import static uk.gov.moj.cpp.listing.utils.QueueUtil.sendMessage; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.getRandomCourtCenterId; @@ -42,7 +39,6 @@ import uk.gov.moj.cpp.listing.it.AbstractIT; import uk.gov.moj.cpp.listing.steps.data.AddDefendantForCourtProceedingsData; import uk.gov.moj.cpp.listing.steps.data.HearingData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.ZonedDateTime; import java.util.Arrays; @@ -53,15 +49,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.path.json.JsonPath; -import org.awaitility.core.ConditionTimeoutException; import org.hamcrest.Matcher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class AddDefendantSteps extends AbstractIT { - private static final Logger LOGGER = LoggerFactory.getLogger(AddDefendantSteps.class); - private static final String PUBLIC_EVENT_SELECTOR_PROGRESSION_ADD_DEFENDANTS_TO_COURT_PROCEEDINGS = "public.progression.defendants-added-to-court-proceedings"; private static final String PUBLIC_EVENT_SELECTOR_DEFENDANT_DETAILS_ADDED_FOR_COURT_PROCEEDINGS = "public.listing.new-defendant-added-for-court-proceedings"; @@ -103,13 +94,10 @@ public void whenCaseDefendantsAddedPublicEventIsPublished() { public void verifyPublicEventDefendantAddedInActiveMQ() { final JsonPath jsRequest = new JsonPath(request); - final String expectedHearingId = hearingData.getId().toString(); - final JsonPath jsonResponse = retrieveMessage(publicEventsMessageNewDefendantAdded, - containsString(expectedHearingId)); - assertNotNull(jsonResponse, "No public new-defendant-added event found for hearingId=" + expectedHearingId); + final JsonPath jsonResponse = retrieveMessage(publicEventsMessageNewDefendantAdded); assertThat(jsonResponse.get("caseId"), is(caseId.toString())); - assertThat(jsonResponse.get("hearingId"), is(expectedHearingId)); + assertThat(jsonResponse.get("hearingId"), is(hearingData.getId().toString())); assertThat(jsonResponse.get("defendantId"), is(jsRequest.getString("defendants[0].id"))); assertThat(jsonResponse.get("courtCentre.id"), is(hearingData.getCourtCentreId().toString())); assertThat(jsonResponse.get("courtCentre.roomId"), is(hearingData.getCourtRoomId().toString())); @@ -158,61 +146,13 @@ public void verifyHearingListedFromAPIWithJmsDelay(final boolean isAllocated) { }); } - /** - * Publishes the {@code public.progression.defendants-added-to-court-proceedings} event and verifies it was - * consumed downstream (via the {@code public.listing.new-defendant-added-for-court-proceedings} JMS message), - * RE-PUBLISHING until the message arrives (or the attempt budget is exhausted). - * - *

Why re-publish? Same case-link race as described on {@link #publishUntilDefendantsAddedReflected}: - * the Case aggregate silently drops the event when {@code hearingIds.isEmpty()}. A dropped publish emits NO - * downstream events, so the JMS consume hangs to timeout with {@link java.util.NoSuchElementException}. - * Re-publishing with a fresh metadata id each attempt recovers once the case<->hearing link forms. - * Safe: a dropped publish produces no duplicates. - */ - public void publishUntilDefendantsAddedConsumed() { - final int maxPublishAttempts = 3; - for (int attempt = 1; attempt <= maxPublishAttempts; attempt++) { - LOGGER.info("[defendants-added-fix] publishing defendants-added event (JMS-consume gate) for case {} (attempt {}/{})", - caseId, attempt, maxPublishAttempts); - whenCaseDefendantsAddedPublicEventIsPublished(); - try { - verifyPublicEventDefendantAddedInActiveMQ(); - LOGGER.info("[defendants-added-fix] downstream JMS event received after {} publish attempt(s)", attempt); - return; - } catch (final java.util.NoSuchElementException caseNotYetLinkedToHearing) { - if (attempt == maxPublishAttempts) { - LOGGER.error("[defendants-added-fix] downstream JMS event still not received after {} attempts — failing", maxPublishAttempts); - throw caseNotYetLinkedToHearing; - } - LOGGER.warn("[defendants-added-fix] attempt {} produced no downstream JMS event (case<->hearing link likely not yet established); re-publishing", attempt); - } - } - } - - /** - * Publishes the {@code public.progression.defendants-added-to-court-proceedings} event and verifies the - * read model, RE-PUBLISHING until the update is reflected (or the attempt budget is exhausted). - * - *

Why re-publish? The event is consumed by the Case aggregate's handler which silently drops - * the update ({@code if (hearingIds.isEmpty()) return Stream.empty();}) when the case is not yet linked to - * a hearing. The async {@code add-hearing-to-case} command runs after {@code list-court-hearing} and has - * no viewstore projection, so the test cannot await the link deterministically. On slow CI the single - * publish can be lost with no JMS redelivery. Re-publishing with a fresh metadata id each attempt - * (framework dedupes by metadata id) recovers once the link is established. - */ - public void publishUntilDefendantsAddedReflected(final boolean allocated) { - publishUntilReflected(LOGGER, "defendants-added-fix", "defendants-added event for case " + caseId, - this::whenCaseDefendantsAddedPublicEventIsPublished, - () -> verifyHearingListedFromAPIWithJmsDelay(allocated)); - } - private AddDefendantForCourtProceedingsData getAddDefendantDetails(final UUID caseId) { final List defendant = Arrays.asList(Defendant.defendant() .withId(DEFENDANT_ID) .withMasterDefendantId(MASTER_DEFENDANT_ID) - .withCourtProceedingsInitiated(ItClock.nowUtc()) + .withCourtProceedingsInitiated(ZonedDateTime.now()) .withLegalEntityDefendant(LegalEntityDefendant.legalEntityDefendant() .withOrganisation(Organisation.organisation() .withName("withOrganisationName") diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/CaseUpdatedAndDefendantProceedingsConcludedSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/CaseUpdatedAndDefendantProceedingsConcludedSteps.java index df543ec08..e8e805adf 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/CaseUpdatedAndDefendantProceedingsConcludedSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/CaseUpdatedAndDefendantProceedingsConcludedSteps.java @@ -7,7 +7,6 @@ import static uk.gov.justice.services.test.utils.core.messaging.MetadataBuilderFactory.metadataOf; import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearing; import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearingWithJmsDelay; -import static uk.gov.moj.cpp.listing.it.util.PublishRetryHelper.publishUntilReflected; import static uk.gov.moj.cpp.listing.utils.FileUtil.getPayload; import static uk.gov.moj.cpp.listing.utils.QueueUtil.publicEvents; import static uk.gov.moj.cpp.listing.utils.QueueUtil.sendMessage; @@ -22,19 +21,15 @@ import javax.json.JsonObject; -import org.awaitility.core.ConditionTimeoutException; import org.hamcrest.Matcher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class CaseUpdatedAndDefendantProceedingsConcludedSteps extends AbstractIT { - private static final Logger LOGGER = LoggerFactory.getLogger(CaseUpdatedAndDefendantProceedingsConcludedSteps.class); - private static final String PUBLIC_EVENT_HEARING_RESULTED_CASE_UPDATED = "public.progression.hearing-resulted-case-updated"; private JmsMessageProducerClient publicEventCaseUpdatedAndHearingResulted; + private final UUID metadataId; private final UUID userId; private final UUID caseId; private final HearingData hearingData; @@ -44,6 +39,7 @@ public CaseUpdatedAndDefendantProceedingsConcludedSteps(UUID caseId, HearingData this.caseId = caseId; this.hearingData = hearingData; this.userId = randomUUID(); + this.metadataId = randomUUID(); this.listedCaseData = hearingData.getListedCases().get(0); this.publicEventCaseUpdatedAndHearingResulted = publicEvents.createPublicProducer(); @@ -57,38 +53,15 @@ public void whenPublicEventCaseUpdatedAndHearingResultedIsPublished() { .replaceAll("DEFENDANT_ID", defendantId.toString()); final JsonObject jsonObject = new StringToJsonObjectConverter().convert(eventPayloadString); - // Fresh metadata id per publish: the framework dedupes events by metadata id, so re-publishing - // with the same id would be silently ignored. A new id guarantees each re-publish is reprocessed. sendMessage(publicEventCaseUpdatedAndHearingResulted, PUBLIC_EVENT_HEARING_RESULTED_CASE_UPDATED, jsonObject, - metadataOf(randomUUID(), PUBLIC_EVENT_HEARING_RESULTED_CASE_UPDATED) + metadataOf(metadataId, PUBLIC_EVENT_HEARING_RESULTED_CASE_UPDATED) .withUserId(userId.toString()) .build()); } - /** - * Publishes the {@code public.progression.hearing-resulted-case-updated} event and verifies the read model, - * RE-PUBLISHING until the update is reflected (or the attempt budget is exhausted). - * - *

Why re-publish instead of publish-once-then-poll? The event is consumed by - * {@code ListingEventProcessor} and routed to the {@code Case} aggregate's handler. That handler - * silently drops the update ({@code if (hearingIds.isEmpty()) return Stream.empty();}) when the - * aggregate does not yet know which hearing the case belongs to. {@code Case.hearingIds} is populated only - * after the asynchronous {@code add-hearing-to-case} command runs — itself triggered by a private event - * emitted after {@code list-court-hearing} — and there is no viewstore projection for the link, so - * the test cannot deterministically await it. On slower CI environments the first publish can be processed - * before the link exists; it is then dropped with no JMS redelivery, so a single publish is lost forever. - * Re-publishing (with a fresh event id each time) guarantees that once the link is established, a - * subsequent publish lands. - */ - public void publishUntilCaseStatusReflected(final boolean isAllocated) { - publishUntilReflected(LOGGER, "defendant-proceeding-fix", "hearing-resulted-case-updated", - this::whenPublicEventCaseUpdatedAndHearingResultedIsPublished, - () -> verifyHearingForCaseStatusAndDefendantProceedingsConcludedFromAPIWithJmsDelay(isAllocated)); - } - public void verifyHearingForCaseStatusAndDefendantProceedingsConcludedFromAPI(boolean isAllocated) { pollForHearing(hearingData.getCourtCentreId().toString(), isAllocated, getLoggedInUser().toString(), new Matcher[]{ withJsonPath("$.hearings[0].listedCases[0].defendants[0].proceedingsConcluded", diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/CourtApplicationSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/CourtApplicationSteps.java index 771a4ef56..3fed2c2b0 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/CourtApplicationSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/CourtApplicationSteps.java @@ -18,7 +18,6 @@ import static uk.gov.justice.services.test.utils.core.random.RandomGenerator.STRING; import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearing; import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearingWithJmsDelay; -import static uk.gov.moj.cpp.listing.it.util.PublishRetryHelper.publishUntilReflected; import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.pollWithDefaults; import static uk.gov.moj.cpp.listing.utils.PropertyUtil.getBaseUri; import static uk.gov.moj.cpp.listing.utils.PropertyUtil.readConfig; @@ -52,7 +51,6 @@ import uk.gov.moj.cpp.listing.steps.data.CourtApplicationUpdateData; import uk.gov.moj.cpp.listing.steps.data.HearingData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.util.UUID; @@ -63,7 +61,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.Filter; import io.restassured.path.json.JsonPath; -import org.awaitility.core.ConditionTimeoutException; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.slf4j.Logger; @@ -140,29 +137,10 @@ public void whenCaseCourtApplicationUpdatedPublicEventIsPublished() { LOGGER.info("Event published:\n\tMedia type = {} \n\tPayload = {}\n\n, \n\tHeader = {}", PUBLIC_EVENT_SELECTOR_PROGRESSION_COURT_APPLICATION_CHANGED, request, getLoggedInHeader()); } - /** - * Publishes the {@code public.progression.court-application-changed} event and verifies the read model, - * RE-PUBLISHING until the update is reflected (or the attempt budget is exhausted). - * - *

Why re-publish instead of publish-once-then-poll? The event is routed to a command handler - * that silently drops the update ({@code if (hearingIds.isEmpty()) return Stream.empty();}) when the - * Application aggregate is not yet linked to a hearing. The link is established asynchronously after - * {@code list-court-hearing} completes. On slower CI environments the single publish can be processed - * before the link exists; it is then dropped with no JMS redelivery, so the 90 s poll can never succeed. - * Re-publishing (with a fresh metadata/event id each time — the framework dedupes by metadata id) guarantees - * that once the link is established a subsequent publish lands. - */ - public void publishUntilCourtApplicationReflected() { - publishUntilReflected(LOGGER, "court-application-fix", "court-application-changed", - this::whenCaseCourtApplicationUpdatedPublicEventIsPublished, - this::verifyCourtApplicationUpdatedFromAPI); - } - public void verifyPublicEventCourtApplicationAdded() { JsonPath jsRequest = new JsonPath(request); - JsonPath jsonResponse = retrieveMessage(publicMessageConsumerCourtApplicationAddedForHearing, - org.hamcrest.CoreMatchers.containsString(jsRequest.getString("hearingId"))); + JsonPath jsonResponse = retrieveMessage(publicMessageConsumerCourtApplicationAddedForHearing); LOGGER.debug("jsonResponse from publicMessageConsumerCourtApplicationAddedForHearing: {}", jsonResponse.prettify()); assertThat(jsonResponse.get("hearingId"), is(jsRequest.getString("hearingId"))); assertThat(jsonResponse.get("courtApplication.id"), is(jsRequest.getString("courtApplication.id"))); @@ -339,7 +317,7 @@ private CourtApplication getCourtApplication(final CourtApplicationData courtApp .withOffenceActiveOrder(OffenceActiveOrder.COURT_ORDER) .build()) .withApplicationStatus(ApplicationStatus.LISTED) - .withApplicationReceivedDate(ItClock.today().toString()) + .withApplicationReceivedDate(LocalDate.now().toString()) .withApplicationReference(STRING.next()) .withApplicationParticulars(STRING.next()) .build(); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/DefendantLegalAidStatusUpdateSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/DefendantLegalAidStatusUpdateSteps.java index a6cb28164..8d46d1e04 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/DefendantLegalAidStatusUpdateSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/DefendantLegalAidStatusUpdateSteps.java @@ -1,12 +1,8 @@ package uk.gov.moj.cpp.listing.steps; -import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; import static java.util.UUID.randomUUID; -import static org.hamcrest.core.IsEqual.equalTo; import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; import static uk.gov.justice.services.test.utils.core.messaging.MetadataBuilderFactory.metadataOf; -import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearingWithJmsDelay; -import static uk.gov.moj.cpp.listing.it.util.PublishRetryHelper.publishUntilReflected; import static uk.gov.moj.cpp.listing.utils.QueueUtil.sendMessage; import uk.gov.justice.services.integrationtest.utils.jms.JmsMessageProducerClient; @@ -19,14 +15,8 @@ import javax.json.JsonObject; -import org.hamcrest.Matcher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class DefendantLegalAidStatusUpdateSteps extends AbstractIT { - private static final Logger LOGGER = LoggerFactory.getLogger(DefendantLegalAidStatusUpdateSteps.class); - private static final String PUBLIC_PROGRESSION_DEFENDANT_LEGALAID_STATUS_UPDATED = "public.progression.defendant-legalaid-status-updated"; private JmsMessageProducerClient publicEventDefendantLegalAidStatusUpdated; @@ -47,44 +37,14 @@ public DefendantLegalAidStatusUpdateSteps(UUID caseId, HearingData hearingData) } public void whenCaseDefendantLegalAidStatusUpdatedPublicEventIsPublished() { - // Fresh randomUUID() per publish: the framework dedupes events by metadata id, so - // re-publishing with the same id would be ignored. A new id guarantees each re-publish - // is reprocessed by the aggregate. + final JsonObject payload = getPayloadForPublicEventFromHearingData(); sendMessage( publicEventDefendantLegalAidStatusUpdated, PUBLIC_PROGRESSION_DEFENDANT_LEGALAID_STATUS_UPDATED, payload, metadataOf(randomUUID(), PUBLIC_PROGRESSION_DEFENDANT_LEGALAID_STATUS_UPDATED).withUserId(randomUUID().toString()).build()); - } - /** - * Asserts that the read model reflects legalAidStatus == "Granted" for the hearing. - * Uses JMS-aware polling to handle asynchronous message processing. - */ - public void verifyLegalAidStatusGranted() { - pollForHearingWithJmsDelay(hearingData.getCourtCentreId().toString(), false, getLoggedInUser().toString(), new Matcher[]{ - withJsonPath("$.hearings[0].listedCases[0].defendants[0].legalAidStatus", equalTo("Granted")) - }); - } - - /** - * Publishes the {@code public.progression.defendant-legalaid-status-updated} event and verifies - * the read model, RE-PUBLISHING until the update is reflected (or the attempt budget is exhausted). - * - *

Why re-publish instead of publish-once-then-poll? The event is consumed by - * {@code ListingEventProcessor} and routes to the {@code Case} aggregate. That aggregate - * silently drops the update ({@code if (hearingIds.isEmpty()) return Stream.empty();}) - * when the case is not yet linked to a hearing. {@code Case.hearingIds} is populated only after - * the asynchronous {@code add-hearing-to-case} command runs — and there is no viewstore - * projection to await that link. On slow CI the first publish can arrive before the link exists, - * is dropped with no JMS redelivery, and the 90s poll can never succeed. Re-publishing with a - * fresh metadata id each attempt guarantees that once the link forms, a subsequent publish lands. - */ - public void publishUntilLegalAidStatusReflected() { - publishUntilReflected(LOGGER, "legalaid-fix", "defendant-legalaid-status-updated for case " + caseId, - this::whenCaseDefendantLegalAidStatusUpdatedPublicEventIsPublished, - this::verifyLegalAidStatusGranted); } private JsonObject getPayloadForPublicEventFromHearingData() { @@ -93,5 +53,6 @@ private JsonObject getPayloadForPublicEventFromHearingData() { .add("defendantId", listedCaseData.getDefendants().get(0).getDefendantId().toString()) .add("legalAidStatus", "Granted") .build(); + } } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/HearingAsMarkedSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/HearingAsMarkedSteps.java index 7b7ccecfb..64cc2ceb6 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/HearingAsMarkedSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/HearingAsMarkedSteps.java @@ -81,8 +81,7 @@ public void whenUnallocatedHearingMarkedAsDuplicateCommandIsSent() { public void verifyHearingMarkedAsDuplicatePublicEventInActiveMQ() { final JsonPath jsRequest = new JsonPath(request); - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingMarkedAsDuplicateEvent, - org.hamcrest.CoreMatchers.containsString(jsRequest.getString("hearingId"))); + final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingMarkedAsDuplicateEvent); LOGGER.info("jsonResponse from publicMessageConsumerHearingMarkedAsDuplicateEvent: {}", jsonResponse.prettify()); @@ -96,8 +95,7 @@ public void verifyHearingMarkedAsDuplicatePublicEventInActiveMQ() { } public void verifyHmiPublicEventForDeleteHearing() { - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHmiHearingDeleted, - org.hamcrest.CoreMatchers.containsString(hearingData.getId().toString())); + final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHmiHearingDeleted); LOGGER.info("jsonResponse from publicMessageConsumerHmiHearingUpdated: {}", jsonResponse.prettify()); assertThat(jsonResponse.getString("hearingId"), is(hearingData.getId().toString())); assertThat(jsonResponse.getString("cancellationReasonCode"), is("CNCL")); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListCourtHearingSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListCourtHearingSteps.java index 2b5279d91..51bbe556c 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListCourtHearingSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListCourtHearingSteps.java @@ -2,6 +2,7 @@ import static com.jayway.jsonpath.Criteria.where; import static com.jayway.jsonpath.Filter.filter; +import static com.jayway.jsonpath.JsonPath.read; import static com.jayway.jsonpath.matchers.JsonPathMatchers.isJson; import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; import static java.text.MessageFormat.format; @@ -18,7 +19,6 @@ import static org.apache.http.HttpStatus.SC_FORBIDDEN; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.anyOf; -import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; @@ -27,8 +27,8 @@ import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.core.IsEqual.equalTo; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static uk.gov.justice.core.courts.Organisation.organisation; import static uk.gov.justice.services.common.converter.LocalDates.to; import static uk.gov.justice.services.common.http.HeaderConstants.USER_ID; @@ -48,9 +48,7 @@ import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollUntilHearingIsPresent; import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.pollWithDefaults; import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.pollWithDelayForJms; -import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessionsForCourtSchedule; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubSearchBookHearingSlotsForCrown; -import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubSearchCourtSchedulesByIdSession; import static uk.gov.moj.cpp.listing.utils.DefenceServiceStub.stubDefenceQueryApiForSearchCasesByOrganisationDefendant; import static uk.gov.moj.cpp.listing.utils.DefenceServiceStub.stubDefenceQueryApiForSearchCasesByPersonDefendant; import static uk.gov.moj.cpp.listing.utils.FileUtil.getPayload; @@ -124,7 +122,6 @@ import uk.gov.moj.cpp.listing.steps.data.ListedCaseData; import uk.gov.moj.cpp.listing.steps.data.OffenceData; import uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.io.IOException; import java.time.LocalDate; @@ -132,9 +129,11 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -382,7 +381,6 @@ private Response getResponseCaseSubmittedForListing(final boolean isStandaloneAp private Response getResponseCaseSubmittedForListingBookedSlot() { stubReferenceDataForFirstHearing(); - hearingsData.getHearingData().forEach(ListCourtHearingSteps::stubCrownBookedSlotResolution); final String listCaseForHearingUrl = String.format("%s/%s", getBaseUri(), format (readConfig().getProperty(LISTING_COMMAND_LIST_COURT_HEARING))); @@ -438,92 +436,6 @@ protected void stubReferenceDataForFirstHearing() { hd.getCourtRoomId().toString())); } - /** - * For a CROWN hearing the {@code bookingReference} IS the courtScheduleId. The listing command resolves - * it against courtscheduler ({@code search.court-schedules-by-id}) and then lists it - * ({@code list.hearings-in-court-sessions}). Stub both so the bookingReference resolves to a session - * echoing this hearing's own centre/room — keeping the enriched hearing consistent with the listed values. - * No-op for MAGISTRATES, unallocated hearings (no booking reference) or hearings without a court centre. - */ - private static void stubCrownBookingReferenceResolution(final HearingData hearingData, final UUID bookingReference) { - if (bookingReference == null - || hearingData.getCourtCentreId() == null - || !"CROWN".equals(hearingData.getJurisdictionType())) { - return; - } - final ZonedDateTime startTime = hearingData.getHearingStartTime() != null - ? hearingData.getHearingStartTime() - : ItClock.nowUtc(); - final LocalDate sessionDate = hearingData.getHearingStartDate() != null - ? hearingData.getHearingStartDate() - : startTime.toLocalDate(); - stubSearchCourtSchedulesByIdSession( - bookingReference.toString(), hearingData.getCourtCentreId(), hearingData.getCourtRoomId(), - sessionDate, startTime, false); - stubListHearingInCourtSessionsForCourtSchedule(hearingData.getId().toString(), bookingReference.toString(), startTime); - } - - /** - * Booked-slot analogue of {@link #stubCrownBookingReferenceResolution}: a CROWN hearing listed with - * pre-booked slots carries the chosen courtScheduleId on {@code bookedSlots[]}, which the listing command - * resolves against courtscheduler ({@code search.court-schedules-by-id}) and then lists - * ({@code list.hearings-in-court-sessions}). Stub both so each bookedSlot's courtScheduleId resolves to a - * non-draft session echoing this hearing's own centre/room — keeping the enriched hearing consistent with - * the listed values. Without these the enrichment degrades to the legacy bookedSlots fallback and logs a - * failed courtscheduler retrieve. No-op for MAGISTRATES or hearings without booked slots. - */ - private static void stubCrownBookedSlotResolution(final HearingData hearingData) { - if (!"CROWN".equals(hearingData.getJurisdictionType()) || !isNotEmpty(hearingData.getBookedSlots())) { - return; - } - hearingData.getBookedSlots().stream() - .filter(slot -> nonNull(slot.getCourtScheduleId())) - .forEach(slot -> { - final ZonedDateTime startTime = nonNull(slot.getStartTime()) - ? slot.getStartTime() - : (nonNull(hearingData.getHearingStartTime()) ? hearingData.getHearingStartTime() : ItClock.nowUtc()); - final UUID roomId = nonNull(slot.getRoomId()) - ? fromString(slot.getRoomId()) - : hearingData.getCourtRoomId(); - stubSearchCourtSchedulesByIdSession( - slot.getCourtScheduleId(), hearingData.getCourtCentreId(), roomId, - startTime.toLocalDate(), startTime, false); - stubListHearingInCourtSessionsForCourtSchedule(hearingData.getId().toString(), slot.getCourtScheduleId(), startTime); - }); - } - - /** - * Variant of {@link #stubCrownBookingReferenceResolution} for template-built payloads (e.g. group cases): - * reads the CROWN bookingReference / courtCentre / room straight from the list-court-hearing JSON and stubs - * the courtscheduler resolution + list calls so the bookingReference resolves to a matching session. - */ - private static void stubCrownBookingReferenceResolutionFromPayload(final JsonObject payload, final UUID fallbackCourtCentreId) { - if (payload == null || !payload.containsKey("hearings") || payload.isNull("hearings")) { - return; - } - final JsonArray hearings = payload.getJsonArray("hearings"); - for (int i = 0; i < hearings.size(); i++) { - final JsonObject hearing = hearings.getJsonObject(i); - if (!"CROWN".equals(hearing.getString("jurisdictionType", null)) - || !hearing.containsKey("bookingReference") || hearing.isNull("bookingReference")) { - continue; - } - final String bookingReference = hearing.getString("bookingReference"); - final JsonObject courtCentre = hearing.containsKey("courtCentre") && !hearing.isNull("courtCentre") - ? hearing.getJsonObject("courtCentre") : null; - final UUID roomId = courtCentre != null && courtCentre.containsKey("roomId") && !courtCentre.isNull("roomId") - ? UUID.fromString(courtCentre.getString("roomId")) : null; - final UUID centreId = courtCentre != null && courtCentre.containsKey("id") && !courtCentre.isNull("id") - ? UUID.fromString(courtCentre.getString("id")) : fallbackCourtCentreId; - final String hearingId = hearing.getString("id", null); - final ZonedDateTime startTime = ItClock.nowUtc(); - stubSearchCourtSchedulesByIdSession(bookingReference, centreId, roomId, startTime.toLocalDate(), startTime, false); - if (hearingId != null) { - stubListHearingInCourtSessionsForCourtSchedule(hearingId, bookingReference, startTime); - } - } - } - private Response getResponseCaseSubmittedForListingWithLegalEntity() { hearingsData.getHearingData().stream() .map(HearingData::getCourtCentreId) @@ -581,6 +493,110 @@ public void verifyHearingListedFromAPI(final boolean isAllocated) { sleepToBeRefactored(); } + public void verifyHearingListedWithJudiciarySourceAndJudicialId(final boolean isAllocated, + final String expectedSource, + final String expectedJudicialId) { + final HearingData hearingData = hearingsData.getHearingData().get(0); + verifyHearingListedWithJudiciarySourceAndJudicialId(hearingData.getCourtCentreId(), isAllocated, expectedSource, expectedJudicialId); + } + + /** + * Poll for a hearing at a specific courtCentreId. Use this overload when the hearing's + * courtCentreId has been changed by an update command — searching by the original + * courtCentreId would miss the hearing. + */ + public void verifyHearingListedWithJudiciarySourceAndJudicialId(final UUID courtCentreId, + final boolean isAllocated, + final String expectedSource, + final String expectedJudicialId) { + final HearingData hearingData = hearingsData.getHearingData().get(0); + final String hearingIdFilter = getHearingFilter(hearingData.getId().toString()); + pollForHearing(courtCentreId.toString(), isAllocated, getLoggedInUser().toString(), new Matcher[]{ + withJsonPath(hearingIdFilter + ".judiciarySource", hasItem(expectedSource)), + withJsonPath(hearingIdFilter + ".judiciary[*].judicialId", hasItem(expectedJudicialId)) + }); + } + + public void verifySessionJudiciaryAllFields(final boolean isAllocated, + final String expectedSource, + final String expectedJudicialId, + final int expectedSeqId, + final String expectedTitlePrefix, + final String expectedTitleJudicialPrefix, + final String expectedTitleJudicialPrefixWelsh, + final String expectedPersonId, + final String expectedRequestedName, + final String expectedSurname, + final String expectedForenames, + final String expectedEmailAddress, + final String expectedJudiciaryType, + final List expectedSpecialisms) { + final HearingData hearingData = hearingsData.getHearingData().get(0); + verifySessionJudiciaryAllFields(hearingData.getCourtCentreId(), isAllocated, expectedSource, expectedJudicialId, + expectedSeqId, expectedTitlePrefix, expectedTitleJudicialPrefix, expectedTitleJudicialPrefixWelsh, + expectedPersonId, expectedRequestedName, expectedSurname, expectedForenames, expectedEmailAddress, + expectedJudiciaryType, expectedSpecialisms); + } + + /** + * Poll at an explicit courtCentreId. Use this overload when the hearing's courtCentreId has + * been changed by an update command — searching by the original courtCentreId would miss it. + */ + public void verifySessionJudiciaryAllFields(final UUID courtCentreId, + final boolean isAllocated, + final String expectedSource, + final String expectedJudicialId, + final int expectedSeqId, + final String expectedTitlePrefix, + final String expectedTitleJudicialPrefix, + final String expectedTitleJudicialPrefixWelsh, + final String expectedPersonId, + final String expectedRequestedName, + final String expectedSurname, + final String expectedForenames, + final String expectedEmailAddress, + final String expectedJudiciaryType, + final List expectedSpecialisms) { + final HearingData hearingData = hearingsData.getHearingData().get(0); + final String hearingIdFilter = getHearingFilter(hearingData.getId().toString()); + final List matchers = new ArrayList<>(); + matchers.add(withJsonPath(hearingIdFilter + ".judiciarySource", hasItem(expectedSource))); + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].judicialId", hasItem(expectedJudicialId))); + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].seqId", hasItem(expectedSeqId))); + if (expectedJudiciaryType != null) { + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].judiciaryType", hasItem(expectedJudiciaryType))); + } + if (expectedTitlePrefix != null) { + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].titlePrefix", hasItem(expectedTitlePrefix))); + } + if (expectedTitleJudicialPrefix != null) { + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].titleJudicialPrefix", hasItem(expectedTitleJudicialPrefix))); + } + if (expectedTitleJudicialPrefixWelsh != null) { + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].titleJudicialPrefixWelsh", hasItem(expectedTitleJudicialPrefixWelsh))); + } + if (expectedPersonId != null) { + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].personId", hasItem(expectedPersonId))); + } + if (expectedRequestedName != null) { + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].requestedName", hasItem(expectedRequestedName))); + } + if (expectedSurname != null) { + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].surname", hasItem(expectedSurname))); + } + if (expectedForenames != null) { + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].forenames", hasItem(expectedForenames))); + } + if (expectedEmailAddress != null) { + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].emailAddress", hasItem(expectedEmailAddress))); + } + for (final String specialism : expectedSpecialisms) { + matchers.add(withJsonPath(hearingIdFilter + ".judiciary[*].specialisms[*]", hasItem(specialism))); + } + pollForHearing(courtCentreId.toString(), isAllocated, getLoggedInUser().toString(), + matchers.toArray(new Matcher[0])); + } + /** * JMS-aware version of verifyHearingListedFromAPI for handling asynchronous message processing timing issues. */ @@ -599,15 +615,6 @@ public void verifyHearingListedFromAPIWithJmsDelay(final boolean isAllocated) { withJsonPath(lastNameFilter) }); } - - public void verifyFirstListedDefendantYouthStatusWithJmsDelay(final boolean isAllocated, final boolean expectedIsYouth) { - final HearingData hearingData = hearingsData.getHearingData().get(0); - pollForHearingWithJmsDelay(hearingData.getCourtCentreId().toString(), isAllocated, getLoggedInUser().toString(), new Matcher[]{ - withJsonPath("$.hearings[0].id", equalTo(hearingData.getId().toString())), - withJsonPath("$.hearings[0].listedCases[0].defendants[0].isYouth", equalTo(expectedIsYouth)) - }); - } - public void verifyHearingDayCourtScheduledUpdated() { final HearingData hearingData = hearingsData.getHearingData().get(0); final String hearingIdFilter = getHearingFilter(hearingData.getId().toString()); @@ -655,7 +662,7 @@ public void verifyQueryAPIFindCaseByPersonDefendantAndHearingDateForUnallocatedH private void verifyCaseByPersonDefendantAndHearingDate(final String caseId, final String urn, final String defendantId, final String firstName, final String lastName, final String dateOfBirth) { final String searchHearingUrl = String.format("%s/%s", getBaseUri(), - format(readConfig().getProperty("listing.get.cases-by-person-defendant"), firstName, lastName, dateOfBirth, ItClock.today())); + format(readConfig().getProperty("listing.get.cases-by-person-defendant"), firstName, lastName, dateOfBirth, LocalDate.now())); setupAsAuthorizedUserToQueryCaseByDefendantAndHearingDate(getLoggedInUser()); @@ -684,7 +691,7 @@ public void verifyQueryAPIFindCaseByOrganisationDefendantAndHearingDate() { private void verifyCaseByOrganisationDefendantAndHearingDate(final String caseId, final String urn, final String defendantId, final String organisationName) { final String searchHearingUrl = String.format("%s/%s", getBaseUri(), - format(readConfig().getProperty("listing.get.cases-by-organisation-defendant"), organisationName, ItClock.today())); + format(readConfig().getProperty("listing.get.cases-by-organisation-defendant"), organisationName, LocalDate.now())); stubDefenceQueryApiForSearchCasesByOrganisationDefendant(caseId, defendantId); setupAsAuthorizedUserToQueryCaseByDefendantAndHearingDate(getLoggedInUser()); @@ -701,15 +708,10 @@ private void verifyCaseByOrganisationDefendantAndHearingDate(final String caseId } public void verifyPublicEventHearingListed() { - final String expectedHearingId = hearingsData.getHearingData().get(0).getId().toString(); - // Match by hearingId so a stale hearing-listed event from another test on the shared - // public topic is skipped rather than consumed (drains until this hearing's event arrives). - final JsonPath jsonResponse = retrieveMessage(publicEventHearingListed, - org.hamcrest.CoreMatchers.containsString(expectedHearingId)); - assertNotNull(jsonResponse, "No public hearing-listed event found for hearingId=" + expectedHearingId); + final JsonPath jsonResponse = retrieveMessage(publicEventHearingListed); LOGGER.info("jsonResponse from publicEventHearingListed: {}", jsonResponse.prettify()); - assertThat(jsonResponse.get("hearingId"), is(expectedHearingId)); + assertThat(jsonResponse.get("hearingId"), is(hearingsData.getHearingData().get(0).getId().toString())); } public void verifyHearingListedWithAnyAllocationFromAPI(final boolean isAllocated) { @@ -861,53 +863,89 @@ public void verifyHearingListedWithHearingDays(final boolean isAllocated, final String courtCentreId = hearingData.getCourtCentreId().toString(); String userId = getLoggedInUser().toString(); - // The caseReference filter is a jayway filter path and matches even an empty - // hearings[] response, so it cannot gate the poll on its own — every expected - // value must be matched INSIDE the poll (IT-guide rule 3) or the assertions - // below run against a pre-projection snapshot and fail on slow (vld) stacks. - final Map expectedPollValues = buildExpectedJsonValues(hearingData, courtScheduleSlots, courtRoomIds); - final List pollMatchers = new ArrayList<>(); - pollMatchers.add(withJsonPath(caseReferenceFilter)); - for (final Map.Entry entry : expectedPollValues.entrySet()) { - pollMatchers.add(withJsonPath(entry.getKey(), equalTo(entry.getValue()))); + // Keep only caseReferenceFilter in poll for initial verification + // Use JMS-aware polling to handle asynchronous message processing + String jsonResponse = pollForHearingWithJmsDelay(courtCentreId, isAllocated, userId, new Matcher[]{ + withJsonPath(caseReferenceFilter) }); + + List failedAssertions = new ArrayList<>(); + + // Check other matchers separately for clearer debugging + validateJsonPath(jsonResponse, lastNameFilter, failedAssertions, "lastName"); + + // Dynamically find the index of the target hearing so assertions don't fail when + // there are multiple hearings in the response (e.g. HMI-enabled variant adds latency + // and the hearing may not land at index 0). + int hearingIndex = findHearingIndex(jsonResponse, hearingData.getId().toString()); + + Map expectedValues = buildExpectedJsonValues(hearingData, courtScheduleSlots, courtRoomIds, hearingIndex); + + for (Map.Entry entry : expectedValues.entrySet()) { + try { + Object actualValue = read(jsonResponse, entry.getKey()); + if (!Objects.equals(actualValue, entry.getValue())) { + failedAssertions.add(String.format("Mismatch at path '%s': expected '%s', but was '%s'", + entry.getKey(), entry.getValue(), actualValue)); + } + } catch (PathNotFoundException e) { + failedAssertions.add("Missing path: " + entry.getKey()); + } } - String jsonResponse = pollForHearingWithJmsDelay(courtCentreId, isAllocated, userId, - pollMatchers.toArray(new Matcher[0])); - - // Poll until the WHOLE hearing projection is present, not just the case reference. The defendant - // lastName, the N hearingDays and the allocation fields are projected asynchronously and can lag the - // case-reference write under suite load; asserting them on the first snapshot where the case ref - // appears is racy (intermittent "Failed JsonPath check: lastName" / "Missing path: ..."). Keeping - // every check inside the poll lets it retry until the full hearing has materialised. - final List matchers = new ArrayList<>(); - matchers.add(withJsonPath(caseReferenceFilter)); - matchers.add(withJsonPath(lastNameFilter)); - buildExpectedJsonValues(hearingData, courtScheduleSlots, courtRoomIds) - .forEach((path, value) -> matchers.add(withJsonPath(path, is(value)))); - pollForHearingWithJmsDelay(courtCentreId, isAllocated, userId, matchers.toArray(new Matcher[0])); + if (!failedAssertions.isEmpty()) { + fail("Following JSONPath assertions failed:\n" + String.join("\n", failedAssertions)); + } } - private Map buildExpectedJsonValues(HearingData hearingData, String[] courtScheduleSlots, String[] courtRoomIds) { - Map expected = new LinkedHashMap<>(); + private void validateJsonPath(String json, com.jayway.jsonpath.JsonPath path, + List failedAssertions, String label) { + try { + Object result = path.read(json); + if (result == null || (result instanceof Collection && ((Collection) result).isEmpty())) { + failedAssertions.add("Failed JsonPath check: " + label); + } + } catch (Exception e) { + failedAssertions.add("Invalid JsonPath or value missing: " + label + " - " + e.getMessage()); + } + } - expected.put("$.hearings[0].id", hearingData.getId().toString()); - expected.put("$.hearings[0].jurisdictionType", hearingData.getJurisdictionType()); - expected.put("$.hearings[0].courtCentreId", hearingData.getCourtCentreId().toString()); - expected.put("$.hearings[0].courtRoomId", hearingData.getCourtRoomId().toString()); + /** + * Finds the array index of the hearing with the given ID in the JSON response. + * Falls back to 0 if the ID cannot be located, so non-HMI variants are unaffected. + */ + private int findHearingIndex(final String jsonResponse, final String hearingId) { + try { + List ids = read(jsonResponse, "$.hearings[*].id"); + for (int i = 0; i < ids.size(); i++) { + if (hearingId.equals(ids.get(i))) { + return i; + } + } + } catch (Exception ignored) { + // Fall through to default + } + return 0; + } - expected.put("$.hearings[0].type.id", hearingData.getHearingTypeData().getTypeId().toString()); - expected.put("$.hearings[0].type.description", hearingData.getHearingTypeData().getTypeDescription()); - expected.put("$.hearings[0].startDate", hearingData.getHearingStartDate().toString()); + private Map buildExpectedJsonValues(HearingData hearingData, String[] courtScheduleSlots, String[] courtRoomIds, int hearingIndex) { + Map expected = new LinkedHashMap<>(); + final String h = "$.hearings[" + hearingIndex + "]"; - // You would continue this pattern for courtApplications, applicants, respondents, etc. + expected.put(h + ".id", hearingData.getId().toString()); + expected.put(h + ".jurisdictionType", hearingData.getJurisdictionType()); + expected.put(h + ".courtCentreId", hearingData.getCourtCentreId().toString()); + expected.put(h + ".courtRoomId", hearingData.getCourtRoomId().toString()); + + expected.put(h + ".type.id", hearingData.getHearingTypeData().getTypeId().toString()); + expected.put(h + ".type.description", hearingData.getHearingTypeData().getTypeDescription()); + expected.put(h + ".startDate", hearingData.getHearingStartDate().toString()); for (int i = 0; i < courtScheduleSlots.length; i++) { - expected.put("$.hearings[0].hearingDays[" + i + "].hearingDate", courtScheduleSlots[i]); - expected.put("$.hearings[0].hearingDays[" + i + "].courtRoomId", courtRoomIds[i]); + expected.put(h + ".hearingDays[" + i + "].hearingDate", courtScheduleSlots[i]); + expected.put(h + ".hearingDays[" + i + "].courtRoomId", courtRoomIds[i]); } - expected.put("$.hearings[0].listedCases[0].defendants[0].isYouth", true); + expected.put(h + ".listedCases[0].defendants[0].isYouth", true); return expected; } @@ -1229,7 +1267,7 @@ private void verifyHearingDetails(final CaseAndDefendantData caseAndDefendantDat withJsonPath("$.hearings[0].allocated", equalTo(true)), withJsonPath("$.hearings[0].endDate", - equalTo(ItClock.today().toString())), + equalTo(LocalDate.now().toString())), withJsonPath("$.hearings[0].listedCases[0].caseIdentifier.caseReference", equalTo(caseAndDefendantData.getCaseUrn())), withJsonPath("$.hearings[0].listedCases[0].defendants[0].masterDefendantId", @@ -1254,7 +1292,7 @@ private void verifyHearingDetailsWithJmsDelay(final CaseAndDefendantData caseAnd withJsonPath("$.hearings[0].allocated", equalTo(true)), withJsonPath("$.hearings[0].endDate", - equalTo(ItClock.today().toString())), + equalTo(LocalDate.now().toString())), withJsonPath("$.hearings[0].listedCases[0].caseIdentifier.caseReference", equalTo(caseAndDefendantData.getCaseUrn())), withJsonPath("$.hearings[0].listedCases[0].defendants[0].masterDefendantId", @@ -1282,7 +1320,7 @@ public void verifyAvailableHearingListedForMatchedDefendant(final CaseAndDefenda withJsonPath("$.hearings[0].allocated", equalTo(true)), withJsonPath("$.hearings[0].endDate", - equalTo(ItClock.today().toString())), + equalTo(LocalDate.now().toString())), withJsonPath("$.hearings[0].listedCases[0].caseIdentifier.caseReference", not(caseAndDefendantData.getCaseUrn())), withJsonPath("$.hearings[0].listedCases[0].defendants[0].masterDefendantId", @@ -1314,7 +1352,7 @@ public void verifyAvailableHearingListedForMatchedDefendantWithJmsDelay(final Ca withJsonPath("$.hearings[0].allocated", equalTo(true)), withJsonPath("$.hearings[0].endDate", - equalTo(ItClock.today().toString())), + equalTo(LocalDate.now().toString())), withJsonPath("$.hearings[0].listedCases[0].caseIdentifier.caseReference", not(caseAndDefendantData.getCaseUrn())), withJsonPath("$.hearings[0].listedCases[0].defendants[0].masterDefendantId", @@ -1340,7 +1378,7 @@ public void verifyAllAvailableHearingListedForMatchedDefendant(final CaseAndDefe withJsonPath("$.hearings[0].jurisdictionType", equalTo(JurisdictionType.CROWN.name())), withJsonPath("$.hearings[0].endDate", - equalTo(ItClock.today().toString())), + equalTo(LocalDate.now().toString())), withJsonPath("$.hearings[0].listedCases[0].caseIdentifier.caseReference", equalTo(caseAndDefendantData.getCaseUrn())), withJsonPath("$.hearings[0].listedCases[0].defendants[0].masterDefendantId", @@ -1348,7 +1386,7 @@ public void verifyAllAvailableHearingListedForMatchedDefendant(final CaseAndDefe withJsonPath("$.hearings[1].jurisdictionType", equalTo(JurisdictionType.CROWN.name())), withJsonPath("$.hearings[1].endDate", - equalTo(ItClock.today().toString())), + equalTo(LocalDate.now().toString())), withJsonPath("$.hearings[1].listedCases[0].caseIdentifier.caseReference", equalTo(caseAndDefendantData.getCaseUrn())), withJsonPath("$.hearings[1].listedCases[0].defendants[0].masterDefendantId", @@ -1382,7 +1420,7 @@ public void verifyAllAvailableHearingListedForMatchedDefendantWithJmsDelay(final withJsonPath("$.hearings[0].jurisdictionType", equalTo(JurisdictionType.CROWN.name())), withJsonPath("$.hearings[0].endDate", - equalTo(ItClock.today().toString())), + equalTo(LocalDate.now().toString())), withJsonPath("$.hearings[0].listedCases[0].caseIdentifier.caseReference", equalTo(caseAndDefendantData.getCaseUrn())), withJsonPath("$.hearings[0].listedCases[0].defendants[0].masterDefendantId", @@ -1390,7 +1428,7 @@ public void verifyAllAvailableHearingListedForMatchedDefendantWithJmsDelay(final withJsonPath("$.hearings[1].jurisdictionType", equalTo(JurisdictionType.CROWN.name())), withJsonPath("$.hearings[1].endDate", - equalTo(ItClock.today().toString())), + equalTo(LocalDate.now().toString())), withJsonPath("$.hearings[1].listedCases[0].caseIdentifier.caseReference", equalTo(caseAndDefendantData.getCaseUrn())), withJsonPath("$.hearings[1].listedCases[0].defendants[0].masterDefendantId", @@ -1472,7 +1510,7 @@ public void verifyAvailableHearing(final CaseAndDefendantData caseAndDefendantDa withJsonPath("$.hearings[0].allocated", equalTo(true)), withJsonPath("$.hearings[0].endDate", - equalTo(ItClock.today().toString())), + equalTo(LocalDate.now().toString())), withJsonPath("$.hearings[0].listedCases[0].caseIdentifier.caseReference", equalTo(caseAndDefendantData.getCaseUrn())), withJsonPath("$.hearings[0].listedCases[0].defendants[0].masterDefendantId", @@ -1501,7 +1539,7 @@ public void verifyAvailableHearingWithJmsDelay(final CaseAndDefendantData caseAn withJsonPath("$.hearings[0].allocated", equalTo(true)), withJsonPath("$.hearings[0].endDate", - equalTo(ItClock.today().toString())), + equalTo(LocalDate.now().toString())), withJsonPath("$.hearings[0].listedCases[0].caseIdentifier.caseReference", equalTo(caseAndDefendantData.getCaseUrn())), withJsonPath("$.hearings[0].listedCases[0].defendants[0].masterDefendantId", @@ -1595,7 +1633,7 @@ private ListCourtHearing createListCourtHearingBookedSlotData(final HearingsData final HearingData hearingData = hearingsData.getHearingData().get(0); return ListCourtHearing.listCourtHearing() - .withAdjournedFromDate(ItClock.today().toString()) + .withAdjournedFromDate(LocalDate.now().toString()) .withHearings(singletonList(HearingListingNeeds.hearingListingNeeds() .withBookedSlots(hearingData.getBookedSlots()) .withCourtCentre(CourtCentre.courtCentre() @@ -1652,14 +1690,9 @@ private ListCourtHearing getListCourtHearingData(final HearingsData hearingsData // Determine if hearing is allocated (has court room) or unallocated final boolean isAllocated = hearingData.getCourtRoomId() != null; - // CROWN treats the bookingReference as the courtScheduleId; the command resolves it via - // search.court-schedules-by-id. Stub that resolution (and the follow-up list call) to echo - // this hearing's own centre/room so the resolved session matches the listed values. - final UUID bookingReference = isAllocated ? randomUUID() : null; - stubCrownBookingReferenceResolution(hearingData, bookingReference); return ListCourtHearing.listCourtHearing() - .withAdjournedFromDate(ItClock.today().toString()) + .withAdjournedFromDate(LocalDate.now().toString()) .withShadowListedOffences(shadowListedOffences) .withHearings(List.of(HearingListingNeeds.hearingListingNeeds() .withCourtCentre(CourtCentre.courtCentre() @@ -1668,7 +1701,7 @@ private ListCourtHearing getListCourtHearingData(final HearingsData hearingsData .withRoomId(hearingData.getCourtRoomId()) .build()) // Only add booking reference for allocated hearings - .withBookingReference(bookingReference) + .withBookingReference(isAllocated ? randomUUID() : null) .withListedStartDateTime(hearingData.getHearingStartTime() != null ? hearingData.getHearingStartTime() : null) .withCourtApplications(isNull(hearingData.getCourtApplications()) ? null : singletonList(CourtApplication.courtApplication() .withId(hearingData.getCourtApplications().get(0).getId()) @@ -1688,12 +1721,12 @@ private ListCourtHearing getListCourtHearingData(final HearingsData hearingsData .withCount(OFFENCE_COUNT) .withOrderIndex(OFFENCE_ORDER_INDEX) .withOffenceLegislation(OFFENCE_LEGISLATION) - .withStartDate(ItClock.today().toString()) + .withStartDate(LocalDate.now().toString()) .build())) .build())) .withParentApplicationId(hearingData.getCourtApplications().get(0).getParentApplicationId()) .withType(getCourtApplicationType(hearingData)) - .withApplicationReceivedDate(ItClock.today().toString()) + .withApplicationReceivedDate(LocalDate.now().toString()) .withApplicationReference(STRING.next()) .withApplicationParticulars(hearingData.getCourtApplications().get(0).getApplicationParticulars()) .withApplicationStatus(ApplicationStatus.LISTED) @@ -1744,7 +1777,7 @@ private ListCourtHearing getListCourtHearingData(final HearingsData hearingsData .withDefendants(lc.getDefendants().stream().map(d -> Defendant.defendant() .withId(d.getDefendantId()) .withMasterDefendantId(d.getMasterDefendantId()) - .withCourtProceedingsInitiated(ItClock.nowUtc()) + .withCourtProceedingsInitiated(ZonedDateTime.now()) .withIsYouth(d.getIsYouth()) .withPersonDefendant(gerPersonDefendant(d)) .withAssociatedPersons(singletonList(AssociatedPerson.associatedPerson() @@ -1758,7 +1791,7 @@ private ListCourtHearing getListCourtHearingData(final HearingsData hearingsData .withOffenceCode(STRING.next()) .withOffenceDefinitionId(randomUUID()) .withWording(STRING.next()) - .withStartDate(ItClock.today().toString()) + .withStartDate(LocalDate.now().toString()) .withOrderIndex(OFFENCE_ORDER_INDEX) .withOffenceTitle(o.getStatementOfOffenceTitle()) .withOffenceLegislation(OFFENCE_LEGISLATION) @@ -1766,13 +1799,13 @@ private ListCourtHearing getListCourtHearingData(final HearingsData hearingsData LaaReference.laaReference() .withApplicationReference(STRING.next()) .withStatusCode(STRING.next()) - .withStatusDate((format(ItClock.today().toString()))) + .withStatusDate((format(LocalDate.now().toString()))) .withStatusDescription(STRING.next()) .withStatusId(randomUUID()).build()) .withReportingRestrictions(List.of(ReportingRestriction.reportingRestriction().withId(randomUUID()) .withLabel("RestrictionApplied") .withJudicialResultId(JUDICIAL_RESULT_ID) - .withOrderedDate(ItClock.today().toString()).build())) + .withOrderedDate(LocalDate.now().toString()).build())) .withCivilOffence(CivilOffence.civilOffence().withIsExParte(o.getCivilOffenceData().getExParte()).build()) .build()) .collect(Collectors.toList())) @@ -1805,7 +1838,7 @@ private List buildCrownNonDefaultDays( .withCourtCentreId(hearingData.getCourtCentreId().toString()) .withRoomId(hearingData.getCourtRoomId().toString()) .withDuration(hearingData.getHearingEstimateMinutes()) - .withStartTime(hearingData.getHearingStartTime() != null ? hearingData.getHearingStartTime() : ItClock.nowUtc()) + .withStartTime(hearingData.getHearingStartTime() != null ? hearingData.getHearingStartTime() : java.time.ZonedDateTime.now()) .build()); } @@ -1824,33 +1857,16 @@ private Person getPerson(final DefendantData d) { } private CourtApplicationParty getApplicant(final CourtApplicationPartyData applicant) { - final CourtApplicationParty.Builder builder = CourtApplicationParty.courtApplicationParty() + return CourtApplicationParty.courtApplicationParty() .withId(applicant.getId()) + .withPersonDetails(Person.person().withLastName(applicant.getLastName()) + .withFirstName(applicant.getFirstName()) + .withGender(Gender.FEMALE) + .withAddress(getAddress(applicant.getAddress())) + .build()) .withSummonsRequired(false) - .withNotificationRequired(false); - if (applicant.getMasterDefendantId() != null) { - builder.withMasterDefendant(MasterDefendant.masterDefendant() - .withMasterDefendantId(applicant.getMasterDefendantId()) - .withPersonDefendant(PersonDefendant.personDefendant() - .withPersonDetails(Person.person() - .withLastName(applicant.getLastName()) - .withFirstName(applicant.getFirstName()) - .withGender(Gender.FEMALE) - .withAddress(getAddress(applicant.getAddress())) - .withDateOfBirth(applicant.getDateOfBirth() != null ? applicant.getDateOfBirth().toString() : null) - .build()) - .build()) - .build()); - } else { - builder.withPersonDetails(Person.person() - .withLastName(applicant.getLastName()) - .withFirstName(applicant.getFirstName()) - .withGender(Gender.FEMALE) - .withAddress(getAddress(applicant.getAddress())) - .withDateOfBirth(applicant.getDateOfBirth() != null ? applicant.getDateOfBirth().toString() : null) - .build()); - } - return builder.build(); + .withNotificationRequired(false) + .build(); } private Address getAddress(final uk.gov.moj.cpp.listing.domain.Address address) { @@ -1921,7 +1937,7 @@ private ListCourtHearing getListCourtHearingDataWithLegalEntity(final HearingsDa .withParentApplicationId(hearingData.getCourtApplications().get(0).getParentApplicationId()) .withApplicationParticulars(hearingData.getCourtApplications().get(0).getApplicationParticulars()) .withType(getCourtApplicationType(hearingData)) - .withApplicationReceivedDate(ItClock.today().toString()) + .withApplicationReceivedDate(LocalDate.now().toString()) .withApplicationReference(STRING.next()) .withApplicationStatus(ApplicationStatus.LISTED) .withApplicant(applicant) @@ -1985,7 +2001,7 @@ private Defendant getDefendant(final ListedCaseData listedCaseData, final Defend return Defendant.defendant() .withId(d.getDefendantId()) .withMasterDefendantId(d.getMasterDefendantId()) - .withCourtProceedingsInitiated(ItClock.nowUtc()) + .withCourtProceedingsInitiated(ZonedDateTime.now()) .withLegalEntityDefendant(LegalEntityDefendant.legalEntityDefendant() .withOrganisation(organisation() .withName(ORGANISATION_NAME) @@ -2014,7 +2030,7 @@ private ListCourtHearing getListCourtHearingDataStandaloneApplication(final Hear .withParentApplicationId(hearingData.getCourtApplications().get(0).getParentApplicationId()) .withApplicationParticulars(hearingData.getCourtApplications().get(0).getApplicationParticulars()) .withType(getCourtApplicationType(hearingData)) - .withApplicationReceivedDate(ItClock.today().toString()) + .withApplicationReceivedDate(LocalDate.now().toString()) .withApplicationReference(STRING.next()) .withApplicationStatus(ApplicationStatus.DRAFT) .withCourtApplicationCases(singletonList(CourtApplicationCase.courtApplicationCase() @@ -2028,7 +2044,7 @@ private ListCourtHearing getListCourtHearingDataStandaloneApplication(final Hear .build())) .withApplicant(getApplicant(hearingData.getCourtApplications().get(0).getApplicant())) .withRespondents(singletonList(CourtApplicationParty.courtApplicationParty() - .withId(hearingData.getCourtApplications().get(0).getRespondent().getId()) + .withId(randomUUID()) .withPersonDetails(Person.person().withLastName(hearingData.getCourtApplications().get(0).getRespondent().getLastName()) .withFirstName(hearingData.getCourtApplications().get(0).getRespondent().getFirstName()) .withGender(Gender.FEMALE) @@ -2075,13 +2091,8 @@ private ListCourtHearing getListForCourtHearingData(final HearingsData hearingsD .map(offence -> offence.getOffenceId()) .collect(Collectors.toList()); - // CROWN: resolve the bookingReference (= courtScheduleId) via search.court-schedules-by-id; stub it to - // echo this hearing's own centre/room so the resolved session matches the listed values. - final UUID bookingReference = randomUUID(); - stubCrownBookingReferenceResolution(hearingData, bookingReference); - return ListCourtHearing.listCourtHearing() - .withAdjournedFromDate(ItClock.today().toString()) + .withAdjournedFromDate(LocalDate.now().toString()) .withShadowListedOffences(shadowListedOffences) .withHearings(singletonList(HearingListingNeeds.hearingListingNeeds() .withCourtCentre(CourtCentre.courtCentre() @@ -2089,7 +2100,7 @@ private ListCourtHearing getListForCourtHearingData(final HearingsData hearingsD .withName(hearingData.getName()) .withRoomId(hearingData.getCourtRoomId()) .build()) - .withBookingReference(bookingReference) + .withBookingReference(randomUUID()) .withCourtApplications(singletonList(getCourtApplication(hearingData))) .withCourtApplicationPartyListingNeeds(hearingData.getCourtApplicationPartyNeeds()) .withId(hearingData.getId()) @@ -2130,7 +2141,7 @@ private ListCourtHearing getListForCourtHearingData(final HearingsData hearingsD .withDefendants(lc.getDefendants().stream().map(d -> Defendant.defendant() .withId(d.getDefendantId()) .withMasterDefendantId(d.getMasterDefendantId()) - .withCourtProceedingsInitiated(ItClock.nowUtc()) + .withCourtProceedingsInitiated(ZonedDateTime.now()) .withIsYouth(d.getIsYouth()) .withPersonDefendant(gerPersonDefendant(d)) .withAssociatedPersons(singletonList(AssociatedPerson.associatedPerson() @@ -2172,7 +2183,7 @@ private CourtApplication getCourtApplication(final HearingData hearingData) { .build())) .withParentApplicationId(hearingData.getCourtApplications().get(0).getParentApplicationId()) .withType(getCourtApplicationType(hearingData)) - .withApplicationReceivedDate(ItClock.today().toString()) + .withApplicationReceivedDate(LocalDate.now().toString()) .withApplicationReference(STRING.next()) .withApplicationParticulars(hearingData.getCourtApplications().get(0).getApplicationParticulars()) .withApplicationStatus(ApplicationStatus.LISTED) @@ -2198,7 +2209,7 @@ private PersonDefendant gerPersonDefendant(final DefendantData d) { .withObservedEthnicityId(randomUUID()) .withObservedEthnicityDescription(STRING.next()) .build()) - .withDateOfBirth(d.getDateOfBirth().toString()) + .withDateOfBirth(LocalDate.now().minusYears(21).toString()) .build()) .build(); } @@ -2210,14 +2221,14 @@ private Offence getOffence(final OffenceData o, final Integer next) { .withOffenceCode(STRING.next()) .withOffenceDefinitionId(randomUUID()) .withWording(STRING.next()) - .withStartDate(ItClock.today().toString()) + .withStartDate(LocalDate.now().toString()) .withOrderIndex(INTEGER.next()) .withOffenceTitle(o.getStatementOfOffenceTitle()) .withLaaApplnReference( LaaReference.laaReference() .withApplicationReference(STRING.next()) .withStatusCode(STRING.next()) - .withStatusDate((format(ItClock.today().toString()))) + .withStatusDate((format(LocalDate.now().toString()))) .withStatusDescription(STRING.next()) .withStatusId(randomUUID()).build()) .build(); @@ -2381,8 +2392,7 @@ public void verifyPublicEventHearingConfirmedAndExtendHearingFromProgression(fin assertThat(jsonResponse.get("confirmedHearing.prosecutionCases.size()"), is(1)); final String allocatedHearingCaseId = jsonResponse.get("confirmedHearing.prosecutionCases[0].id"); - final JsonPath jsonResponse1 = retrieveMessage(publicMessageConsumerHearingConfirmedForExtendHearing, isJson(Matchers.allOf( - withJsonPath("$.confirmedHearing.id", is(unAllocatedHearingId.toString()))))); + final JsonPath jsonResponse1 = retrieveMessage(publicMessageConsumerHearingConfirmedForExtendHearing); assertThat(jsonResponse1.getBoolean("sendNotificationToParties"), is(true)); assertThat(jsonResponse1.get("confirmedHearing.id"), is(unAllocatedHearingId.toString())); @@ -2418,15 +2428,13 @@ public void verifyPublicEventHearingConfirmedAndExtendHearingFromProgression(fin public void verifyPublicEventHearingConfirmedEventAndExtendPartialHearingFromProgression(final UUID allocatedHearingId, final UUID unAllocatedHearingId) { final List newCaseIds = new ArrayList<>(); - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingConfirmedForExtendHearing, isJson(Matchers.allOf( - withJsonPath("$.confirmedHearing.id", is(allocatedHearingId.toString()))))); + final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingConfirmedForExtendHearing); assertThat(jsonResponse.get("confirmedHearing.id"), is(allocatedHearingId.toString())); assertThat(jsonResponse.get("confirmedHearing.prosecutionCases.size()"), is(1)); final String allocatedHearingCaseId = jsonResponse.get("confirmedHearing.prosecutionCases[0].id"); - final JsonPath jsonResponse1 = retrieveMessage(publicMessageConsumerHearingConfirmedForExtendHearing, isJson(Matchers.allOf( - withJsonPath("$.confirmedHearing.id", is(unAllocatedHearingId.toString()))))); + final JsonPath jsonResponse1 = retrieveMessage(publicMessageConsumerHearingConfirmedForExtendHearing); assertThat(jsonResponse1.get("confirmedHearing.id"), is(unAllocatedHearingId.toString())); assertThat(jsonResponse1.get("confirmedHearing.prosecutionCases.size()"), is(1)); @@ -2464,16 +2472,12 @@ public void verifyHearingUpdatedToCaseInActiveMQ(final UUID allocatedHearingId, } public void verifyPublicEventHearingUpdatedPartially(final UUID hearingId) { - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingPartiallyUpdated, - containsString(hearingId.toString())); - assertNotNull(jsonResponse, "No public hearing-partially-updated event found for hearingId=" + hearingId); + final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingPartiallyUpdated); assertThat(jsonResponse.get("hearingIdToBeUpdated"), is(hearingId.toString())); } public void verifyPublicEVentHearingChangesSaved(final UUID hearingId) { - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingChangesSaved, - containsString(hearingId.toString())); - assertNotNull(jsonResponse, "No public hearing-changes-saved event found for hearingId=" + hearingId); + final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingChangesSaved); assertThat(jsonResponse.get("hearingId"), is(hearingId.toString())); } @@ -2496,26 +2500,17 @@ public void verifyPublicEventHearingConfirmed() { } public JsonPath getHearingConfirmedPublicEventPayload() { - // NOTE: shared getter — callers (e.g. GroupCasesIT) use a Steps instance without hearingsData set, - // so it cannot be filtered by this.hearingsData. Filter at the call site where the expected id is known. return retrieveMessage(publicMessageConsumerHearingConfirmedForExtendHearing); } - // noteId is derived server-side from (courtRoomId, hearingDate): creating the same pair twice - // makes the ListingNote aggregate log ERROR "Note already exists" and no-op, so both helpers - // de-duplicate before posting (hearings in shared test data often reuse a courtroom). public void createListingNotes() { - this.hearingsData.getHearingData().stream().filter(hearing -> hearing.getCourtRoomId() != null) - .map(HearingData::getCourtRoomId) - .distinct() - .forEach(courtRoomId -> notesSteps.createNoteForListing(courtRoomId, "2020-05-21", "note 1")); + this.hearingsData.getHearingData().stream().filter(hearing -> hearing.getCourtRoomId() != null). + forEach(hearing -> notesSteps.createNoteForListing(hearing.getCourtRoomId(), "2020-05-21", "note 1")); } public void createListingNotesForStartDays() { - this.hearingsData.getHearingData().stream().filter(hearing -> hearing.getCourtRoomId() != null) - .map(hearing -> java.util.Map.entry(hearing.getCourtRoomId(), hearing.getHearingStartDate().toString())) - .distinct() - .forEach(roomAndDate -> notesSteps.createNoteForListing(roomAndDate.getKey(), roomAndDate.getValue(), "note 1")); + this.hearingsData.getHearingData().stream().filter(hearing -> hearing.getCourtRoomId() != null). + forEach(hearing -> notesSteps.createNoteForListing(hearing.getCourtRoomId(), hearing.getHearingStartDate().toString(), "note 1")); } public void listCourtHearing(final JsonObject listCourtHearingJsonObject, Optional adjournedFromDate, Optional> shadowListedOffences) { @@ -2567,7 +2562,6 @@ public void listCourtHearing(final JsonObject listCourtHearingJsonObject, final final CourtCentreData courtCentreData = new CourtCentreData(courtCentreId, DEFAULT_START_TIME, DEFAULT_DURATION_HOURS_MINS, null, "City of London Magistrates' Court"); stubGetReferenceDataCourtCentreById(courtCentreData); stubGetReferenceDataHearingTypes(hearingTypeId); - stubCrownBookingReferenceResolutionFromPayload(listCourtHearingJsonObject, courtCentreId); final String listCaseForHearingUrl = String.format("%s/%s", getBaseUri(), format (readConfig().getProperty(LISTING_COMMAND_LIST_COURT_HEARING))); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListCourtHearingStepsWithWeekCommencing.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListCourtHearingStepsWithWeekCommencing.java index e888ea397..08db91d5f 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListCourtHearingStepsWithWeekCommencing.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListCourtHearingStepsWithWeekCommencing.java @@ -1,21 +1,18 @@ package uk.gov.moj.cpp.listing.steps; +import static java.time.LocalDate.now; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.UUID.randomUUID; import static uk.gov.moj.cpp.listing.steps.data.HearingsData.hearingsDataForWeekCommencing; import static uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData.updatedHearingData; import static uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData.updatedHearingDataWithWeekCommencingDate; -import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubGetCourtSchedulesByIdWithDraftStatus; -import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessions; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.getRandomCourtRoomId; import uk.gov.moj.cpp.listing.steps.data.HearingsData; import uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; -import java.time.ZoneOffset; import java.util.List; import java.util.UUID; @@ -23,10 +20,10 @@ public class ListCourtHearingStepsWithWeekCommencing { - private final static LocalDate DEFAULT_START_DATE = ItClock.today(); - private final static LocalDate DEFAULT_END_DATE = ItClock.today().plusDays(1L); + private final static LocalDate DEFAULT_START_DATE = now(); + private final static LocalDate DEFAULT_END_DATE = now().plusDays(1L); - private final static String WEEK_COMMENCING_START_DATE = ItClock.today().toString(); + private final static String WEEK_COMMENCING_START_DATE = now().toString(); public static List loadFixedHearingData() { final UUID firstFixedHearingId = randomUUID(); @@ -41,21 +38,21 @@ public static List loadFixedHearingData() { final UUID secondCourtRoomId = getRandomCourtRoomId(asList(firstCourtRoomId)); final UUID thirdCourtRoomId = getRandomCourtRoomId(asList(firstCourtRoomId, secondCourtRoomId)); - final LocalDate firstFixedHearingStartDate = ItClock.today().plusDays(1); - final LocalDate secondFixedHearingStartDate = ItClock.today(); - final LocalDate thirdFixedHearingStartDate = ItClock.today(); - final LocalDate fourthFixedHearingStartDate = ItClock.today().plusDays(4L); + final LocalDate firstFixedHearingStartDate = now().plusDays(1); + final LocalDate secondFixedHearingStartDate = now(); + final LocalDate thirdFixedHearingStartDate = now(); + final LocalDate fourthFixedHearingStartDate = now().plusDays(4L); - final LocalDate firstFixedHearingEndDate = ItClock.today().plusDays(2); - final LocalDate secondFixedHearingEndDate = ItClock.today().plusDays(1); - final LocalDate thirdFixedHearingEndDate = ItClock.today().plusDays(4L); - final LocalDate fourthFixedHearingEndDate = ItClock.today().plusDays(5L); + final LocalDate firstFixedHearingEndDate = now().plusDays(2); + final LocalDate secondFixedHearingEndDate = now().plusDays(1); + final LocalDate thirdFixedHearingEndDate = now().plusDays(4L); + final LocalDate fourthFixedHearingEndDate = now().plusDays(5L); final HearingsData hearingsData1 = hearingsDataForWeekCommencing(firstFixedHearingId, firstFixedHearingEndDate, firstCourtRoomId, null, null, firstFixedHearingStartDate); final HearingsData hearingsData2 = hearingsDataForWeekCommencing(secondFixedHearingId, secondFixedHearingEndDate, secondCourtRoomId, null, null, secondFixedHearingStartDate); final HearingsData hearingsData3 = hearingsDataForWeekCommencing(thirdFixedHearingId, thirdFixedHearingEndDate, thirdCourtRoomId, null, null, thirdFixedHearingStartDate); final HearingsData hearingsData4 = hearingsDataForWeekCommencing(fourthFixedHearingId, fourthFixedHearingEndDate, firstCourtRoomId, null, null, fourthFixedHearingStartDate); - final HearingsData hearingsData5 = hearingsDataForWeekCommencing(seventhFixedHearingId, ItClock.today(), null, null, null, ItClock.today()); + final HearingsData hearingsData5 = hearingsDataForWeekCommencing(seventhFixedHearingId, now(), null, null, null, now()); final HearingsData hearingsData6 = hearingsDataForWeekCommencing(fifthFixedHearingId, DEFAULT_END_DATE, secondCourtRoomId, null, null, DEFAULT_START_DATE); final HearingsData hearingsData7 = hearingsDataForWeekCommencing(sixthFixedHearingId, DEFAULT_END_DATE, thirdCourtRoomId, null, null, DEFAULT_START_DATE); @@ -81,25 +78,6 @@ public static void verifyHearingListedForWeekCommencing(final String jurisdictio public static UpdatedHearingData updatedHearingListedData(final HearingsData hearingsData) { UpdatedHearingData updatedHearingData = updatedHearingData(hearingsData.getHearingData().get(0)); - // CROWN updates carry a courtScheduleId on their nonDefaultDay; the single-day update - // enrichment re-fetches it via search.court-schedules-by-id. Without these stubs the - // catch-all answers with an alien shape -> parses empty -> WARN "CROWN single-day - // update: failed to fetch court schedules ... Returning unchanged". - updatedHearingData.getNonDefaultDays().get(0).getCourtScheduleId().ifPresent(courtScheduleId -> { - final LocalDate updatedStartDate = LocalDate.parse(updatedHearingData.getStartDate()); - // Both stubs MUST carry the UPDATED start date/time: the enrichment rebuilds the - // hearing day from these session responses, so stale (seed-time) values would - // silently revert the very date this update is asserting on. - final java.time.ZonedDateTime updatedStartTime = updatedStartDate.atTime(10, 0).atZone(ZoneOffset.UTC); - stubGetCourtSchedulesByIdWithDraftStatus(singletonList(courtScheduleId), false, - updatedStartDate, - updatedHearingData.getCourtCentreId(), - updatedHearingData.getCourtRoomId(), - updatedStartTime); - stubListHearingInCourtSessions(hearingsData.getHearingData().get(0).getId().toString(), - courtScheduleId, - updatedStartTime); - }); final UpdateHearingSteps updateHearingSteps = new UpdateHearingSteps(hearingsData, updatedHearingData); updateHearingSteps.whenHearingIsUpdatedForListing(); updateHearingSteps.verifyHearingUpdatedWhenQueryingFromAPI(); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListNextHearingSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListNextHearingSteps.java index c9394c1a6..370ced5c8 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListNextHearingSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListNextHearingSteps.java @@ -8,7 +8,6 @@ import static org.apache.http.HttpStatus.SC_ACCEPTED; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.anyOf; -import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.CoreMatchers.is; @@ -29,7 +28,6 @@ import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.getHearingFilter; import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearing; import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearingWithJmsDelay; -import static uk.gov.moj.cpp.listing.it.util.PublishRetryHelper.verifyOrRepublishUntilReflected; import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.pollWithDefaults; import static uk.gov.moj.cpp.listing.steps.ListCourtHearingSteps.getJsonPathQueryForCaseReference; import static uk.gov.moj.cpp.listing.steps.ListCourtHearingSteps.getJsonPathQueryForDefendantLastName; @@ -43,9 +41,7 @@ import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCourtCentreById; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataCourtMappings; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataHearingTypes; -import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessionsForCourtSchedule; import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubSearchBookHearingSlotsForCrown; -import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubSearchCourtSchedulesByIdSession; import static uk.gov.moj.cpp.listing.utils.ReferenceDataStub.stubGetReferenceDataJudiciaries; import uk.gov.justice.core.courts.Address; @@ -98,7 +94,6 @@ import uk.gov.moj.cpp.listing.steps.data.ListedCaseData; import uk.gov.moj.cpp.listing.steps.data.OffenceData; import uk.gov.moj.cpp.listing.utils.QueueUtil; -import uk.gov.moj.cpp.listing.steps.UpdateDefendantOffencesSteps; import java.time.LocalDate; import java.time.LocalTime; @@ -111,7 +106,6 @@ import java.util.stream.Collectors; import uk.gov.justice.services.messaging.JsonObjects; -import uk.gov.moj.cpp.listing.it.util.ItClock; import javax.json.JsonObject; import javax.ws.rs.core.Response; @@ -119,7 +113,6 @@ import com.jayway.jsonpath.ReadContext; import io.restassured.path.json.JsonPath; import org.hamcrest.Matcher; -import org.awaitility.core.ConditionTimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -484,32 +477,6 @@ public void verifyOffenceAddedToAllocatedHearingFromApi(HearingsData existedHear ))); } - /** - * Verifies the read model reflects a new offence, re-publishing the - * {@code public.progression.offences-for-defendant-changed} event (with a fresh metadata id each time) - * if the first verify times out, up to {@code maxPublishAttempts} total attempts. - * - *

Why re-publish instead of publish-once-then-poll? The event is consumed async by - * {@code ListingEventProcessor} and routed to the {@code Case} aggregate. If the case's hearing-link - * has not yet been established when the first publish is processed, the update is silently dropped with - * no JMS redelivery, so a single publish is lost forever on slow CI (vld pipeline). Re-publishing with - * a fresh metadata id each time (the steps instance regenerates it in - * {@code publishCaseDefendantOffencesUpdated}) guarantees that once the link is established, a - * subsequent publish lands. Root-cause class: cross-aggregate eventual-consistency race, same pattern - * as {@code UpdateCaseMarkersSteps#publishUntilCaseMarkersReflected}. - * - *

The caller is expected to have already done the initial publish and to pass the offence - * id extracted from that publish's return value. This method first tries to verify the existing - * publish; only if that times out does it re-publish and retry. - */ - public void verifyOrRepublishUntilOffenceReflected(final UpdateDefendantOffencesSteps offenceSteps, - final HearingsData existedHearingsData, - final String offenceId) { - verifyOrRepublishUntilReflected(LOGGER, "list-next-hearing-fix", "offences-for-defendant-changed for offence " + offenceId, - offenceSteps::whenCaseDefendantOffencesUpdatedPublicEventIsPublishedAddedOnly, - () -> verifyOffenceAddedToAllocatedHearingFromApi(existedHearingsData, offenceId)); - } - public void verifyCasesAreInAllocatedHearingFromApi(HearingsData existedHearingsData, final HearingsData hearingsData) { final String searchHearingUrl = String.format("%s/%s", getBaseUri(), format(readConfig().getProperty("listing.range.search.hearings"), existedHearingsData.getHearingData().get(0).getCourtCentreId(), true)); @@ -534,12 +501,8 @@ public void verifyOldHearingDeleted(final HearingsData hearingsData) { } public void verifyPublicUnallocatedOldHearingDeletedInPublicMQ(final HearingsData hearingsData) { - final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerUnallocatedHearingDeleted, - anyOf(containsString(hearingsData.getHearingData().get(0).getId().toString()), - containsString(hearingsData.getHearingData().get(1).getId().toString()))); - final JsonPath jsonResponse2 = QueueUtil.retrieveMessage(publicMessageConsumerUnallocatedHearingDeleted, - anyOf(containsString(hearingsData.getHearingData().get(0).getId().toString()), - containsString(hearingsData.getHearingData().get(1).getId().toString()))); + final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerUnallocatedHearingDeleted); + final JsonPath jsonResponse2 = QueueUtil.retrieveMessage(publicMessageConsumerUnallocatedHearingDeleted); final List actualHearingIds = Arrays.asList(jsonResponse.get("hearingId"), jsonResponse2.get("hearingId")); final List expectedHearingIds = hearingsData.getHearingData().stream() @@ -550,8 +513,7 @@ public void verifyPublicUnallocatedOldHearingDeletedInPublicMQ(final HearingsDat } public void verifyPublicOffencesRemovedFromExistingHearingInActiveMQ(final UUID existedHearingId, final HearingsData hearingsData) { - final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerOffencesRemovedFromExistingHearing, - containsString(existedHearingId.toString())); + final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerOffencesRemovedFromExistingHearing); hearingsData.getHearingData().get(0).getListedCases().stream() .flatMap(listedCaseData -> listedCaseData.getDefendants().stream()) @@ -564,21 +526,16 @@ public void verifyPublicOffencesRemovedFromExistingHearingInActiveMQ(final UUID } public void verifyPublicHearingAddedToCaseInActiveMQ(final UUID existedHearingId) { - final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerHearingAddedToCase, - containsString(existedHearingId.toString())); + final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerHearingAddedToCase); assertThat(jsonResponse.get("hearingId"), is(existedHearingId.toString())); } public void verifyPublicOffencesMovedToHearingInActiveMQ(final HearingsData hearingsData, final HearingsData oldHearingsData, final UUID seedingHearingId) { - final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerOffencesMovedToHearing, - anyOf(containsString(hearingsData.getHearingData().get(0).getId().toString()), - containsString(hearingsData.getHearingData().get(1).getId().toString()))); + final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerOffencesMovedToHearing); - final JsonPath jsonResponse2 = QueueUtil.retrieveMessage(publicMessageConsumerOffencesMovedToHearing, - anyOf(containsString(hearingsData.getHearingData().get(0).getId().toString()), - containsString(hearingsData.getHearingData().get(1).getId().toString()))); + final JsonPath jsonResponse2 = QueueUtil.retrieveMessage(publicMessageConsumerOffencesMovedToHearing); assertThat(jsonResponse.get("seedingHearingId"), is(seedingHearingId.toString())); @@ -605,8 +562,7 @@ public void verifyPublicOffencesMovedToHearingInActiveMQ() { } public void verifyPublicOffencesRemovedFromExistingAllocatedHearingInActiveMQ(final UUID existedHearingId, final HearingsData hearingsData, final String... newOffence) { - final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerOffencesRemovedFromExistingAllocatedHearing, - containsString(existedHearingId.toString())); + final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerOffencesRemovedFromExistingAllocatedHearing); final List offenceIds = hearingsData.getHearingData().get(0).getListedCases().stream() .flatMap(listedCaseData -> listedCaseData.getDefendants().stream()) @@ -663,23 +619,36 @@ private void verifyHearingListedFromAPIWithJmsDelay(final HearingData hearingDat } public void verifyAllocatedHearingListedFromAPI(final HearingData hearingData) { - final String hearingId = hearingData.getId().toString(); - // Match the hearing by id at ANY position. This scenario lists three hearings in one court centre, - // so a positional hearings[0]/[1] matcher races under load when the target lands at index >= 2 - // (manifested as a 90s RestPoller ConditionTimeout). Mirrors verifyHearingListedFromAPI. pollForHearing(hearingData.getCourtCentreId().toString(), true, getLoggedInUser().toString(), new Matcher[]{ - withJsonPath("$.hearings[?(@.id == '" + hearingId + "')].jurisdictionType", - contains(hearingData.getJurisdictionType())), - withJsonPath("$.hearings[?(@.id == '" + hearingId + "')].courtCentreId", - contains(hearingData.getCourtCentreId().toString())), - withJsonPath("$.hearings[?(@.id == '" + hearingId + "')].type.id", - contains(hearingData.getHearingTypeData().getTypeId().toString())), - withJsonPath("$.hearings[?(@.id == '" + hearingId + "')].type.description", - contains(hearingData.getHearingTypeData().getTypeDescription())), - withJsonPath("$.hearings[?(@.id == '" + hearingId + "')].startDate", - contains(hearingData.getHearingStartDate().toString())), - withJsonPath("$.hearings[?(@.id == '" + hearingId + "')].listedCases[0].defendants[0].isYouth", - contains(true)) + anyOf(allOf( + withJsonPath("$.hearings[0].id", + equalTo(hearingData.getId().toString())), + withJsonPath("$.hearings[0].jurisdictionType", + equalTo(hearingData.getJurisdictionType())), + withJsonPath("$.hearings[0].courtCentreId", + equalTo(hearingData.getCourtCentreId().toString())), + withJsonPath("$.hearings[0].type.id", + equalTo(hearingData.getHearingTypeData().getTypeId().toString())), + withJsonPath("$.hearings[0].type.description", + equalTo(hearingData.getHearingTypeData().getTypeDescription())), + withJsonPath("$.hearings[0].startDate", + equalTo(hearingData.getHearingStartDate().toString())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].isYouth", + equalTo(true))), + allOf(withJsonPath("$.hearings[1].id", + equalTo(hearingData.getId().toString())), + withJsonPath("$.hearings[1].jurisdictionType", + equalTo(hearingData.getJurisdictionType())), + withJsonPath("$.hearings[1].courtCentreId", + equalTo(hearingData.getCourtCentreId().toString())), + withJsonPath("$.hearings[1].type.id", + equalTo(hearingData.getHearingTypeData().getTypeId().toString())), + withJsonPath("$.hearings[1].type.description", + equalTo(hearingData.getHearingTypeData().getTypeDescription())), + withJsonPath("$.hearings[1].startDate", + equalTo(hearingData.getHearingStartDate().toString())), + withJsonPath("$.hearings[1].listedCases[0].defendants[0].isYouth", + equalTo(true)))) }); } @@ -734,42 +703,14 @@ public void verifyHearingUnscheduledListedFromAPI(final HearingData hearingData) pollForUnscheduledHearings(getLoggedInUser(), courtCentreId, unscheduledHearingVerifiedMatcher); } - /** - * For a CROWN hearing the {@code bookingReference} IS the courtScheduleId. The listing command resolves - * it against courtscheduler ({@code search.court-schedules-by-id}) then lists it. Stub both so the - * bookingReference resolves to a session echoing this hearing's own centre/room. No-op for MAGISTRATES - * or hearings without a court centre. - */ - private static void stubCrownBookingReferenceResolution(final HearingData hearingData, final UUID bookingReference) { - if (bookingReference == null - || hearingData.getCourtCentreId() == null - || !"CROWN".equals(hearingData.getJurisdictionType())) { - return; - } - final ZonedDateTime startTime = hearingData.getHearingStartTime() != null - ? hearingData.getHearingStartTime() - : ItClock.nowUtc(); - final LocalDate sessionDate = hearingData.getHearingStartDate() != null - ? hearingData.getHearingStartDate() - : startTime.toLocalDate(); - stubSearchCourtSchedulesByIdSession( - bookingReference.toString(), hearingData.getCourtCentreId(), hearingData.getCourtRoomId(), - sessionDate, startTime, false); - stubListHearingInCourtSessionsForCourtSchedule(hearingData.getId().toString(), bookingReference.toString(), startTime); - } - private HearingListingNeeds buildHearingListingNeeds(final HearingData hearingData) { - // CROWN treats the bookingReference as the courtScheduleId; the command resolves it via - // search.court-schedules-by-id. Stub that resolution to echo this hearing's own centre/room. - final UUID bookingReference = randomUUID(); - stubCrownBookingReferenceResolution(hearingData, bookingReference); return HearingListingNeeds.hearingListingNeeds() .withCourtCentre(CourtCentre.courtCentre() .withId(hearingData.getCourtCentreId()) .withName(hearingData.getName()) .withRoomId(hearingData.getCourtRoomId()) .build()) - .withBookingReference(bookingReference) + .withBookingReference(randomUUID()) .withId(hearingData.getId()) .withEarliestStartDateTime(hearingData.getHearingStartTime() != null ? hearingData.getHearingStartTime() : null) .withEndDate(hearingData.getHearingEndDate() != null ? hearingData.getHearingEndDate().toString() : null) @@ -871,12 +812,12 @@ private final List buildApplication(final HearingData hearingD .withOffenceCode(STRING.next()) .withOffenceTitle(STRING.next()) .withWording(STRING.next()) - .withStartDate(ItClock.today().toString()) + .withStartDate(LocalDate.now().toString()) .build())) .build())) .withParentApplicationId(hearingData.getCourtApplications().get(0).getParentApplicationId()) .withType(getCourtApplicationType(hearingData, LinkType.LINKED, Jurisdiction.CROWN)) - .withApplicationReceivedDate(ItClock.today().toString()) + .withApplicationReceivedDate(LocalDate.now().toString()) .withApplicationReference(STRING.next()) .withApplicationParticulars(hearingData.getCourtApplications().get(0).getApplicationParticulars()) .withApplicationStatus(ApplicationStatus.LISTED) @@ -937,7 +878,7 @@ private List buildProsecutionCases(final HearingData hearingDat .withDefendants(lc.getDefendants().stream().map(d -> Defendant.defendant() .withId(d.getDefendantId()) .withMasterDefendantId(d.getMasterDefendantId()) - .withCourtProceedingsInitiated(ItClock.nowUtc()) + .withCourtProceedingsInitiated(ZonedDateTime.now()) .withIsYouth(d.getIsYouth()) .withPersonDefendant(buildPersonDefendant(d)) .withAssociatedPersons(singletonList(AssociatedPerson.associatedPerson() @@ -951,14 +892,14 @@ private List buildProsecutionCases(final HearingData hearingDat .withOffenceCode(STRING.next()) .withOffenceDefinitionId(randomUUID()) .withWording(STRING.next()) - .withStartDate(ItClock.today().toString()) + .withStartDate(LocalDate.now().toString()) .withOrderIndex(INTEGER.next()) .withOffenceTitle(o.getStatementOfOffenceTitle()) .withLaaApplnReference( LaaReference.laaReference() .withApplicationReference(STRING.next()) .withStatusCode(STRING.next()) - .withStatusDate((format(ItClock.today().toString()))) + .withStatusDate((format(LocalDate.now().toString()))) .withStatusDescription(STRING.next()) .withStatusId(randomUUID()).build()) .withSeedingHearing(SeedingHearing.seedingHearing() @@ -969,7 +910,7 @@ private List buildProsecutionCases(final HearingData hearingDat .withReportingRestrictions(Arrays.asList(ReportingRestriction.reportingRestriction().withId(randomUUID()) .withLabel("RestrictionApplied") .withJudicialResultId(JUDICIAL_RESULT_ID) - .withOrderedDate(ItClock.today().toString()).build())) + .withOrderedDate(LocalDate.now().toString()).build())) .build()) .collect(Collectors.toList())) .withProsecutionCaseId(listedCaseData.getCaseId()) @@ -1024,7 +965,7 @@ private PersonDefendant buildPersonDefendant(final DefendantData d) { .withObservedEthnicityId(randomUUID()) .withObservedEthnicityDescription(STRING.next()) .build()) - .withDateOfBirth(ItClock.today().minusYears(21).toString()) + .withDateOfBirth(LocalDate.now().minusYears(21).toString()) .build()) .build(); } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListUnAllocatedCourtHearingSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListUnAllocatedCourtHearingSteps.java index 64961df4a..d2074ec73 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListUnAllocatedCourtHearingSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListUnAllocatedCourtHearingSteps.java @@ -53,7 +53,6 @@ import uk.gov.moj.cpp.listing.steps.data.HearingData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; import uk.gov.moj.cpp.listing.steps.data.ListedCaseData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.time.ZonedDateTime; @@ -189,7 +188,7 @@ private List convertToListedCases(final HearingData hearingData .withDefendants(lc.getDefendants().stream().map(d -> Defendant.defendant() .withId(d.getDefendantId()) .withMasterDefendantId(d.getMasterDefendantId()) - .withCourtProceedingsInitiated(ItClock.nowUtc()) + .withCourtProceedingsInitiated(ZonedDateTime.now()) .withIsYouth(d.getIsYouth()) .withPersonDefendant(PersonDefendant.personDefendant() .withBailStatus(new BailStatus.Builder().withCode(d.getBailStatus().getCode()).withDescription(d.getBailStatus().getDescription()).withId(d.getBailStatus().getId()).build()) @@ -213,7 +212,7 @@ private List convertToListedCases(final HearingData hearingData .withObservedEthnicityId(randomUUID()) .withObservedEthnicityDescription(STRING.next()) .build()) - .withDateOfBirth(ItClock.today().minusYears(21).toString()) + .withDateOfBirth(LocalDate.now().minusYears(21).toString()) .build()) .build()) .withAssociatedPersons(asList(AssociatedPerson.associatedPerson() @@ -237,14 +236,14 @@ private List convertToListedCases(final HearingData hearingData .withOffenceCode(STRING.next()) .withOffenceDefinitionId(randomUUID()) .withWording(STRING.next()) - .withStartDate(ItClock.today().toString()) + .withStartDate(LocalDate.now().toString()) .withOrderIndex(INTEGER.next()) .withOffenceTitle(o.getStatementOfOffenceTitle()) .withLaaApplnReference( LaaReference.laaReference() .withApplicationReference(STRING.next()) .withStatusCode(STRING.next()) - .withStatusDate((format(ItClock.today().toString()))) + .withStatusDate((format(LocalDate.now().toString()))) .withStatusDescription(STRING.next()) .withStatusId(randomUUID()).build()) .build()) @@ -297,7 +296,7 @@ private List getCourtApplications(final HearingData hearingDat .withSpiOutApplicableFlag(false) .withOffenceActiveOrder(OffenceActiveOrder.COURT_ORDER) .build()) - .withApplicationReceivedDate(ItClock.today().toString()) + .withApplicationReceivedDate(LocalDate.now().toString()) .withApplicationReference(STRING.next()) .withApplicationStatus(ApplicationStatus.LISTED) .withApplicant(applicant) @@ -362,7 +361,7 @@ private ListUnscheduledCourtHearing getListCourtHearingDataStandaloneApplication .withSpiOutApplicableFlag(false) .withOffenceActiveOrder(OffenceActiveOrder.COURT_ORDER) .build()) - .withApplicationReceivedDate(ItClock.today().toString()) + .withApplicationReceivedDate(LocalDate.now().toString()) .withApplicationReference(STRING.next()) .withApplicationStatus(ApplicationStatus.DRAFT) .withApplicant(applicant) diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListUnscheduledCourtHearingSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListUnscheduledCourtHearingSteps.java index b1c5b1618..50346d46e 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListUnscheduledCourtHearingSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/ListUnscheduledCourtHearingSteps.java @@ -53,7 +53,6 @@ import uk.gov.moj.cpp.listing.steps.data.HearingData; import uk.gov.moj.cpp.listing.steps.data.HearingsData; import uk.gov.moj.cpp.listing.steps.data.ListedCaseData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.time.ZonedDateTime; @@ -223,7 +222,7 @@ private List convertToListedCases(final HearingData hearingData .withDefendants(lc.getDefendants().stream().map(d -> Defendant.defendant() .withId(d.getDefendantId()) .withMasterDefendantId(d.getMasterDefendantId()) - .withCourtProceedingsInitiated(ItClock.nowUtc()) + .withCourtProceedingsInitiated(ZonedDateTime.now()) .withIsYouth(d.getIsYouth()) .withPersonDefendant(PersonDefendant.personDefendant() .withBailStatus(new BailStatus.Builder().withCode(d.getBailStatus().getCode()).withDescription(d.getBailStatus().getDescription()).withId(d.getBailStatus().getId()).build()) @@ -247,7 +246,7 @@ private List convertToListedCases(final HearingData hearingData .withObservedEthnicityId(randomUUID()) .withObservedEthnicityDescription(STRING.next()) .build()) - .withDateOfBirth(ItClock.today().minusYears(21).toString()) + .withDateOfBirth(LocalDate.now().minusYears(21).toString()) .build()) .build()) .withAssociatedPersons(asList(AssociatedPerson.associatedPerson() @@ -271,14 +270,14 @@ private List convertToListedCases(final HearingData hearingData .withOffenceCode(STRING.next()) .withOffenceDefinitionId(randomUUID()) .withWording(STRING.next()) - .withStartDate(ItClock.today().toString()) + .withStartDate(LocalDate.now().toString()) .withOrderIndex(INTEGER.next()) .withOffenceTitle(o.getStatementOfOffenceTitle()) .withLaaApplnReference( LaaReference.laaReference() .withApplicationReference(STRING.next()) .withStatusCode(STRING.next()) - .withStatusDate((format(ItClock.today().toString()))) + .withStatusDate((format(LocalDate.now().toString()))) .withStatusDescription(STRING.next()) .withStatusId(randomUUID()).build()) .build()) @@ -347,7 +346,7 @@ private List getCourtApplications(final HearingData hearingDat .withSpiOutApplicableFlag(false) .withOffenceActiveOrder(OffenceActiveOrder.COURT_ORDER) .build()) - .withApplicationReceivedDate(ItClock.today().toString()) + .withApplicationReceivedDate(LocalDate.now().toString()) .withApplicationReference(STRING.next()) .withApplicationStatus(ApplicationStatus.LISTED) .withApplicant(applicant) @@ -392,7 +391,7 @@ private ListUnscheduledCourtHearing getListCourtHearingDataStandaloneApplication .withLinkType(LinkType.STANDALONE) .withJurisdiction(Jurisdiction.MAGISTRATES) .build()) - .withApplicationReceivedDate(ItClock.today().toString()) + .withApplicationReceivedDate(LocalDate.now().toString()) .withApplicationReference(STRING.next()) .withApplicationStatus(ApplicationStatus.DRAFT) .withApplicant(CourtApplicationParty.courtApplicationParty() diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PayloadBasedListCourtHearingSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PayloadBasedListCourtHearingSteps.java index 3113e7874..0651318ff 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PayloadBasedListCourtHearingSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PayloadBasedListCourtHearingSteps.java @@ -25,7 +25,6 @@ import uk.gov.moj.cpp.listing.it.AbstractIT; import uk.gov.moj.cpp.listing.steps.data.CourtCentreData; import uk.gov.moj.cpp.listing.utils.PropertyUtil; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.text.MessageFormat; import java.time.*; @@ -195,7 +194,7 @@ private void setupStubsForHearing(PayloadGenerator.PayloadValues values) { .withNano(0); } if (isNull(values.hearingDate)) { - values.hearingDate = ItClock.today().plusDays(30).toString(); + values.hearingDate = LocalDate.now().plusDays(30).toString(); } stubListHearingInCourtSessions(values.hearingId, values.courtScheduleId,values.hearingStartTime); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PayloadGenerator.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PayloadGenerator.java index 187d20a02..5121ddbd0 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PayloadGenerator.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PayloadGenerator.java @@ -1,7 +1,5 @@ package uk.gov.moj.cpp.listing.steps; -import uk.gov.moj.cpp.listing.it.util.ItClock; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.TextNode; @@ -53,19 +51,19 @@ public static JsonNode loadPayloadWithDynamicValues(String courtType, String sce try { String resourcePath = String.format("/test-data/%s/%s/%s.json", courtType, scenario, testCase); InputStream inputStream = PayloadGenerator.class.getResourceAsStream(resourcePath); - + if (inputStream == null) { throw new RuntimeException("Test data file not found: " + resourcePath); } - + JsonNode originalNode = objectMapper.readTree(inputStream); Map placeholderValues = generateDynamicValues(null); - + JsonNode processedNode = replacePlaceholders(originalNode, placeholderValues); - + LOGGER.info("Loaded and processed test data from: {}", resourcePath); return processedNode; - + } catch (IOException e) { throw new RuntimeException("Failed to load test data file", e); } @@ -93,7 +91,7 @@ private static Map generateDynamicValues(Map exc values.put("%%JURISDICTION_TYPE%%", "MAGISTRATES"); // Can be parameterized later // Generate dates and times - LocalDateTime now = ItClock.nowLocalDateTime(); + LocalDateTime now = LocalDateTime.now(); LocalDateTime futureDateTime = now.plusDays(30); LocalDate futureDate = futureDateTime.toLocalDate(); ZonedDateTime zonedDateTime = now @@ -136,22 +134,22 @@ public static JsonNode loadPayloadWithCustomValues(String courtType, String scen try { String resourcePath = String.format("/test-data/%s/%s/%s.json", courtType, scenario, testCase); InputStream inputStream = PayloadGenerator.class.getResourceAsStream(resourcePath); - + if (inputStream == null) { throw new RuntimeException("Test data file not found: " + resourcePath); } - + JsonNode originalNode = objectMapper.readTree(inputStream); - + // Start with default values and override with custom ones Map placeholderValues = generateDynamicValues(customValues); placeholderValues.putAll(customValues); - + JsonNode processedNode = replacePlaceholders(originalNode, placeholderValues); - + LOGGER.info("Loaded and processed test data from: {} with custom values", resourcePath); return processedNode; - + } catch (IOException e) { throw new RuntimeException("Failed to load test data file", e); } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PublishCourtListSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PublishCourtListSteps.java index 408867121..cb14a5e50 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PublishCourtListSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/PublishCourtListSteps.java @@ -11,10 +11,8 @@ import static org.custommonkey.xmlunit.XMLAssert.assertXpathEvaluatesTo; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.core.IsEqual.equalTo; @@ -48,7 +46,6 @@ import java.util.concurrent.ThreadPoolExecutor; import uk.gov.justice.services.messaging.JsonObjects; -import uk.gov.moj.cpp.listing.it.util.ItClock; import javax.json.JsonObject; import javax.json.JsonReader; import javax.ws.rs.core.Response; @@ -107,7 +104,7 @@ public void sendPublishCourtListCommand() { public void verifyCourtListPublishStatus(final String expectedPublishStatus, final String weekCommencing) { final String courtCentreId = commandJsonObject.getString("courtCentreId"); final String courtListType = commandJsonObject.getString("publishCourtListType"); - final String publishDate = ItClock.today().toString(); + final String publishDate = LocalDate.now().toString(); final String queryPart = format(readConfig().getProperty("listing.court.list.publish.status"), courtCentreId, courtListType, @@ -461,9 +458,7 @@ private static void createHearingForListing(final HearingsData hearingsData) { } public void verifyPublicEventForCourtListPublished(final String courtCentreId, final String publishCourtListType, final Boolean weekCommencing, final Boolean sendNotificationToParties, final int courtListItems) { - final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerPublishCourtList, - containsString(courtCentreId)); - assertNotNull(jsonResponse, "No public publish-court-list event found for courtCentreId=" + courtCentreId); + final JsonPath jsonResponse = QueueUtil.retrieveMessage(publicMessageConsumerPublishCourtList); LOGGER.info("jsonResponse from publicMessageConsumerHearingUpdated: {}", jsonResponse.prettify()); LOGGER.info("jsonResponse from publicMessageConsumerHearingUpdated "); assertThat(jsonResponse.get("courtCentreId"), is(courtCentreId)); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/RemoveOffencesFromHearingSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/RemoveOffencesFromHearingSteps.java index 43f3e7f0e..7a74546b0 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/RemoveOffencesFromHearingSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/RemoveOffencesFromHearingSteps.java @@ -49,8 +49,7 @@ public void whenRaisedOffencesRemovedPublicEvent(final String hearingId, final L } public void verifyPublicListingOffencesRemovedFromAllocatedHearing() { - final JsonPath jsonResponse = retrieveMessage(publicSelectedOffenceRemovedFromHearing, - org.hamcrest.CoreMatchers.containsString(hearingId)); + final JsonPath jsonResponse = retrieveMessage(publicSelectedOffenceRemovedFromHearing); assertThat(jsonResponse.get("hearingId"), is(hearingId)); assertThat(jsonResponse.get("isResultFlow"), is(false)); } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/RestrictCourtListSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/RestrictCourtListSteps.java index 8a914e82e..efff19932 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/RestrictCourtListSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/RestrictCourtListSteps.java @@ -3,24 +3,17 @@ import static com.jayway.jsonpath.Criteria.where; import static com.jayway.jsonpath.Filter.filter; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasNoJsonPath; -import static com.jayway.jsonpath.matchers.JsonPathMatchers.isJson; import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; import static java.text.MessageFormat.format; import static org.apache.http.HttpStatus.SC_ACCEPTED; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.core.IsEqual.equalTo; -import static org.hamcrest.core.IsNull.notNullValue; import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearing; import static uk.gov.moj.cpp.listing.utils.PropertyUtil.getBaseUri; import static uk.gov.moj.cpp.listing.utils.PropertyUtil.readConfig; -import static uk.gov.moj.cpp.listing.utils.QueueUtil.publicEvents; -import static uk.gov.moj.cpp.listing.utils.QueueUtil.retrieveMessage; import uk.gov.justice.services.common.converter.ObjectToJsonValueConverter; import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer; -import uk.gov.justice.services.integrationtest.utils.jms.JmsMessageConsumerClient; import uk.gov.moj.cpp.listing.it.AbstractIT; import uk.gov.moj.cpp.listing.steps.data.CourtApplicationData; import uk.gov.moj.cpp.listing.steps.data.HearingData; @@ -35,19 +28,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.Filter; -import io.restassured.path.json.JsonPath; import org.hamcrest.Matcher; public class RestrictCourtListSteps extends AbstractIT { private static final String LISTING_COMMAND_RESTRICT_COURT_LIST = "listing.command.restrict-court-list"; private static final String MEDIA_TYPE_RESTRICT_COURT_LIST = "application/vnd.listing.command.restrict-court-list+json"; - private static final String PUBLIC_EVENT_COURT_LIST_RESTRICTED = "public.listing.court-list-restricted"; private String request; private final HearingsData hearingsData; - private final JmsMessageConsumerClient publicMessageConsumerCourtListRestricted; ObjectMapper objectMapper = new ObjectMapperProducer().objectMapper(); @@ -55,7 +45,6 @@ public class RestrictCourtListSteps extends AbstractIT { public RestrictCourtListSteps(HearingsData hearingsData) { this.hearingsData = hearingsData; - this.publicMessageConsumerCourtListRestricted = publicEvents.createPublicConsumer(PUBLIC_EVENT_COURT_LIST_RESTRICTED); givenAUserHasLoggedInAsAListingOfficer(USER_ID_VALUE); } @@ -209,104 +198,5 @@ public RestrictCourtListData getCourtApplicationTypeToBeRestricted(HearingsData .build(); } - public RestrictCourtListData getCourtApplicationRespondentDataToBeRestricted(HearingsData hearingsData) { - HearingData hearingData = hearingsData.getHearingData().get(0); - CourtApplicationData courtApplicationData = hearingsData.getHearingData().get(0).getCourtApplications().get(0); - return RestrictCourtListData.restrictCourtList() - .withCourtApplicatonRespondentIds(Arrays.asList(courtApplicationData.getRespondent().getId())) - .withHearingId(hearingData.getId()) - .withRestrictCourtList(true) - .build(); - } - - public RestrictCourtListData getCourtApplicationDataToBeUnrestricted(HearingsData hearingsData) { - HearingData hearingData = hearingsData.getHearingData().get(0); - CourtApplicationData courtApplicationData = hearingsData.getHearingData().get(0).getCourtApplications().get(0); - return RestrictCourtListData.restrictCourtList() - .withCourtApplicatonIds(Arrays.asList(courtApplicationData.getId())) - .withCourtApplicationApplicantIds(Arrays.asList(courtApplicationData.getApplicant().getId())) - .withHearingId(hearingData.getId()) - .withRestrictCourtList(false) - .build(); - } - - public RestrictCourtListData getCourtApplicationApplicantAndRespondentDataToBeRestricted(HearingsData hearingsData) { - HearingData hearingData = hearingsData.getHearingData().get(0); - CourtApplicationData courtApplicationData = hearingsData.getHearingData().get(0).getCourtApplications().get(0); - return RestrictCourtListData.restrictCourtList() - .withCourtApplicatonIds(Arrays.asList(courtApplicationData.getId())) - .withCourtApplicationApplicantIds(Arrays.asList(courtApplicationData.getApplicant().getId())) - .withCourtApplicatonRespondentIds(Arrays.asList(courtApplicationData.getRespondent().getId())) - .withHearingId(hearingData.getId()) - .withRestrictCourtList(true) - .build(); - } - - public RestrictCourtListData getCourtApplicationSubjectDataToBeRestricted(HearingsData hearingsData) { - HearingData hearingData = hearingsData.getHearingData().get(0); - CourtApplicationData courtApplicationData = hearingsData.getHearingData().get(0).getCourtApplications().get(0); - return RestrictCourtListData.restrictCourtList() - .withCourtApplicationSubjectIds(Arrays.asList(courtApplicationData.getSubject().getId())) - .withHearingId(hearingData.getId()) - .withRestrictCourtList(true) - .build(); - } - - public void verifyPublicCourtListRestrictedEvent(final Boolean restrictCourtList) { - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerCourtListRestricted, - isJson(allOf(withJsonPath("$.restrictCourtList", equalTo(restrictCourtList)), - withJsonPath("$.hearingId", equalTo(hearingsData.getHearingData().get(0).getId().toString()))))); - assertThat(jsonResponse, notNullValue()); - assertThat(jsonResponse.get("hearingId"), is(hearingsData.getHearingData().get(0).getId().toString())); - } - - public void verifyPublicCourtListRestrictedEventWithApplicant(final Boolean restrictCourtList) { - final CourtApplicationData courtApplication = hearingsData.getHearingData().get(0).getCourtApplications().get(0); - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerCourtListRestricted, - isJson(allOf(withJsonPath("$.restrictCourtList", equalTo(restrictCourtList)), - withJsonPath("$.hearingId", equalTo(hearingsData.getHearingData().get(0).getId().toString()))))); - assertThat(jsonResponse, notNullValue()); - assertThat(jsonResponse.get("hearingId"), is(hearingsData.getHearingData().get(0).getId().toString())); - assertThat(jsonResponse.get("courtApplicationApplicantIds[0]"), is(courtApplication.getApplicant().getId().toString())); - } - - public void verifyPublicCourtListRestrictedEventWithApplicantAndRespondent(final Boolean restrictCourtList) { - final CourtApplicationData courtApplication = hearingsData.getHearingData().get(0).getCourtApplications().get(0); - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerCourtListRestricted, - isJson(allOf(withJsonPath("$.restrictCourtList", equalTo(restrictCourtList)), - withJsonPath("$.hearingId", equalTo(hearingsData.getHearingData().get(0).getId().toString()))))); - assertThat(jsonResponse, notNullValue()); - assertThat(jsonResponse.get("hearingId"), is(hearingsData.getHearingData().get(0).getId().toString())); - assertThat(jsonResponse.get("courtApplicationApplicantIds[0]"), is(courtApplication.getApplicant().getId().toString())); - assertThat(jsonResponse.get("courtApplicationRespondentIds[0]"), is(courtApplication.getRespondent().getId().toString())); - } - - public void verifyPublicCourtListRestrictedEventWithSubject(final Boolean restrictCourtList) { - final CourtApplicationData courtApplication = hearingsData.getHearingData().get(0).getCourtApplications().get(0); - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerCourtListRestricted, - isJson(allOf(withJsonPath("$.restrictCourtList", equalTo(restrictCourtList)), - withJsonPath("$.hearingId", equalTo(hearingsData.getHearingData().get(0).getId().toString()))))); - assertThat(jsonResponse, notNullValue()); - assertThat(jsonResponse.get("hearingId"), is(hearingsData.getHearingData().get(0).getId().toString())); - assertThat(jsonResponse.get("courtApplicationSubjectIds[0]"), is(courtApplication.getSubject().getId().toString())); - } - - public void verifyCourtApplicationSubjectListingRestrictedInHearing(final Boolean restrictCourtListingOfSubject) { - final Filter idFilter = filter(where("id").is(hearingsData.getHearingData().get(0).getId().toString())); - final com.jayway.jsonpath.JsonPath hearingIdFilter = com.jayway.jsonpath.JsonPath.compile("$.hearings[?]", idFilter); - - pollForHearing(hearingsData.getHearingData().get(0).getCourtCentreId().toString(), false, getLoggedInUser().toString(), new Matcher[]{ - withJsonPath(hearingIdFilter), - withJsonPath("$.hearings[0].id", - equalTo(hearingsData.getHearingData().get(0).getId().toString())), - withJsonPath("$.hearings[0].courtApplications[0].id", - equalTo(hearingsData.getHearingData().get(0).getCourtApplications().get(0).getId().toString())), - withJsonPath("$.hearings[0].courtApplications[0].subject.id", - equalTo(hearingsData.getHearingData().get(0).getCourtApplications().get(0).getSubject().getId().toString())), - withJsonPath("$.hearings[0].courtApplications[0].subject.restrictFromCourtList", - equalTo(restrictCourtListingOfSubject)) - }); - } - } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/SequenceHearingSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/SequenceHearingSteps.java index 3bfe32518..d41ca0277 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/SequenceHearingSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/SequenceHearingSteps.java @@ -9,9 +9,7 @@ import static uk.gov.justice.services.messaging.JsonObjects.createObjectBuilder; import static javax.ws.rs.core.Response.Status.OK; import static org.hamcrest.CoreMatchers.allOf; -import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsEqual.equalTo; import static uk.gov.justice.services.common.converter.ZonedDateTimes.fromString; @@ -107,9 +105,7 @@ public void verifyHearingDaysAreSequencedFromAPI() { } public void verifyPublicEventHearingUpdated() { - final String expectedHearingId = sequenceHearingData.getHearingId().toString(); - JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingUpdated, containsString(expectedHearingId)); - assertNotNull(jsonResponse, "No public hearing-updated event found for hearingId=" + expectedHearingId); + JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingUpdated); String startDate = sequenceHearingData.getUpdatedHearingData().getStartDate(); String endDate = sequenceHearingData.getUpdatedHearingData().getEndDate(); Integer sequence = sequenceHearingData.getSequencedDays().get(parse(startDate)); @@ -175,9 +171,7 @@ public void verifyHearingDaysAreSequencedForHearing(final UUID courtCentreId, fi } public void verifyHearingDaySequencedPublicEvent() { - final String expectedHearingId = sequenceHearingData.getHearingId().toString(); - JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingSequenced, containsString(expectedHearingId)); - assertNotNull(jsonResponse, "No public hearing-sequenced event found for hearingId=" + expectedHearingId); + JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingSequenced); LOGGER.info("jsonResponse from privateMessageConsumerHearingDaysSequenced: {}", jsonResponse.prettify()); assertThat(jsonResponse.get("hearingId"), is(sequenceHearingData.getHearingId().toString())); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateCaseMarkersSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateCaseMarkersSteps.java index 6fca86652..1fb27d70d 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateCaseMarkersSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateCaseMarkersSteps.java @@ -6,29 +6,29 @@ import static uk.gov.justice.services.test.utils.core.messaging.MetadataBuilderFactory.metadataOf; import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearing; import static uk.gov.moj.cpp.listing.helper.SearchHearingHelper.pollForHearingWithJmsDelay; -import static uk.gov.moj.cpp.listing.it.util.PublishRetryHelper.publishUntilReflected; import static uk.gov.moj.cpp.listing.utils.QueueUtil.publicEvents; import static uk.gov.moj.cpp.listing.utils.QueueUtil.sendMessage; +import uk.gov.justice.core.courts.CaseMarkersUpdated; +import uk.gov.justice.core.courts.Marker; +import uk.gov.justice.services.common.converter.ObjectToJsonValueConverter; +import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer; import uk.gov.justice.services.integrationtest.utils.jms.JmsMessageProducerClient; import uk.gov.moj.cpp.listing.it.AbstractIT; import uk.gov.moj.cpp.listing.steps.data.CaseMarkerData; import uk.gov.moj.cpp.listing.steps.data.HearingData; +import java.util.Collections; +import java.util.List; import java.util.UUID; -import javax.json.Json; -import javax.json.JsonArray; import javax.json.JsonObject; +import com.fasterxml.jackson.databind.ObjectMapper; import org.hamcrest.Matcher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class UpdateCaseMarkersSteps extends AbstractIT { - private static final Logger LOGGER = LoggerFactory.getLogger(UpdateCaseMarkersSteps.class); - private static final String PUBLIC_EVENT_CASE_PUBLIC_PROGRESSION_CASE_MARKERS_UPDATED = "public.progression.case-markers-updated"; private JmsMessageProducerClient publicEventCaseMarkersToBeUpdated; @@ -36,12 +36,18 @@ public class UpdateCaseMarkersSteps extends AbstractIT { private final HearingData hearingData; private final CaseMarkerData caseMarkerData; private final UUID caseId; + private final UUID metadataId; private final UUID userId; + ObjectMapper objectMapper = new ObjectMapperProducer().objectMapper(); + + ObjectToJsonValueConverter objectToJsonValueConverter = new ObjectToJsonValueConverter(objectMapper); + public UpdateCaseMarkersSteps(final UUID caseId, final HearingData hearingData, final CaseMarkerData caseMarkerData) { this.hearingData = hearingData; this.caseMarkerData = caseMarkerData; this.caseId = caseId; + metadataId = randomUUID(); userId = randomUUID(); publicEventCaseMarkersToBeUpdated = publicEvents.createPublicProducer(); @@ -50,62 +56,36 @@ public UpdateCaseMarkersSteps(final UUID caseId, final HearingData hearingData, } public void whenCaseMarkerUpdatedPublicEventIsPublished() { - final JsonObject caseMarkersUpdated = getCaseMarkerUpdate(caseId, hearingData.getId()); + final CaseMarkersUpdated caseMarkersUpdated = getCaseMarkerUpdate(caseId, hearingData.getId()); publishCaseMarkersUpdated(caseMarkersUpdated); } - /** - * Publishes the {@code public.progression.case-markers-updated} event and verifies the read model, - * RE-PUBLISHING until the update is reflected (or the attempt budget is exhausted). - * - *

Why re-publish instead of publish-once-then-poll? The event is consumed by - * {@code ListingEventProcessor} and routed to {@code listing.command.update-case-markers}, handled by - * the {@code Case} aggregate's {@code addedCaseMarkers(...)}. That method silently drops the update - * ({@code if (hearingIds.isEmpty()) return Stream.empty();}) when the aggregate does not yet know which - * hearing the case belongs to. {@code Case.hearingIds} is populated only after the asynchronous - * {@code add-hearing-to-case} command runs — itself triggered by a private event emitted after - * {@code list-court-hearing} — and {@code HearingAddedToCase} has no viewstore projection, so the - * test cannot deterministically await the link. On slower environments (the vld validation pipeline) the - * first publish can be processed before the link exists; it is then dropped with no JMS redelivery, so a - * single publish is lost forever and the 90s poll can never succeed. This was the root cause of - * CaseMarkerUpdateIT failing on vld (builds 651954 / 653051) while passing locally — a cross-aggregate - * eventual-consistency race against a deliberate, correct production guard (you cannot mark a case that is - * not yet linked to a hearing). Re-publishing (with a fresh event id each time) guarantees that once the - * link is established, a subsequent publish lands. - */ - public void publishUntilCaseMarkersReflected(final UUID caseIdToUpdateMarkers) { - publishUntilReflected(LOGGER, "case-marker-fix", "case-markers-updated for case " + caseIdToUpdateMarkers, - this::whenCaseMarkerUpdatedPublicEventIsPublished, - () -> verifyCaseMarkersUpdatedThroughAPIWithJmsDelay(caseIdToUpdateMarkers)); - } - - public JsonObject getCaseMarkerUpdate(final UUID caseId, final UUID hearingId) { - return Json.createObjectBuilder() - .add("prosecutionCaseId", caseId.toString()) - .add("hearingId", hearingId.toString()) - .add("caseMarkers", buildCaseMarkers()) + public CaseMarkersUpdated getCaseMarkerUpdate(final UUID caseId, final UUID hearingId) { + return CaseMarkersUpdated.caseMarkersUpdated() + .withCaseMarkers(buildCaseMarkers()) + .withProsecutionCaseId(caseId) + .withHearingId(hearingId) .build(); } - private JsonArray buildCaseMarkers() { - return Json.createArrayBuilder() - .add(Json.createObjectBuilder() - .add("id", caseMarkerData.getId().toString()) - .add("markerTypeid", caseMarkerData.getCaseMarkerTypeId().toString()) - .add("markerTypeCode", caseMarkerData.getCaseMarkerCode()) - .add("markerTypeDescription", caseMarkerData.getCaseMarkerDescription())) - .build(); + private List buildCaseMarkers() { + return Collections.singletonList(Marker.marker() + .withId(caseMarkerData.getId()) + .withMarkerTypeid(caseMarkerData.getCaseMarkerTypeId()) + .withMarkerTypeCode(caseMarkerData.getCaseMarkerCode()) + .withMarkerTypeDescription(caseMarkerData.getCaseMarkerDescription()) + .build()); } - private void publishCaseMarkersUpdated(final JsonObject caseMarkersUpdated) { - // Fresh event id per publish: the framework dedupes events by metadata id, so re-publishing - // with the same id would be ignored. A new id guarantees each re-publish is reprocessed. + private void publishCaseMarkersUpdated(final CaseMarkersUpdated caseMarkersUpdated) { + final JsonObject updateCaseMarkersObject = (JsonObject) objectToJsonValueConverter.convert(caseMarkersUpdated); + sendMessage( publicEventCaseMarkersToBeUpdated, PUBLIC_EVENT_CASE_PUBLIC_PROGRESSION_CASE_MARKERS_UPDATED, - caseMarkersUpdated, - metadataOf(randomUUID(), PUBLIC_EVENT_CASE_PUBLIC_PROGRESSION_CASE_MARKERS_UPDATED).withUserId(userId.toString()).build()); + updateCaseMarkersObject, + metadataOf(metadataId, PUBLIC_EVENT_CASE_PUBLIC_PROGRESSION_CASE_MARKERS_UPDATED).withUserId(userId.toString()).build()); } public void verifyCaseMarkersUpdatedThroughAPI(final UUID caseIdToUpdateMarkers) { diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateDefendantOffencesSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateDefendantOffencesSteps.java index 03bfb1bba..4ef2fbcd5 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateDefendantOffencesSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateDefendantOffencesSteps.java @@ -12,7 +12,6 @@ import static uk.gov.justice.core.courts.LaaReference.laaReference; import static uk.gov.justice.services.common.http.HeaderConstants.USER_ID; import static uk.gov.justice.services.test.utils.core.http.RequestParamsBuilder.requestParams; -import static uk.gov.moj.cpp.listing.it.util.PublishRetryHelper.publishUntilReflected; import static uk.gov.moj.cpp.listing.it.util.RestPollerHelper.pollWithDelayForJms; import static uk.gov.justice.services.test.utils.core.matchers.ResponsePayloadMatcher.payload; import static uk.gov.justice.services.test.utils.core.matchers.ResponseStatusMatcher.status; @@ -45,7 +44,6 @@ import uk.gov.moj.cpp.listing.steps.data.ReportingRestrictionData; import uk.gov.moj.cpp.listing.steps.data.UpdatedOffenceData; import uk.gov.moj.cpp.listing.utils.QueueUtil; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.util.ArrayList; @@ -56,7 +54,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.path.json.JsonPath; -import org.awaitility.core.ConditionTimeoutException; import org.skyscreamer.jsonassert.Customization; import org.skyscreamer.jsonassert.JSONCompareMode; import org.skyscreamer.jsonassert.comparator.CustomComparator; @@ -103,7 +100,7 @@ public class UpdateDefendantOffencesSteps extends AbstractIT { private final OffenceData offenceData; private final UUID offenceIdToBeDeleted; private final UUID caseId; - private UUID metadataId; + private final UUID metadataId; private final UUID userId; ObjectMapper objectMapper = new ObjectMapperProducer().objectMapper(); @@ -168,78 +165,9 @@ public void whenCaseDefendantOffencesUpdatedPublicEventIsPublishedDeletedOnly() publishCaseDefendantOffencesUpdated(offencesForDefendantUpdated); } - /** - * Publishes the combined {@code public.progression.defendant-offences-changed} event - * (update + add + delete) and RE-PUBLISHES until the update is reflected in the - * viewstore via REST, or the attempt budget is exhausted. - * - *

Why re-publish? The event is handled by the {@code Case} aggregate which - * silently drops the update ({@code hearingIds.isEmpty() → Stream.empty()}) when the - * case is not yet linked to a hearing. The async {@code add-hearing-to-case} command populates - * the link; on slow CI it may not complete before the first publish is processed, causing the - * event to be dropped with no JMS redelivery. Re-publishing (with a fresh metadata id each - * time) recovers once the link is established. - * - *

Why gate on REST, not on a JMS consume? The previous gate consumed - * {@code verifyPublicEventDefendantOffencesUpdatedInActiveMQ()}, which reads back the test's - * own published event off the public topic — an echo that is ALWAYS present on the first - * attempt, so the loop never actually re-published and the case<->hearing race was left - * unguarded. The downstream private-event verifies (called by the test after this gate) then - * died with {@code NoSuchElement} when the link had not yet formed. A REST poll is the true - * proof the aggregate applied the combined event, and — being a read — it does NOT consume - * the private JMS messages the test must subsequently assert. Once REST reflects the update, all - * downstream {@code offences-to-be-*} / {@code offence-*} events are guaranteed emitted and - * buffered on the consumers. This mirrors the proven gate used by the *Only variants. - * - *

The combined event's updated offence is built by the same {@code buildUpdatedOffences} / - * {@code buildOffence} as the {@code updatedOnly} variant, so {@link #verifyDefendentOffenceUpdatedOnlyFromAPI(boolean)} - * is a valid predicate here; the additional add (appended) and delete (offences[1]) do not shift offences[0]. - */ - public void publishUntilOffencesReflected(final boolean isAllocated) { - publishUntilReflected(LOGGER, "offences-fix", "combined defendant-offences-changed for case " + caseId, - this::whenCaseDefendantOffencesUpdatedPublicEventIsPublished, - () -> verifyDefendentOffenceUpdatedOnlyFromAPI(isAllocated)); - } - - /** - * Publishes the updated-only offences event and polls REST until the update is reflected, - * RE-PUBLISHING if the poll times out (case<->hearing link race — same root cause as - * {@link #publishUntilOffencesConsumed()}). - */ - public void publishUntilOffencesUpdatedOnlyReflected(final boolean isAllocated) { - publishUntilReflected(LOGGER, "offences-fix", "defendant-offences-changed (updatedOnly) for case " + caseId, - this::whenCaseDefendantOffencesUpdatedPublicEventIsPublishedUpdatedOnly, - () -> verifyDefendentOffenceUpdatedOnlyFromAPI(isAllocated)); - } - - /** - * Publishes the added-only offences event and polls REST until the addition is reflected, - * RE-PUBLISHING if the poll times out (case<->hearing link race — same root cause as - * {@link #publishUntilOffencesConsumed()}). - */ - public void publishUntilOffencesAddedOnlyReflected(final boolean isAllocated) { - publishUntilReflected(LOGGER, "offences-fix", "defendant-offences-changed (addedOnly) for case " + caseId, - this::whenCaseDefendantOffencesUpdatedPublicEventIsPublishedAddedOnly, - () -> verifyDefendentOffenceAddedOnlyFromAPI(isAllocated)); - } - - /** - * Publishes the deleted-only offences event and polls REST until the deletion is reflected, - * RE-PUBLISHING if the poll times out (case<->hearing link race — same root cause as - * {@link #publishUntilOffencesConsumed()}). - */ - public void publishUntilOffencesDeletedOnlyReflected(final boolean isAllocated) { - publishUntilReflected(LOGGER, "offences-fix", "defendant-offences-changed (deletedOnly) for case " + caseId, - this::whenCaseDefendantOffencesUpdatedPublicEventIsPublishedDeletedOnly, - () -> verifyDefendentOffenceDeletedOnlyFromAPI(isAllocated)); - } - private void publishCaseDefendantOffencesUpdated(OffencesForDefendantUpdated offencesForDefendantUpdated) { final JsonObject updateCaseDefendantDetailsObject = (JsonObject) objectToJsonValueConverter.convert(offencesForDefendantUpdated); - // Fresh event id per publish: the framework dedupes events by metadata id, so re-publishing - // with the same id would be ignored. A new id guarantees each re-publish is reprocessed. - metadataId = randomUUID(); sendMessage( publicEventDefendantOffencesUpdated, PUBLIC_EVENT_PROGRESSION_OFFENCES_FOR_DEFENDANT_CHANGED, @@ -301,7 +229,7 @@ public void verifyPublicEventDefendantOffencesUpdatedInActiveMQ() { " \"prosecutionCaseId\": \"" + caseId + "\"\n" + " }\n" + " ],\n" + - " \"modifiedDate\": \"" + ItClock.today() + "\",\n" + + " \"modifiedDate\": \"" + LocalDate.now() + "\",\n" + " \"updatedOffences\": [\n" + " {\n" + " \"defendantId\": \"" + defendantData.getDefendantId() + "\",\n" + @@ -755,7 +683,7 @@ private OffencesForDefendantUpdated getOffencesForDefendantUpdated(UUID caseId, return OffencesForDefendantUpdated.offencesForDefendantUpdated() .withAddedOffences(buildAddedOffences(caseId, defendantId)) .withDeletedOffences(buildDeletedOffence(caseId, defendantId, offenceIdToBeDeleted)) - .withModifiedDate(ItClock.today().toString()) + .withModifiedDate(LocalDate.now().toString()) .withUpdatedOffences(buildUpdatedOffences(caseId, defendantId, updatedOffenceData)) .build(); } @@ -763,7 +691,7 @@ private OffencesForDefendantUpdated getOffencesForDefendantUpdated(UUID caseId, private OffencesForDefendantUpdated getOffencesForDefendantUpdatedOnly(UUID caseId, UUID defendantId) { return OffencesForDefendantUpdated.offencesForDefendantUpdated() - .withModifiedDate(ItClock.today().toString()) + .withModifiedDate(LocalDate.now().toString()) .withUpdatedOffences(buildUpdatedOffences(caseId, defendantId, updatedOffenceData)) .build(); } @@ -772,7 +700,7 @@ private OffencesForDefendantUpdated getOffencesForDefendantAddedOnly(UUID caseId return OffencesForDefendantUpdated.offencesForDefendantUpdated() .withAddedOffences(buildAddedOffences(caseId, defendantId)) - .withModifiedDate(ItClock.today().toString()) + .withModifiedDate(LocalDate.now().toString()) .build(); } @@ -780,7 +708,7 @@ private OffencesForDefendantUpdated getOffencesForDefendantDeletedOnly(UUID case return OffencesForDefendantUpdated.offencesForDefendantUpdated() .withDeletedOffences(buildDeletedOffence(caseId, defendantId, offenceIdToBeDeleted)) - .withModifiedDate(ItClock.today().toString()) + .withModifiedDate(LocalDate.now().toString()) .build(); } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateDefendantSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateDefendantSteps.java index 03759b5fb..3f0f4bfed 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateDefendantSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateDefendantSteps.java @@ -4,7 +4,6 @@ import static com.jayway.jsonpath.Filter.filter; import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; import static java.util.UUID.randomUUID; -import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.core.IsEqual.equalTo; import static uk.gov.justice.core.courts.Organisation.organisation; import static uk.gov.justice.services.test.utils.core.messaging.MetadataBuilderFactory.metadataOf; @@ -29,8 +28,6 @@ import uk.gov.moj.cpp.listing.steps.data.UpdateCaseDefendantData; import uk.gov.moj.cpp.listing.steps.data.UpdatedDefendantData; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; import javax.json.JsonObject; @@ -64,11 +61,7 @@ public UpdateDefendantSteps(final UUID caseId, final HearingData hearingData, fi } public void whenPublicEventProgressionCaseDefendantsUpdatedIsPublished() { - whenPublicEventProgressionCaseDefendantChangedIsPublished(updatedDefendantData); - } - - public void whenPublicEventProgressionCaseDefendantChangedIsPublished(final UpdatedDefendantData defendantData) { - final UpdateCaseDefendantData updateCaseDefendantDetails = getUpdateCaseDefendantDetails(caseId, defendantData); + final UpdateCaseDefendantData updateCaseDefendantDetails = getUpdateCaseDefendantDetails(caseId, updatedDefendantData); final JsonObject updateCaseDefendantDetailsObject = (JsonObject) objectToJsonValueConverter.convert(updateCaseDefendantDetails); sendMessage( @@ -78,27 +71,6 @@ public void whenPublicEventProgressionCaseDefendantChangedIsPublished(final Upda metadataOf(randomUUID(), PUBLIC_EVENT_SELECTOR_PROGRESSION_CASE_DEFENDANT_CHANGED).withUserId(randomUUID().toString()).build()); } - /** - * Polls the hearing view until the first listed defendant's {@code isYouth} flag matches the expected value. - */ - /** - * Polls until the target defendant (by hearing, case and defendant id) is present with the expected {@code isYouth} value. - * Uses a defendant-object filter so missing {@code isYouth} on the payload does not false-pass. - */ - @SuppressWarnings("rawtypes") - public void verifyListedDefendantIsYouthWithJmsDelay(final boolean isAllocated, final boolean expectedIsYouth) { - pollForHearingWithJmsDelay(hearingData.getCourtCentreId().toString(), isAllocated, getLoggedInUser().toString(), new Matcher[]{ - withJsonPath(getJsonPathQueryForDefendantWithIsYouth(expectedIsYouth)) - }); - } - - /** - * Verifies defendant field updates (name, id, etc.) without asserting {@code isYouth}. - */ - public void verifyDefendantDetailsUpdatedWithJmsDelay(final boolean isAllocated) { - verifyHearingListedFromAPIWithJmsDelay(isAllocated, null); - } - public void verifyHearingListedFromAPI(final boolean isAllocated) { final com.jayway.jsonpath.JsonPath lastNameFilter = getJsonPathQueryForDefendantLastName(hearingData, listedCaseData, updatedDefendantData, updatedDefendantData.getLastName()); @@ -120,18 +92,28 @@ public void verifyHearingListedFromAPI(final boolean isAllocated) { equalTo(hearingData.getHearingTypeData().getTypeDescription())), withJsonPath("$.hearings[0].startDate", equalTo(hearingData.getHearingStartDate().toString())), - withJsonPath(getJsonPathQueryForDefendantAttribute("id"), - hasItem(equalTo(updatedDefendantData.getDefendantId().toString()))), - withJsonPath(getJsonPathQueryForDefendantAttribute("masterDefendantId"), - hasItem(equalTo(updatedDefendantData.getMasterDefendantId().toString()))), - withJsonPath(getJsonPathQueryForDefendantAttribute("bailStatus.code"), - hasItem(equalTo(updatedDefendantData.getBailStatus().getCode()))), - withJsonPath(getJsonPathQueryForDefendantAttribute("bailStatus.id"), - hasItem(equalTo(updatedDefendantData.getBailStatus().getId().toString()))), - withJsonPath(getJsonPathQueryForDefendantAttribute("firstName"), - hasItem(equalTo(updatedDefendantData.getFirstName()))), - withJsonPath(getJsonPathQueryForDefendantAttribute("restrictFromCourtList"), - hasItem(equalTo(hearingData.getListedCases().get(0).getDefendants().get(0).getRestrictFromCourtList()))) + withJsonPath("$.hearings[0].listedCases[0].defendants[0].id", + equalTo(updatedDefendantData.getDefendantId().toString())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].masterDefendantId", + equalTo(updatedDefendantData.getMasterDefendantId().toString())), +// withJsonPath("$.hearings[0].listedCases[0].defendants[0].custodyTimeLimit", +// equalTo(updatedDefendantData.getCustodyTimeLimit())), +// withJsonPath("$.hearings[0].listedCases[0].defendants[0].dateOfBirth", +// equalTo(updatedDefendantData.getDateOfBirth())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].bailStatus.code", + equalTo(updatedDefendantData.getBailStatus().getCode())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].bailStatus.id", + equalTo(updatedDefendantData.getBailStatus().getId().toString())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].firstName", + equalTo(updatedDefendantData.getFirstName())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].lastName", + equalTo(updatedDefendantData.getLastName())), +// withJsonPath("$.hearings[0].listedCases[0].defendants[0].specificRequirements", +// equalTo(updatedDefendantData.getSpecificRequirements())), +// withJsonPath("$.hearings[0].listedCases[0].defendants[0].organisationName", +// equalTo(updatedDefendantData.getLegalEntityName())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].restrictFromCourtList", + equalTo(hearingData.getListedCases().get(0).getDefendants().get(0).getRestrictFromCourtList())) }); } @@ -140,74 +122,43 @@ public void verifyHearingListedFromAPI(final boolean isAllocated) { * JMS-aware version of verifyHearingListedFromAPI for handling asynchronous message processing timing issues. */ public void verifyHearingListedFromAPIWithJmsDelay(final boolean isAllocated) { - verifyHearingListedFromAPIWithJmsDelay(isAllocated, null); - } - - /** - * Same as {@link #verifyHearingListedFromAPIWithJmsDelay(boolean)} with an optional assertion on the first listed defendant's youth flag. - */ - @SuppressWarnings("rawtypes") - public void verifyHearingListedFromAPIWithJmsDelay(final boolean isAllocated, final Boolean expectedIsYouth) { final com.jayway.jsonpath.JsonPath lastNameFilter = getJsonPathQueryForDefendantLastName(hearingData, listedCaseData, updatedDefendantData, updatedDefendantData.getLastName()); final com.jayway.jsonpath.JsonPath caseReferenceFilter = getJsonPathQueryForCaseReference(hearingData, listedCaseData, updatedDefendantData, listedCaseData.getCaseReference()); - final List matchers = new ArrayList<>(); - matchers.add(withJsonPath(lastNameFilter)); - matchers.add(withJsonPath(caseReferenceFilter)); - matchers.add(withJsonPath("$.hearings[0].id", - equalTo(hearingData.getId().toString()))); - matchers.add(withJsonPath("$.hearings[0].jurisdictionType", - equalTo(hearingData.getJurisdictionType()))); - matchers.add(withJsonPath("$.hearings[0].courtCentreId", - equalTo(hearingData.getCourtCentreId().toString()))); - matchers.add(withJsonPath("$.hearings[0].type.id", - equalTo(hearingData.getHearingTypeData().getTypeId().toString()))); - matchers.add(withJsonPath("$.hearings[0].type.description", - equalTo(hearingData.getHearingTypeData().getTypeDescription()))); - matchers.add(withJsonPath("$.hearings[0].startDate", - equalTo(hearingData.getHearingStartDate().toString()))); - matchers.add(withJsonPath(getJsonPathQueryForDefendantAttribute("id"), - hasItem(equalTo(updatedDefendantData.getDefendantId().toString())))); - matchers.add(withJsonPath(getJsonPathQueryForDefendantAttribute("masterDefendantId"), - hasItem(equalTo(updatedDefendantData.getMasterDefendantId().toString())))); - matchers.add(withJsonPath(getJsonPathQueryForDefendantAttribute("bailStatus.code"), - hasItem(equalTo(updatedDefendantData.getBailStatus().getCode())))); - matchers.add(withJsonPath(getJsonPathQueryForDefendantAttribute("bailStatus.id"), - hasItem(equalTo(updatedDefendantData.getBailStatus().getId().toString())))); - matchers.add(withJsonPath(getJsonPathQueryForDefendantAttribute("firstName"), - hasItem(equalTo(updatedDefendantData.getFirstName())))); - matchers.add(withJsonPath(getJsonPathQueryForDefendantAttribute("restrictFromCourtList"), - hasItem(equalTo(hearingData.getListedCases().get(0).getDefendants().get(0).getRestrictFromCourtList())))); - if (expectedIsYouth != null) { - matchers.add(withJsonPath(getJsonPathQueryForDefendantIsYouth(), hasItem(equalTo(expectedIsYouth)))); - } - - pollForHearingWithJmsDelay(hearingData.getCourtCentreId().toString(), isAllocated, getLoggedInUser().toString(), - matchers.toArray(new Matcher[0])); - - } - - private com.jayway.jsonpath.JsonPath getJsonPathQueryForDefendantIsYouth() { - return getJsonPathQueryForDefendantAttribute("isYouth"); - } + // Use JMS-aware polling to handle asynchronous message processing + pollForHearingWithJmsDelay(hearingData.getCourtCentreId().toString(), isAllocated, getLoggedInUser().toString(), new Matcher[]{ - private com.jayway.jsonpath.JsonPath getJsonPathQueryForDefendantWithIsYouth(final boolean expectedIsYouth) { - final HearingDefendantFilter hearingDefendantFilter = new HearingDefendantFilter(hearingData, listedCaseData, updatedDefendantData).invoke(); - final Filter isYouthFilter = filter(where("isYouth").eq(expectedIsYouth)); - return com.jayway.jsonpath.JsonPath.compile("$.hearings[?].listedCases[?].defendants[?][?]", - hearingDefendantFilter.getHearingFilter(), - hearingDefendantFilter.getListingCaseFilter(), - hearingDefendantFilter.getDefendantFilter(), - isYouthFilter); - } + withJsonPath(lastNameFilter), + withJsonPath(caseReferenceFilter), + withJsonPath("$.hearings[0].id", + equalTo(hearingData.getId().toString())), + withJsonPath("$.hearings[0].jurisdictionType", + equalTo(hearingData.getJurisdictionType())), + withJsonPath("$.hearings[0].courtCentreId", + equalTo(hearingData.getCourtCentreId().toString())), + withJsonPath("$.hearings[0].type.id", + equalTo(hearingData.getHearingTypeData().getTypeId().toString())), + withJsonPath("$.hearings[0].type.description", + equalTo(hearingData.getHearingTypeData().getTypeDescription())), + withJsonPath("$.hearings[0].startDate", + equalTo(hearingData.getHearingStartDate().toString())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].id", + equalTo(updatedDefendantData.getDefendantId().toString())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].masterDefendantId", + equalTo(updatedDefendantData.getMasterDefendantId().toString())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].bailStatus.code", + equalTo(updatedDefendantData.getBailStatus().getCode())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].bailStatus.id", + equalTo(updatedDefendantData.getBailStatus().getId().toString())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].firstName", + equalTo(updatedDefendantData.getFirstName())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].lastName", + equalTo(updatedDefendantData.getLastName())), + withJsonPath("$.hearings[0].listedCases[0].defendants[0].restrictFromCourtList", + equalTo(hearingData.getListedCases().get(0).getDefendants().get(0).getRestrictFromCourtList())) + }); - private com.jayway.jsonpath.JsonPath getJsonPathQueryForDefendantAttribute(final String attribute) { - final HearingDefendantFilter hearingDefendantFilter = new HearingDefendantFilter(hearingData, listedCaseData, updatedDefendantData).invoke(); - return com.jayway.jsonpath.JsonPath.compile("$.hearings[?].listedCases[?].defendants[?]." + attribute, - hearingDefendantFilter.getHearingFilter(), - hearingDefendantFilter.getListingCaseFilter(), - hearingDefendantFilter.getDefendantFilter()); } private static com.jayway.jsonpath.JsonPath getJsonPathQueryForDefendantLastName(final HearingData hearing, final ListedCaseData listedCase, final UpdatedDefendantData defendant, final String expectedLastName) { diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateHearingSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateHearingSteps.java index 8f22741f4..833056605 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateHearingSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/UpdateHearingSteps.java @@ -88,7 +88,6 @@ import java.util.UUID; import uk.gov.justice.services.messaging.JsonObjects; -import uk.gov.moj.cpp.listing.it.util.ItClock; import javax.json.JsonArray; import javax.json.JsonArrayBuilder; import javax.json.JsonObject; @@ -811,32 +810,20 @@ public void verifyNonDefaultDaysAssignedEventNotRaised() { } public void verifyPublicEventHearingConfirmed() { - final String expectedHearingId = updatedHearingData.getHearingId().toString(); - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingConfirmed, - org.hamcrest.CoreMatchers.containsString(expectedHearingId)); - assertNotNull("No public hearing-confirmed event found for hearingId=" + expectedHearingId, jsonResponse); + final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingConfirmed); verifyHearingPublicDetails(jsonResponse, "confirmedHearing"); } public void verifyPublicEventHearingDaysChangedForHearing() { - final String expectedHearingId = updatedHearingData.getHearingId().toString(); - // Match by hearingId so a stale event from another test on the shared public topic is skipped. - final JsonPath jsonResponse = retrieveMessage(publicEventHearingDaysChangedForHearing, - org.hamcrest.CoreMatchers.containsString(expectedHearingId)); - assertNotNull("No public hearing-days-changed event found for hearingId=" + expectedHearingId, jsonResponse); + final JsonPath jsonResponse = retrieveMessage(publicEventHearingDaysChangedForHearing); - assertThat(jsonResponse.get("hearingId"), is(expectedHearingId)); + assertThat(jsonResponse.get("hearingId"), is(updatedHearingData.getHearingId().toString())); assertThat(jsonResponse.get("hearingDays[0].courtCentreId"), is(updatedHearingData.getCourtCentreId().toString())); } public void verifyPublicEventHearingConfirmed_hasNoJudiciary() { - final String expectedHearingId = updatedHearingData.getHearingId().toString(); - // Match by hearingId so a stale hearing-confirmed event from another test on the shared public - // topic is skipped rather than consumed (drains until this hearing's event arrives). - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingConfirmed, - org.hamcrest.CoreMatchers.containsString(expectedHearingId)); - assertNotNull("No public hearing-confirmed event found for hearingId=" + expectedHearingId, jsonResponse); - assertThat(jsonResponse.get("confirmedHearing.id"), is(expectedHearingId)); + final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingConfirmed); + assertThat(jsonResponse.get("confirmedHearing.id"), is(updatedHearingData.getHearingId().toString())); assertNull(jsonResponse.get("confirmedHearing.judiciary")); } @@ -950,8 +937,7 @@ public void verifyProsecutionCaseDefendantsOffenceIds(final int count) { } public void verifyHearingUnallocatedCourtroomRemoveds(final UUID hearingId) { - final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingUnallocatedCourtroomRemoved, - org.hamcrest.CoreMatchers.containsString(hearingId.toString())); + final JsonPath jsonResponse = retrieveMessage(publicMessageConsumerHearingUnallocatedCourtroomRemoved); LOGGER.info("jsonResponse from publicMessageConsumerHearingUnallocatedCourtroomRemoved: {}", jsonResponse.prettify()); assertThat(jsonResponse.getUUID("hearingId"), is(hearingId)); @@ -1126,8 +1112,8 @@ public String verifyHearingFoundByAllocatedAndCourtCentreFromAPIAndStartDateAndE } public String verifyHearingFoundByAllocatedAndCourtCentreFromAPIAndStartDateAndEndDateCourtCalendarWithPagination(UUID courtCentreId,int pageSize, int pageNumber, int itemCount) { - var startDate = ItClock.today().toString(); - var endDate = ItClock.today().plusDays(3).toString(); + var startDate = LocalDate.now().toString(); + var endDate = LocalDate.now().plusDays(3).toString(); final String searchHearingUrl = String.format("%s/%s", getBaseUri(), format(readConfig().getProperty("listing.search.hearingscourt.calendar.by.allocated.court-centre-id.start-date.end-date-with-pagination"), ALLOCATED, @@ -1482,23 +1468,17 @@ public void verifyAllocatedHearingFoundByRangeSearch(final UUID courtCentreId, f public void verifyHearingPayloadProperty(final String hearingId, final String propertyName, final Matcher matcher) { final String hearingIdFilter = getHearingFilter(hearingId); - // Poll until the hearing is in the unallocated list AND the property has actually been cleared. - // Polling only on list membership and then reading the property outside the loop was racy: on a - // slower (pipeline/vld) read model the row shows up unallocated a beat before the property is - // nulled, so the stale original value leaked through (HearingIT:576 flake). A cleared property is - // omitted from the projection, so the filtered path resolves to an empty list -> hasSize(0). final String payload = pollForHearingWithJmsDelay(updatedHearingData.getCourtCentreId().toString(), UNALLOCATED, getLoggedInUser().toString(), new Matcher[]{ - withJsonPath(hearingIdFilter, hasSize(1)), - withJsonPath(hearingIdFilter + "." + propertyName, hasSize(0)) + withJsonPath(hearingIdFilter, hasSize(1)) }); final JsonObject payloadAsJsonObject = new StringToJsonObjectConverter().convert(payload); - final Object propertyValue = payloadAsJsonObject.getJsonArray("hearings").stream(). + Object courtRoomId = payloadAsJsonObject.getJsonArray("hearings").stream(). map(h -> (JsonObject) h) .filter(h -> h.getString("id").equals(hearingId)) .findFirst().get().get(propertyName); - assertThat(propertyValue, matcher); + assertThat(courtRoomId, matcher); } public void verifyJudiciaryChangedForHearingStatusPublicEvent() { @@ -1516,8 +1496,8 @@ public void publishPublicHearingResultedEvent(final HearingData hearingData) thr .replaceAll("%DEFENDANT_ID%", hearingData.getListedCases().get(0).getDefendants().get(0).getDefendantId().toString()) .replaceAll("%PROSECUTION_CASE_ID%", hearingData.getListedCases().get(0).getCaseId().toString()) .replaceAll("%CASE_URN%", hearingData.getListedCases().get(0).getCaseReference().toString()) - .replaceAll("%SHARED_TIME%", ItClock.nowUtc().format(DateTimeFormatter.ISO_INSTANT)) - .replaceAll("%HEARING_DAY%", ItClock.today().toString()) + .replaceAll("%SHARED_TIME%", ZonedDateTime.now().format(DateTimeFormatter.ISO_INSTANT)) + .replaceAll("%HEARING_DAY%", LocalDate.now().toString()) ); sendMessage(publicEventMessageProducer, EVENT_SELECTED_PUBLIC_HEARING_RESULTED, eventPayload, diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/CommittingCourtTestDetails.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/CommittingCourtTestDetails.java deleted file mode 100644 index 2dc20b926..000000000 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/CommittingCourtTestDetails.java +++ /dev/null @@ -1,32 +0,0 @@ -package uk.gov.moj.cpp.listing.steps.data; - -import java.util.UUID; - -/** - * Optional committing court for integration tests (Xhibit / cp-xhibit reference data paths). - * {@code courtHouseType} uses the same strings as reference data / Xhibit, e.g. {@code MAGISTRATES_COURT}, {@code CROWN_COURT}. - */ -public final class CommittingCourtTestDetails { - - private final UUID courtCentreId; - private final String courtHouseName; - private final String courtHouseType; - - public CommittingCourtTestDetails(final UUID courtCentreId, final String courtHouseName, final String courtHouseType) { - this.courtCentreId = courtCentreId; - this.courtHouseName = courtHouseName; - this.courtHouseType = courtHouseType; - } - - public UUID getCourtCentreId() { - return courtCentreId; - } - - public String getCourtHouseName() { - return courtHouseName; - } - - public String getCourtHouseType() { - return courtHouseType; - } -} diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/CourtApplicationPartyData.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/CourtApplicationPartyData.java index 232b4b4f5..d15a4ab51 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/CourtApplicationPartyData.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/CourtApplicationPartyData.java @@ -20,15 +20,7 @@ public class CourtApplicationPartyData { private final Address address; - private final UUID masterDefendantId; - - private final java.time.LocalDate dateOfBirth; - public CourtApplicationPartyData(final UUID id, String firstName, Boolean respondent, String lastName, final CourtApplicationPartyType courtApplicationPartyType, final LegalEntityDefendantData legalEntityDefendant, final Address address) { - this(id, firstName, respondent, lastName, courtApplicationPartyType, legalEntityDefendant, address, null, null); - } - - public CourtApplicationPartyData(final UUID id, String firstName, Boolean respondent, String lastName, final CourtApplicationPartyType courtApplicationPartyType, final LegalEntityDefendantData legalEntityDefendant, final Address address, final UUID masterDefendantId, final java.time.LocalDate dateOfBirth) { this.id = id; this.firstName = firstName; this.respondent = respondent; @@ -36,8 +28,6 @@ public CourtApplicationPartyData(final UUID id, String firstName, Boolean respon this.courtApplicationPartyType = courtApplicationPartyType; this.legalEntityDefendant = legalEntityDefendant; this.address = address; - this.masterDefendantId = masterDefendantId; - this.dateOfBirth = dateOfBirth; } public String getFirstName() { @@ -67,12 +57,4 @@ public LegalEntityDefendantData getLegalEntityDefendant() { public Address getAddress() { return address; } - - public UUID getMasterDefendantId() { - return masterDefendantId; - } - - public java.time.LocalDate getDateOfBirth() { - return dateOfBirth; - } } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/HearingsData.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/HearingsData.java index a8645c6af..ba3b3feb8 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/HearingsData.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/HearingsData.java @@ -18,26 +18,6 @@ public static HearingsData hearingsData() { return new HearingsData(HearingsDataFactory.hearingsData()); } - public static HearingsData hearingsDataForYoungDefendants() { - return new HearingsData(HearingsDataFactory.hearingsDataForYoungDefendants()); - } - - public static HearingsData singleHearingDataForYoungDefendants() { - return new HearingsData(List.of(HearingsDataFactory.hearingsDataForYoungDefendants().get(0))); - } - - public static HearingsData hearingsDataWithAdultDefendants() { - return new HearingsData(HearingsDataFactory.hearingsDataWithAdultDefendants()); - } - - public static HearingsData hearingsDataForYoungCourtApplicationRespondent() { - return new HearingsData(HearingsDataFactory.hearingsDataForYoungCourtApplicationRespondent()); - } - - public static HearingsData hearingsDataForYoungCourtApplicationSubject() { - return new HearingsData(HearingsDataFactory.hearingsDataForYoungCourtApplicationSubject()); - } - public static HearingsData hearingsDataWithExParteOffence() { return new HearingsData(HearingsDataFactory.hearingsDataForCasesWithExParte()); } @@ -81,10 +61,6 @@ public static HearingsData hearingsDataForWeekCommencing(final LocalDate startDa return new HearingsData(HearingsDataFactory.hearingsDataForWeekCommencing(startDate, duration)); } - public static HearingsData hearingsDataForWeekCommencingWithYoungDefendants(final LocalDate startDate, final Integer duration) { - return new HearingsData(HearingsDataFactory.hearingsDataForWeekCommencingWithYoungDefendants(startDate, duration)); - } - public static HearingsData hearingsDataForWeekCommencing(final LocalDate startDate, final Integer duration, UUID courtCenterId, UUID courtRoomId, String roles) { return new HearingsData(HearingsDataFactory.hearingsDataForWeekCommencing(startDate, duration, courtCenterId, courtRoomId, roles)); } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/OffenceData.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/OffenceData.java index 496679a22..ec92a4844 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/OffenceData.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/OffenceData.java @@ -30,8 +30,6 @@ public class OffenceData { private String indictmentParticular; private CivilOffenceData civilOffenceData; - private CommittingCourtTestDetails committingCourtTestDetails; - public OffenceData(final UUID offenceId, final String offenceCode, final LocalDate startDate, final LocalDate endDate, final String statementOfOffenceTitle, final String statementOfOffenceTitleWelsh, final String offenceWording, @@ -60,14 +58,6 @@ public OffenceData(final UUID offenceId, final String offenceCode, this.civilOffenceData = civilOffenceData; } - public CommittingCourtTestDetails getCommittingCourtTestDetails() { - return committingCourtTestDetails; - } - - public void setCommittingCourtTestDetails(final CommittingCourtTestDetails committingCourtTestDetails) { - this.committingCourtTestDetails = committingCourtTestDetails; - } - public Optional getLaaApplnReference() { return laaApplnReference; } @@ -190,6 +180,5 @@ public void copyOffenceData(OffenceData offenceData) { this.statementOfOffenceTitle = offenceData.getStatementOfOffenceTitle(); this.statementOfOffenceTitleWelsh = offenceData.getStatementOfOffenceTitleWelsh(); this.civilOffenceData = offenceData.getCivilOffenceData(); - this.committingCourtTestDetails = offenceData.getCommittingCourtTestDetails(); } } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/RestrictCourtListData.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/RestrictCourtListData.java index 6952234e1..af518fa53 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/RestrictCourtListData.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/RestrictCourtListData.java @@ -21,16 +21,13 @@ public class RestrictCourtListData { private final List courtApplicationRespondentIds; - private final List courtApplicationSubjectIds; - private final Optional courtApplicationType; - public RestrictCourtListData(final List caseIds, final List courtApplicationApplicantIds, final List courtApplicationIds, final List courtApplicationRespondentIds, final List courtApplicationSubjectIds, final List defendantIds, final UUID hearingId, final List offenceIds, final Boolean restrictCourtList, final Optional courtApplicationType) { + public RestrictCourtListData(final List caseIds, final List courtApplicationApplicantIds, final List courtApplicationIds, final List courtApplicationRespondentIds, final List defendantIds, final UUID hearingId, final List offenceIds, final Boolean restrictCourtList, final Optional courtApplicationType) { this.caseIds = caseIds; this.courtApplicationApplicantIds = courtApplicationApplicantIds; this.courtApplicationIds = courtApplicationIds; this.courtApplicationRespondentIds = courtApplicationRespondentIds; - this.courtApplicationSubjectIds = courtApplicationSubjectIds; this.defendantIds = defendantIds; this.hearingId = hearingId; this.offenceIds = offenceIds; @@ -66,10 +63,6 @@ public List getCourtApplicationRespondentIds() { return courtApplicationRespondentIds; } - public List getCourtApplicationSubjectIds() { - return courtApplicationSubjectIds; - } - public UUID getHearingId() { return hearingId; } @@ -99,8 +92,6 @@ public static class Builder { private List courtApplicatonRespondentIds; - private List courtApplicationSubjectIds; - private Optional courtApplicationType; public Builder withCaseIds(final List casesId) { @@ -121,12 +112,6 @@ public Builder withCourtApplicatonRespondentIds(final List courtApplicaton this.courtApplicatonRespondentIds = courtApplicatonRespondentIds; return this; } - - public Builder withCourtApplicationSubjectIds(final List courtApplicationSubjectIds) { - this.courtApplicationSubjectIds = courtApplicationSubjectIds; - return this; - } - public Builder withDefendantIds(final List defendantsId) { this.defendantIds = defendantsId; return this; @@ -153,7 +138,7 @@ public Builder withCourtApplicationType(final Optional courtApplicationT } public RestrictCourtListData build() { - return new RestrictCourtListData(caseIds, courtApplicationApplicantIds, courtApplicatonIds, courtApplicatonRespondentIds, courtApplicationSubjectIds, defendantIds, hearingId, offenceIds, restrictCourtList, courtApplicationType); + return new RestrictCourtListData(caseIds, courtApplicationApplicantIds, courtApplicatonIds, courtApplicatonRespondentIds, defendantIds, hearingId, offenceIds, restrictCourtList, courtApplicationType); } } } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/SequenceHearingData.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/SequenceHearingData.java index 2b375e84f..b6d34f2f2 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/SequenceHearingData.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/SequenceHearingData.java @@ -1,7 +1,5 @@ package uk.gov.moj.cpp.listing.steps.data; -import uk.gov.moj.cpp.listing.it.util.ItClock; - import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; @@ -47,7 +45,7 @@ private Map createSequenceMap(String endDate) { private List getLocalDateRange(final String endDate) { - final LocalDate start = LocalDate.parse(ItClock.today().toString(), DateTimeFormatter.ofPattern("yyyy-MM-dd")); + final LocalDate start = LocalDate.parse(LocalDate.now().toString(), DateTimeFormatter.ofPattern("yyyy-MM-dd")); final LocalDate end = LocalDate.parse(endDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")); final int days = (int) start.until(end, ChronoUnit.DAYS); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/UpdatedDefendantData.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/UpdatedDefendantData.java index 8c7ac779d..40bcd9085 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/UpdatedDefendantData.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/UpdatedDefendantData.java @@ -4,15 +4,12 @@ import static java.util.UUID.randomUUID; import static org.codehaus.groovy.runtime.InvokerHelper.asList; -import java.time.LocalDate; - import uk.gov.justice.core.courts.AssociatedDefenceOrganisation; import uk.gov.justice.core.courts.BailStatus; import uk.gov.justice.core.courts.DefenceOrganisation; import uk.gov.justice.core.courts.DefendantAlias; import uk.gov.justice.core.courts.FundingType; import uk.gov.justice.core.courts.Organisation; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.util.List; import java.util.Optional; @@ -41,69 +38,12 @@ public class UpdatedDefendantData { private final AssociatedDefenceOrganisation associatedDefenceOrganisation; public static UpdatedDefendantData updatedDefendantData(DefendantData defendantData) { - return baseBuilder(defendantData).withIsYouth(Boolean.TRUE).build(); - } - - public static UpdatedDefendantData updatedDefendantDataWithIsYouth(final DefendantData defendantData, final Boolean isYouth) { - final Builder builder = baseBuilder(defendantData); - if (isYouth != null) { - builder.withIsYouth(isYouth); - } - return builder.build(); - } - - /** - * Partial progression update: omits {@code isYouth} from the public event payload (null in courts Defendant). - */ - public static UpdatedDefendantData partialDefendantUpdateWithoutIsYouth(final DefendantData defendantData, - final String firstName, - final String lastName) { - return baseBuilder(defendantData) - .withFirstName(firstName) - .withLastName(lastName) - .build(); - } - - private static Builder baseBuilder(final DefendantData defendantData) { - return Builder.UpdatedDefendantData() - .withBailStatus(new BailStatus.Builder().withCode(defendantData.getBailStatus().getCode()) - .withDescription(defendantData.getBailStatus().getDescription()) - .withId(fromString(defendantData.getBailStatus().getId().toString())).build()) - .withCustodyTimeLimit("2025-07-27") - .withDateOfBirth("2006-07-27") - .withDefendantId(defendantData.getDefendantId()) - .withMasterDefendantId(fromString(defendantData.getMasterDefendantId().toString())) - .withFirstName(defendantData.getFirstName()) - .withLastName(defendantData.getLastName()) - .withOrganisationName("withOrganisationName") - .withLegalEntityName("withOrganisationName") - .withLegalEntityId(fromString("55b8e1fd-085d-4236-a14f-8a35d86db8b2")) - .withSpecificRequirements("withSpecificRequirements") - .withCourtCentreId(randomUUID()) - .withPncId("pncId") - .withAssociatedDefenceOrganisation(AssociatedDefenceOrganisation.associatedDefenceOrganisation() - .withFundingType(FundingType.REPRESENTATION_ORDER) - .withAssociationStartDate("2018-10-10") - .withDefenceOrganisation(DefenceOrganisation.defenceOrganisation() - .withOrganisation(Organisation.organisation() - .withName("withOrganisationName") - .build()) - .withLaaContractNumber("LAACONTRACT") - .build()) - .build()) - .withAliases(asList(DefendantAlias.defendantAlias() - .withFirstName("Alias First Name") - .withLastName("Alias Last Name") - .build())); - } - - public static UpdatedDefendantData updatedDefendantDataWithUnder18DateOfBirth(final DefendantData defendantData) { return Builder.UpdatedDefendantData() .withBailStatus(new BailStatus.Builder().withCode(defendantData.getBailStatus().getCode()) .withDescription(defendantData.getBailStatus().getDescription()) .withId(fromString(defendantData.getBailStatus().getId().toString())).build()) .withCustodyTimeLimit("2025-07-27") - .withDateOfBirth(ItClock.today().minusYears(15).toString()) + .withDateOfBirth("2025-07-27") .withDefendantId(defendantData.getDefendantId()) .withMasterDefendantId(fromString(defendantData.getMasterDefendantId().toString())) .withFirstName(defendantData.getFirstName()) diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/UpdatedHearingData.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/UpdatedHearingData.java index 035472109..461246b2e 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/UpdatedHearingData.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/UpdatedHearingData.java @@ -13,7 +13,6 @@ import uk.gov.justice.listing.events.Offences; import uk.gov.justice.listing.events.ProsecutionCases; import uk.gov.justice.services.test.utils.core.random.RandomGenerator; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.DayOfWeek; import java.time.LocalDate; @@ -173,11 +172,6 @@ public static UpdatedHearingData updatedHearingDataForAllocationWithNonDefaultDa return updatedHearingDataForAllocationWithNonDefaultDays(hearingId, judiciary); } - public static UpdatedHearingData updatedHearingDataForAllocationWithMagistratesSearch(final UUID hearingId) { - final List judiciary = singletonList(new JudicialRoleData(of(true), of(true), UUID.randomUUID(), UUID.randomUUID(), new JudicialRoleTypeData(Optional.of(randomUUID()), "MAGISTRATE"))); - return updatedHearingDataForAllocationWithoutCourtScheduleIds(hearingId, judiciary); - } - public static UpdatedHearingData updatedHearingDataForAllocationWithNonDefaultDaysWithAdditionalFields(final UUID hearingId) { final List judiciary = singletonList(new JudicialRoleData(of(true), of(true), UUID.randomUUID(), UUID.randomUUID(), new JudicialRoleTypeData(Optional.of(randomUUID()), "MAGISTRATE"))); return updatedHearingDataForAllocationWithNonDefaultDaysWithAdditionalFields(hearingId, judiciary); @@ -207,7 +201,7 @@ private static UpdatedHearingData updatedHearingDataForAllocationWithVirtual(fin final UUID courtCentreId = getRandomCourtCenterId(); final UUID roomId = getRandomCourtRoomId(); - final LocalDate startDate = nextOrSameWorkingDay(ItClock.today()); + final LocalDate startDate = nextOrSameWorkingDay(LocalDate.now()); final ZonedDateTime startTimeWithZone = ZonedDateTime.of(startDate, DEFAULT_START_TIME, UTC); final List nonSittingDays = singletonList(startDate.plusDays(1).toString()); @@ -256,7 +250,7 @@ private static UpdatedHearingData updatedHearingDataForAllocationForDefendant(fi public static UpdatedHearingData updatedHearingDataForPublicListNote(final HearingData hearingData, final Boolean hasVideoLink, final String publicListNote) { - final LocalDate startDate = nextOrSameWorkingDay(ItClock.today()); + final LocalDate startDate = nextOrSameWorkingDay(LocalDate.now()); final List nonSittingDays = singletonList(startDate.plusDays(1).toString()); @@ -282,26 +276,6 @@ private static boolean isWeekEnd(LocalDate date) { } - private static UpdatedHearingData updatedHearingDataForAllocationWithoutCourtScheduleIds(final UUID hearingId, final List judiciary) { - final UUID courtCentreId = getRandomCourtCenterId(); - final UUID roomId = getRandomCourtRoomId(); - - final LocalDate startDate = nextOrSameWorkingDay(ItClock.today()); - final ZonedDateTime startTimeWithZone = ZonedDateTime.of(startDate, DEFAULT_START_TIME, UTC); - - final List nonSittingDays = singletonList(startDate.plusDays(1).toString()); - - final NonDefaultDayData nonDefaultDayData = new NonDefaultDayData(startTimeWithZone.format(DATE_TIME_FORMAT), of(DURATION), empty(), of(1), of(OUCODE), of(SESSION), of(courtCentreId).map(UUID::toString), of(roomId).map(UUID::toString), empty()); - - final List nonDefaultDays = singletonList(nonDefaultDayData); - - final String endDate = startDate.toString(); - - return new UpdatedHearingData(hearingId, courtCentreId, RandomGenerator.STRING.next(), roomId, SENTENCE_HEARING_TYPE, - startDate.toString(), endDate, nonDefaultDays, - nonSittingDays, HEARING_LANGUAGE_WELSH, judiciary, JURISDICTION_TYPE_MAGISTRATES, null, null, null, true, "publicListNote", false, null, null); - } - private static UpdatedHearingData updatedHearingDataForAllocationWithNonDefaultDays(final UUID hearingId, final List judiciary) { final String endDate = "2020-04-23"; final LocalDate startDate = LocalDate.parse(endDate); @@ -319,7 +293,7 @@ private static UpdatedHearingData updatedHearingDataForAllocationWithNonDefaultD } public static UpdatedHearingData updatedHearingDataForAllocationWithNonDefaultDaysWithoutCourtRoomSelection(final UUID hearingId, final UUID courtCentreId) { - final String endDate = ItClock.today().toString(); + final String endDate = LocalDate.now().toString(); final LocalDate startDate = LocalDate.parse(endDate); final ZonedDateTime startTimeWithZone = ZonedDateTime.parse("2020-04-23T11:32:41.587Z"); @@ -348,7 +322,7 @@ private static UpdatedHearingData updatedHearingDataForAllocationWithNonDefaultD } public static UpdatedHearingData updatedHearingData(final HearingData hearingData) { - return updatedHearingData(hearingData, ItClock.today().plusDays(21)); + return updatedHearingData(hearingData, LocalDate.now().plusDays(21)); } public static UpdatedHearingData updatedHearingData(final HearingData hearingData, final LocalDate startDate) { diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingDayFactory.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingDayFactory.java index 9a0c7fd2a..01e10df50 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingDayFactory.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingDayFactory.java @@ -1,9 +1,9 @@ package uk.gov.moj.cpp.listing.steps.data.factory; +import static java.time.ZonedDateTime.now; import static uk.gov.moj.cpp.listing.steps.data.HearingDay.hearingDay; import uk.gov.justice.services.test.utils.common.helper.StoppedClock; -import uk.gov.moj.cpp.listing.it.util.ItClock; import uk.gov.moj.cpp.listing.steps.data.HearingDay; import java.util.List; @@ -14,7 +14,7 @@ public class HearingDayFactory { public static List buildHearingDaysWithCancelledFlag(final Boolean firstDayFlag, final Boolean secondDayFlag, final Boolean thirdDayFlag) { - final StoppedClock clock = new StoppedClock(ItClock.nowUtc()); + final StoppedClock clock = new StoppedClock(now()); final HearingDay day1 = hearingDay() .withListedDurationMinutes(30) .withListingSequence(0) diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingsDataFactory.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingsDataFactory.java index 59ec62a44..3ff3e44d2 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingsDataFactory.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/data/factory/HearingsDataFactory.java @@ -1,5 +1,6 @@ package uk.gov.moj.cpp.listing.steps.data.factory; +import static java.time.LocalDate.now; import static java.util.Collections.singletonList; import static java.util.Optional.of; import static java.util.UUID.fromString; @@ -36,7 +37,6 @@ import uk.gov.moj.cpp.listing.steps.data.OffenceData; import uk.gov.moj.cpp.listing.steps.data.OrganisationData; import uk.gov.moj.cpp.listing.steps.data.ReportingRestrictionData; -import uk.gov.moj.cpp.listing.it.util.ItClock; import java.time.LocalDate; import java.time.ZonedDateTime; @@ -59,7 +59,7 @@ public class HearingsDataFactory { private static final HearingTypeData TRIAL_HEARING_TYPE = new HearingTypeData(UUID.fromString("bf8155e1-90b9-4080-b133-bfbad895d6e4"), "Trial", "welsh trial"); private static final BailStatus BAIL_CONDITIONAL = new BailStatus.Builder().withId(fromString("34443c87-fa6f-34c0-897f-0cce45773df5")).withCode("P").withDescription("Custody or remanded into custody").build(); - public static final ZonedDateTime SLOT_START_TIME = ItClock.nowUtc(); + public static final ZonedDateTime SLOT_START_TIME = ZonedDateTime.now(); public static final Integer SLOT_DURATION = 25; public static final String SLOT_SCHEDULE_ID = randomUUID().toString(); public static final String SLOT_SESSION = "AM"; @@ -75,22 +75,6 @@ public static List hearingsData() { return manyRandomHearings(2); } - public static List hearingsDataForYoungDefendants() { - return List.of(randomHearingWithYoungDefendants(), randomHearingWithYoungDefendants()); - } - - public static List hearingsDataForYoungCourtApplicationRespondent() { - return List.of(randomHearingWithYoungCourtApplicationRespondent(), randomHearingWithYoungCourtApplicationRespondent()); - } - - public static List hearingsDataForYoungCourtApplicationSubject() { - return List.of(randomHearingWithYoungCourtApplicationSubject(), randomHearingWithYoungCourtApplicationSubject()); - } - - public static List hearingsDataWithAdultDefendants() { - return List.of(randomHearingWithAdultDefendants()); - } - public static List trialHearingsData() { return manyRandomHearings(2, TRIAL_HEARING_TYPE); } @@ -131,10 +115,6 @@ public static List hearingsDataForWeekCommencing(final LocalDate we return createRandomHearingWithWeekCommencing(1, weekCommencingStartDate, weekCommencingDuration); } - public static List hearingsDataForWeekCommencingWithYoungDefendants(final LocalDate weekCommencingStartDate, final Integer weekCommencingDuration) { - return createRandomHearingWithWeekCommencingAndYoungDefendants(1, weekCommencingStartDate, weekCommencingDuration); - } - public static List hearingsDataForWeekCommencing(final LocalDate weekCommencingStartDate, final Integer weekCommencingDuration, UUID courtCenterId, UUID courtRoomId, String role) { return createRandomHearingWithWeekCommencing(1, weekCommencingStartDate, weekCommencingDuration, courtCenterId, courtRoomId, role); } @@ -405,7 +385,7 @@ private static List manyRandomHearingsWithAllocationData(final Inte final CaseAndDefendantData caseAndDefendantData, final UUID courtCentreId, final UUID courtRoomId) { - final LocalDate hearingEndDate = ItClock.today().plusDays(1); + final LocalDate hearingEndDate = LocalDate.now().plusDays(1); return IntStream.range(0, numberOfHearings) .mapToObj((int i) -> randomHearing(hearingEndDate, courtCentreId, courtRoomId, singletonList(randomJudicialRole()), caseAndDefendantData)) .collect(toList()); @@ -425,7 +405,7 @@ private static List manyRandomHearingsWithAllocationDataWithDate(fi private static List manyRandomHearingsWithUnAllocationData(final Integer numberOfHearings, final CaseAndDefendantData caseAndDefendantData) { - final LocalDate hearingEndDate = ItClock.today().plusDays(1); + final LocalDate hearingEndDate = LocalDate.now().plusDays(1); return IntStream.range(0, numberOfHearings) .mapToObj((int i) -> randomUnAllocatedHearing(hearingEndDate, null, singletonList(randomJudicialRole()), caseAndDefendantData)) .collect(toList()); @@ -504,12 +484,6 @@ private static List createRandomHearingWithWeekCommencing(final Int .collect(toList()); } - private static List createRandomHearingWithWeekCommencingAndYoungDefendants(final Integer numberOfHearings, final LocalDate weekCommencingStartDate, final Integer weekCommencingDuration) { - return IntStream.range(0, numberOfHearings) - .mapToObj((int i) -> randomHearingWithWeekCommencingDatesAndYoungDefendants(null, null, null, weekCommencingStartDate, weekCommencingDuration)) - .collect(toList()); - } - private static List createRandomHearingWithWeekCommencing(final Integer numberOfHearings, final LocalDate weekCommencingStartDate, final Integer weekCommencingDuration, UUID courtCenterId, UUID courtRoomId, String judicialRoles) { return IntStream.range(0, numberOfHearings) .mapToObj((int i) -> randomHearingWithWeekCommencingDates(null, courtCenterId, courtRoomId, singletonList(randomJudicialRole(judicialRoles)), weekCommencingStartDate, weekCommencingDuration)) @@ -562,17 +536,17 @@ private static List manyRandomHearingsStandaloneApplication(final I public static List hearingsDataWithShadowListedOffences() { final UUID courtCentreId = getRandomCourtCenterId(); final String judiciaryType = MAGISTRATES_JURISDICTION; - final LocalDate hearingEndDate = ItClock.today().plusDays(1); + final LocalDate hearingEndDate = LocalDate.now().plusDays(1); final UUID courtRoomId = getRandomCourtRoomId(); final List listedCaseData = manyRandomListingCases(2); final List listHearingData = new ArrayList<>(); - listHearingData.add(new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), + listHearingData.add(new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, Arrays.asList(randomJudicialRole(judiciaryType)), judiciaryType, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", ItClock.today().toString())); + singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", LocalDate.now().toString())); listHearingData.get(0) .getListedCases() @@ -590,18 +564,6 @@ private static List manyRandomListingCases(final Integer numberO .collect(toList()); } - private static List manyRandomListedCasesWithYoungDefendants(final Integer numberOfListingCases) { - return IntStream.range(0, numberOfListingCases) - .mapToObj((int i) -> randomListedCaseWithYoungDefendants()) - .collect(toList()); - } - - private static List manyRandomListedCasesWithAdultDefendants(final Integer numberOfListingCases) { - return IntStream.range(0, numberOfListingCases) - .mapToObj((int i) -> randomListedCaseWithAdultDefendants()) - .collect(toList()); - } - private static List manyRandomListingSingleCivilCases(final Integer numberOfListingCases) { return IntStream.range(0, numberOfListingCases) .mapToObj((int i) -> randomListedSingleCivilCase()) @@ -707,18 +669,6 @@ private static List manyRandomDefendants(final Integer numberOfDe .collect(toList()); } - private static List manyRandomYoungDefendants(final Integer numberOfDefendants) { - return IntStream.range(0, numberOfDefendants) - .mapToObj((int i) -> randomYoungDefendant()) - .collect(toList()); - } - - private static List manyRandomAdultDefendants(final Integer numberOfDefendants) { - return IntStream.range(0, numberOfDefendants) - .mapToObj((int i) -> randomAdultDefendant()) - .collect(toList()); - } - private static List manyRandomDefendantsTwoOffences(final Integer numberOfDefendants) { return IntStream.range(0, numberOfDefendants) .mapToObj((int i) -> randomDefendant(2)) @@ -789,14 +739,6 @@ private static ListedCaseData randomListedCase() { return new ListedCaseData(randomUUID(), randomUUID(), STRING.next(), randomCaseReference(), manyRandomDefendants(2), false, false, manyRandomCaseMarkers(1), STRING.next(), null, null, null, null); } - private static ListedCaseData randomListedCaseWithYoungDefendants() { - return new ListedCaseData(randomUUID(), randomUUID(), STRING.next(), randomCaseReference(), manyRandomYoungDefendants(2), false, false, manyRandomCaseMarkers(1), STRING.next(), null, null, null, null); - } - - private static ListedCaseData randomListedCaseWithAdultDefendants() { - return new ListedCaseData(randomUUID(), randomUUID(), STRING.next(), randomCaseReference(), manyRandomAdultDefendants(2), false, false, manyRandomCaseMarkers(1), STRING.next(), null, null, null, null); - } - private static ListedCaseData randomListedSingleCivilCase() { return new ListedCaseData(randomUUID(), randomUUID(), STRING.next(), randomCaseReference(), manyRandomDefendants(2), false, false, manyRandomCaseMarkers(1), STRING.next(), null, true, null, null); } @@ -845,7 +787,7 @@ private static ListedCaseData randomListedCaseWithDefendantHavingListingReason(f } private static LaaReferenceData randomLaaReferenceData() { - return new LaaReferenceData(STRING.next(), of(ItClock.today()), Optional.of(ItClock.today()), STRING.next(), ItClock.today(), STRING.next(), randomUUID()); + return new LaaReferenceData(STRING.next(), of(LocalDate.now()), Optional.of(LocalDate.now()), STRING.next(), LocalDate.now(), STRING.next(), randomUUID()); } private static String randomCaseReference() { @@ -856,31 +798,31 @@ private static String randomCaseReference() { private static OffenceData randomOffence() { final CivilOffenceData civilOffenceData = new CivilOffenceData(false); - return new OffenceData(randomUUID(), STRING.next(), ItClock.today(), - ItClock.today(), STRING.next(), STRING.next(), STRING.next(), - OFFENCE_COUNT, OFFENCE_ORDER_INDEX, OFFENCE_LEGISLATION, randomUUID(), Optional.of(randomCustodyTimeLimit()), Optional.of(randomLaaReferenceData()), ItClock.today(), of(Boolean.FALSE), manyRandomReportingRestriction(2), STRING.next(), civilOffenceData); + return new OffenceData(randomUUID(), STRING.next(), LocalDate.now(), + LocalDate.now(), STRING.next(), STRING.next(), STRING.next(), + OFFENCE_COUNT, OFFENCE_ORDER_INDEX, OFFENCE_LEGISLATION, randomUUID(), Optional.of(randomCustodyTimeLimit()), Optional.of(randomLaaReferenceData()), LocalDate.now(), of(Boolean.FALSE), manyRandomReportingRestriction(2), STRING.next(), civilOffenceData); } private static OffenceData randomOffence(OffenceData offence) { final CivilOffenceData civilOffenceData = new CivilOffenceData(BOOLEAN.next()); - return new OffenceData(offence.getOffenceId(), STRING.next(), ItClock.today(), - ItClock.today(), STRING.next(), STRING.next(), STRING.next(), - OFFENCE_COUNT, OFFENCE_ORDER_INDEX, OFFENCE_LEGISLATION, randomUUID(), Optional.of(randomCustodyTimeLimit()), Optional.of(randomLaaReferenceData()), ItClock.today(), of(Boolean.FALSE), manyRandomReportingRestriction(2), STRING.next(), civilOffenceData); + return new OffenceData(offence.getOffenceId(), STRING.next(), LocalDate.now(), + LocalDate.now(), STRING.next(), STRING.next(), STRING.next(), + OFFENCE_COUNT, OFFENCE_ORDER_INDEX, OFFENCE_LEGISLATION, randomUUID(), Optional.of(randomCustodyTimeLimit()), Optional.of(randomLaaReferenceData()), LocalDate.now(), of(Boolean.FALSE), manyRandomReportingRestriction(2), STRING.next(), civilOffenceData); } private static OffenceData randomOffenceWithoutReportingRestriction() { final CivilOffenceData civilOffenceData = new CivilOffenceData(BOOLEAN.next()); - return new OffenceData(randomUUID(), STRING.next(), ItClock.today(), - ItClock.today(), STRING.next(), STRING.next(), STRING.next(), - OFFENCE_COUNT, OFFENCE_ORDER_INDEX, OFFENCE_LEGISLATION, randomUUID(), Optional.of(randomCustodyTimeLimit()), Optional.of(randomLaaReferenceData()), ItClock.today(), of(Boolean.FALSE), null, STRING.next(), civilOffenceData); + return new OffenceData(randomUUID(), STRING.next(), LocalDate.now(), + LocalDate.now(), STRING.next(), STRING.next(), STRING.next(), + OFFENCE_COUNT, OFFENCE_ORDER_INDEX, OFFENCE_LEGISLATION, randomUUID(), Optional.of(randomCustodyTimeLimit()), Optional.of(randomLaaReferenceData()), LocalDate.now(), of(Boolean.FALSE), null, STRING.next(), civilOffenceData); } private static OffenceData randomOffenceWithExParteOffenceListedCase() { final CivilOffenceData civilOffenceData = new CivilOffenceData(true); - return new OffenceData(randomUUID(), STRING.next(), ItClock.today(), - ItClock.today(), STRING.next(), STRING.next(), STRING.next(), - OFFENCE_COUNT, OFFENCE_ORDER_INDEX, OFFENCE_LEGISLATION, randomUUID(), Optional.of(randomCustodyTimeLimit()), Optional.of(randomLaaReferenceData()), ItClock.today(), of(Boolean.FALSE), null, STRING.next(), civilOffenceData); + return new OffenceData(randomUUID(), STRING.next(), LocalDate.now(), + LocalDate.now(), STRING.next(), STRING.next(), STRING.next(), + OFFENCE_COUNT, OFFENCE_ORDER_INDEX, OFFENCE_LEGISLATION, randomUUID(), Optional.of(randomCustodyTimeLimit()), Optional.of(randomLaaReferenceData()), LocalDate.now(), of(Boolean.FALSE), null, STRING.next(), civilOffenceData); } private static List manyRandomReportingRestriction(final Integer numberOfReportingRestrictions) { @@ -890,7 +832,7 @@ private static List manyRandomReportingRestriction(fin } private static ReportingRestrictionData randomReportingRestriction() { - return new ReportingRestrictionData(randomUUID(), Optional.of(JUDICIAL_RESULT_ID), "RestrictionApplied", Optional.of(ItClock.today())); + return new ReportingRestrictionData(randomUUID(), Optional.of(JUDICIAL_RESULT_ID), "RestrictionApplied", Optional.of(LocalDate.now())); } private static CustodyTimeLimit randomCustodyTimeLimit() { @@ -899,94 +841,80 @@ private static CustodyTimeLimit randomCustodyTimeLimit() { private static DefendantData randomDefendant() { return new DefendantData(randomUUID(), STRING.next(), STRING.next(), - ItClock.today().minusYears(21), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), + LocalDate.now(), LocalDate.now(), BAIL_CONDITIONAL, STRING.next(), manyRandomOffences(3), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), STRING.next()); - } - - private static DefendantData randomYoungDefendant() { - return new DefendantData(randomUUID(), STRING.next(), STRING.next(), - ItClock.today().minusYears(15), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), - manyRandomOffences(3), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), STRING.next()); - } - - private static DefendantData randomAdultDefendant() { - return new DefendantData(randomUUID(), STRING.next(), STRING.next(), - ItClock.today().minusYears(25), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), - manyRandomOffences(3), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), STRING.next()); + Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ZonedDateTime.now(), STRING.next()); } private static DefendantData randomDefendant(final int numberOfOffences) { return new DefendantData(randomUUID(), STRING.next(), STRING.next(), - ItClock.today(), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), + LocalDate.now(), LocalDate.now(), BAIL_CONDITIONAL, STRING.next(), manyRandomOffences(numberOfOffences), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), STRING.next()); + Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ZonedDateTime.now(), STRING.next()); } private static DefendantData randomDefendant(final List offences) { return new DefendantData(randomUUID(), STRING.next(), STRING.next(), - ItClock.today(), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), + LocalDate.now(), LocalDate.now(), BAIL_CONDITIONAL, STRING.next(), manyRandomOffences(offences), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), STRING.next()); + Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ZonedDateTime.now(), STRING.next()); } private static DefendantData randomDefendantWithoutReportingRestriction() { return new DefendantData(randomUUID(), STRING.next(), STRING.next(), - ItClock.today(), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), + LocalDate.now(), LocalDate.now(), BAIL_CONDITIONAL, STRING.next(), manyRandomOffencesWithoutReportingRestriction(3), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), STRING.next()); + Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ZonedDateTime.now(), STRING.next()); } private static DefendantData randomDefendantWithExParteOffenceListedCase() { return new DefendantData(randomUUID(), STRING.next(), STRING.next(), - ItClock.today(), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), + LocalDate.now(), LocalDate.now(), BAIL_CONDITIONAL, STRING.next(), manyRandomOffencesWithExParteOffenceListedCase(3), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), STRING.next()); + Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ZonedDateTime.now(), STRING.next()); } private static DefendantData randomDefendantSingleOffence() { return new DefendantData(randomUUID(), STRING.next(), STRING.next(), - ItClock.today(), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), + LocalDate.now(), LocalDate.now(), BAIL_CONDITIONAL, STRING.next(), manyRandomOffences(1), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), STRING.next()); + Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ZonedDateTime.now(), STRING.next()); } private static DefendantData randomDefendantWithGivenNumberOfOffences(final Integer offence) { return new DefendantData(randomUUID(), STRING.next(), STRING.next(), - ItClock.today(), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), + LocalDate.now(), LocalDate.now(), BAIL_CONDITIONAL, STRING.next(), manyRandomOffences(offence), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), STRING.next()); + Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ZonedDateTime.now(), STRING.next()); } private static DefendantData randomDefendantMultipleOffences() { return new DefendantData(randomUUID(),STRING.next(), STRING.next(), - ItClock.today(), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), + LocalDate.now(), LocalDate.now(), BAIL_CONDITIONAL, STRING.next(), manyRandomOffences(2), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), STRING.next()); + Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ZonedDateTime.now(), STRING.next()); } private static DefendantData randomDefendant(final CaseAndDefendantData caseAndDefendantData) { final UUID masterDefendantId = caseAndDefendantData.getMasterDefendantId(); return new DefendantData(masterDefendantId, STRING.next(), STRING.next(), - ItClock.today(), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), + LocalDate.now(), LocalDate.now(), BAIL_CONDITIONAL, STRING.next(), manyRandomOffences(1), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, masterDefendantId, ItClock.nowUtc(), STRING.next()); + Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, masterDefendantId, ZonedDateTime.now(), STRING.next()); } private static DefendantData randomDefendantWithLegalEntityDefendant() { return new DefendantData(randomUUID(), STRING.next(), STRING.next(), - ItClock.today(), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), + LocalDate.now(), LocalDate.now(), BAIL_CONDITIONAL, STRING.next(), manyRandomOffences(1), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - false, Boolean.FALSE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), STRING.next()); + false, Boolean.FALSE, Boolean.FALSE, randomUUID(), ZonedDateTime.now(), STRING.next()); } private static DefendantData randomDefendantWithListingReason(final String listingReason) { return new DefendantData(randomUUID(), STRING.next(), STRING.next(), - ItClock.today(), ItClock.today(), BAIL_CONDITIONAL, STRING.next(), + LocalDate.now(), LocalDate.now(), BAIL_CONDITIONAL, STRING.next(), manyRandomOffences(3), new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ItClock.nowUtc(), listingReason); + Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, randomUUID(), ZonedDateTime.now(), listingReason); } private static CaseMarkerData randomCaseMarker() { @@ -996,53 +924,8 @@ private static CaseMarkerData randomCaseMarker() { private static HearingData randomHearing() { return randomHearing(null, null, null); } - - private static HearingData randomHearingWithYoungDefendants() { - final List listedCaseData = manyRandomListedCasesWithYoungDefendants(2); - return new HearingData(randomUUID(), UUID.fromString("b52f805c-2821-4904-a0e0-26f7fda6dd08"), PTP_HEARING_TYPE, ItClock.today(), - null, HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, - null, ItClock.nowUtc(), listedCaseData, - null, CROWN_JURISDICTION, - STRING.next(), - singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court"); - } - - private static HearingData randomHearingWithYoungCourtApplicationRespondent() { - final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), UUID.fromString("b52f805c-2821-4904-a0e0-26f7fda6dd08"), PTP_HEARING_TYPE, ItClock.today(), - null, HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, - null, ItClock.nowUtc(), listedCaseData, - null, CROWN_JURISDICTION, - STRING.next(), - singletonList(randomCourtApplicationDataWithYoungRespondent(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court"); - } - - private static HearingData randomHearingWithYoungCourtApplicationSubject() { - final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), UUID.fromString("b52f805c-2821-4904-a0e0-26f7fda6dd08"), PTP_HEARING_TYPE, ItClock.today(), - null, HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, - null, ItClock.nowUtc(), listedCaseData, - null, CROWN_JURISDICTION, - STRING.next(), - singletonList(randomCourtApplicationDataWithYoungSubject(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court"); - } - - private static HearingData randomHearingWithAdultDefendants() { - final List listedCaseData = manyRandomListedCasesWithAdultDefendants(2); - return new HearingData(randomUUID(), UUID.fromString("b52f805c-2821-4904-a0e0-26f7fda6dd08"), PTP_HEARING_TYPE, ItClock.today(), - null, HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, - null, ItClock.nowUtc(), listedCaseData, - null, CROWN_JURISDICTION, - STRING.next(), - singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court"); - } - private static HearingData randomHearingWithExParte() { - return randomHearingWithExParte(ItClock.today(), getRandomCourtRoomId(), null); + return randomHearingWithExParte(LocalDate.now(), getRandomCourtRoomId(), null); } private static HearingData notHmiEnabledRandomHearing(){ @@ -1129,9 +1012,9 @@ private static HearingData randomHearing(final LocalDate hearingEndDate, final U private static HearingData randomHearing(final UUID courtCentreId, final LocalDate hearingEndDate, final UUID courtRoomId, final List judicialRoles) { final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), @@ -1139,9 +1022,9 @@ private static HearingData randomHearing(final UUID courtCentreId, final LocalDa } private static HearingData randomHearingWithExParte(final UUID courtCentreId, final LocalDate hearingEndDate, final UUID courtRoomId, final List judicialRoles) { final List listedCaseData = manyRandomListingCasesWithExParteOffenceListedCase(); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), @@ -1150,9 +1033,9 @@ private static HearingData randomHearingWithExParte(final UUID courtCentreId, fi private static HearingData randomHearing(final UUID courtCentreId, final UUID courtRoomId, final List judicialRoles, final String jurisdictionType) { final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), - ItClock.today().plusDays(3), HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, - courtRoomId, ItClock.nowUtc(), listedCaseData, + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now().plusDays(3), HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, isBlank(jurisdictionType) ? CROWN_JURISDICTION : jurisdictionType, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), @@ -1161,9 +1044,9 @@ judicialRoles, isBlank(jurisdictionType) ? CROWN_JURISDICTION : jurisdictionType private static HearingData randomHearingForHMI(final UUID courtCentreId, final LocalDate hearingEndDate, final UUID courtRoomId, final List judicialRoles) { final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, - null, ItClock.nowUtc(), listedCaseData, + null, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), @@ -1172,9 +1055,9 @@ private static HearingData randomHearingForHMI(final UUID courtCentreId, final L private static HearingData randomHearing(final HearingData hearingData, final UUID courtCentreId, final LocalDate hearingEndDate, final UUID courtRoomId, final List judicialRoles) { final List listedCaseData = manyRandomListingCases(hearingData.getListedCases()); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(hearingData.getCourtApplications().get(0), listedCaseData.get(0).getCaseId())), @@ -1183,29 +1066,29 @@ private static HearingData randomHearing(final HearingData hearingData, final UU private static HearingData randomHearingWithGivenNumberDefendantAndOffencesForFixedDateHearing(final UUID courtCentreId, final UUID courtRoomUUID, LocalDate hearingEndDate, final List judicialRoles, String court, final Integer numberOfCases, final Integer numberOfDefendants, final Integer numberOfOffences) { final List listedCaseData = manyRandomListingCasesWithGivenDefendantAndOffences(numberOfCases, numberOfDefendants, numberOfOffences); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), - ItClock.today(), HEARING_ESTIMATE_MINUTES, - courtRoomUUID, ItClock.nowUtc().truncatedTo(ChronoUnit.HOURS), listedCaseData, + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now(), HEARING_ESTIMATE_MINUTES, + courtRoomUUID, ZonedDateTime.now().truncatedTo(ChronoUnit.HOURS), listedCaseData, judicialRoles, MAGISTRATES_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), court, ItClock.today().toString()); + singletonList(randomCourtApplicationPartyNeed()), court, LocalDate.now().toString()); } private static HearingData randomHearingSingleOffenceForFixedDateHearing(final UUID courtCentreId, final UUID courtRoomUUID, LocalDate hearingEndDate, final List judicialRoles, String court, final Integer numberOfCases) { final List listedCaseData = manyRandomListingCasesSingleOffence(numberOfCases); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), - ItClock.today(), HEARING_ESTIMATE_MINUTES, - courtRoomUUID, ItClock.nowUtc().truncatedTo(ChronoUnit.HOURS), listedCaseData, + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now(), HEARING_ESTIMATE_MINUTES, + courtRoomUUID, ZonedDateTime.now().truncatedTo(ChronoUnit.HOURS), listedCaseData, judicialRoles, MAGISTRATES_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), court, ItClock.today().toString()); + singletonList(randomCourtApplicationPartyNeed()), court, LocalDate.now().toString()); } private static HearingData randomHearingSingleOffence(final UUID courtCentreId, final LocalDate hearingEndDate, final UUID courtRoomId, final List judicialRoles) { final List listedCaseData = manyRandomListingCasesSingleOffence(2); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), @@ -1214,9 +1097,9 @@ private static HearingData randomHearingSingleOffence(final UUID courtCentreId, private static HearingData randomHearingMultipleOffences() { final List listedCaseData = manyRandomListingCasesMultipleOffences(1); - return new HearingData(randomUUID(), getRandomCourtCenterId(), PTP_HEARING_TYPE, ItClock.today(), - ItClock.today().plusDays(1), HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, - null, ItClock.nowUtc(), listedCaseData, + return new HearingData(randomUUID(), getRandomCourtCenterId(), PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now().plusDays(1), HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, + null, ZonedDateTime.now(), listedCaseData, null, MAGISTRATES_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), @@ -1225,9 +1108,9 @@ private static HearingData randomHearingMultipleOffences() { private static HearingData randomHearingMultipleDefendants() { final List listedCaseData = manyRandomListingCasesMultipleDefendantsOffences(1); - return new HearingData(randomUUID(), getRandomCourtCenterId(), PTP_HEARING_TYPE, ItClock.today(), - ItClock.today().plusDays(1), HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, - null, ItClock.nowUtc(), listedCaseData, + return new HearingData(randomUUID(), getRandomCourtCenterId(), PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now().plusDays(1), HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, + null, ZonedDateTime.now(), listedCaseData, null, MAGISTRATES_JURISDICTION, STRING.next(), null, @@ -1236,9 +1119,9 @@ private static HearingData randomHearingMultipleDefendants() { private static HearingData randomHearing(final UUID courtCentreId, final UUID courtRoomId, final List judicialRoles, final HearingTypeData trialHearingType) { final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), courtCentreId, trialHearingType, ItClock.today(), - ItClock.today().plusDays(3), HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, - courtRoomId, ItClock.nowUtc(), listedCaseData, + return new HearingData(randomUUID(), courtCentreId, trialHearingType, LocalDate.now(), + LocalDate.now().plusDays(3), HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), @@ -1247,9 +1130,9 @@ private static HearingData randomHearing(final UUID courtCentreId, final UUID co private static HearingData randomHearingWithPossibleDisqualification() { final List listedCaseData = manyRandomListingCases(2, "For disqualification"); - return new HearingData(randomUUID(), getRandomCourtCenterId(), PTP_HEARING_TYPE, ItClock.today(), - ItClock.today().plusDays(3), HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, - null, ItClock.nowUtc(), listedCaseData, + return new HearingData(randomUUID(), getRandomCourtCenterId(), PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now().plusDays(3), HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, + null, ZonedDateTime.now(), listedCaseData, null, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), @@ -1258,9 +1141,9 @@ private static HearingData randomHearingWithPossibleDisqualification() { private static HearingData randomHearingWithWeekCommencingDates(final LocalDate hearingEndDate, final UUID courtCenterId, final UUID courtRoomId, final List judicialRoles, final LocalDate weekCommencingStartDate, final Integer weekCommencingDuration) { final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), courtCenterId, STRING.next(), PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(randomUUID(), courtCenterId, STRING.next(), PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), singletonList(randomCourtApplicationPartyNeed()), @@ -1269,20 +1152,9 @@ private static HearingData randomHearingWithWeekCommencingDates(final LocalDate private static HearingData randomHearingWithWeekCommencingDates(final LocalDate hearingEndDate, final UUID courtRoomId, final List judicialRoles, final LocalDate weekCommencingStartDate, final Integer weekCommencingDuration) { final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), getRandomCourtCenterId(), STRING.next(), PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(randomUUID(), getRandomCourtCenterId(), STRING.next(), PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES, - courtRoomId, ItClock.nowUtc(), listedCaseData, - judicialRoles, CROWN_JURISDICTION, STRING.next(), - singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), - weekCommencingStartDate, null, weekCommencingDuration); - } - - private static HearingData randomHearingWithWeekCommencingDatesAndYoungDefendants(final LocalDate hearingEndDate, final UUID courtRoomId, final List judicialRoles, final LocalDate weekCommencingStartDate, final Integer weekCommencingDuration) { - final List listedCaseData = manyRandomListedCasesWithYoungDefendants(2); - return new HearingData(randomUUID(), getRandomCourtCenterId(), STRING.next(), PTP_HEARING_TYPE, ItClock.today(), - hearingEndDate, HEARING_ESTIMATE_MINUTES, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), singletonList(randomCourtApplicationPartyNeed()), @@ -1303,9 +1175,9 @@ private static HearingData createRandomHearingWithBookedSlot(final LocalDate hea .withStartTime(SLOT_START_TIME) .build()); - return new HearingData(randomUUID(), getRandomCourtCenterId(), STRING.next(), PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(randomUUID(), courtCentreId, STRING.next(), PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), singletonList(randomCourtApplicationPartyNeed()), @@ -1314,80 +1186,80 @@ private static HearingData createRandomHearingWithBookedSlot(final LocalDate hea private static HearingData randomHearingWithAdjournmentFromDate(final UUID courtCentreId, final UUID courtRoomId, final List judicialRoles) { final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), - ItClock.today(), HEARING_ESTIMATE_MINUTES, - courtRoomId, ItClock.nowUtc().truncatedTo(ChronoUnit.HOURS), listedCaseData, + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now(), HEARING_ESTIMATE_MINUTES, + courtRoomId, ZonedDateTime.now().truncatedTo(ChronoUnit.HOURS), listedCaseData, judicialRoles, MAGISTRATES_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", ItClock.today().toString()); + singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", LocalDate.now().toString()); } private static HearingData randomHearingWithAdjournmentFromDate_withSingleCivilCase(final UUID courtCentreId, final UUID courtRoomId, final List judicialRoles) { final List listedCaseData = manyRandomListingSingleCivilCases(2); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), - ItClock.today(), HEARING_ESTIMATE_MINUTES, - courtRoomId, ItClock.nowUtc().truncatedTo(ChronoUnit.HOURS), listedCaseData, + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now(), HEARING_ESTIMATE_MINUTES, + courtRoomId, ZonedDateTime.now().truncatedTo(ChronoUnit.HOURS), listedCaseData, judicialRoles, MAGISTRATES_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", ItClock.today().toString()); + singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", LocalDate.now().toString()); } private static HearingData randomHearingWithAdjournmentFromDateSingleCase(final UUID courtCentreId, final List judicialRoles) { final List listedCaseData = manyRandomListingCasesSingleDefendant(1); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), - ItClock.today(), HEARING_ESTIMATE_MINUTES, - randomUUID(), ItClock.nowUtc().truncatedTo(ChronoUnit.HOURS), listedCaseData, + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now(), HEARING_ESTIMATE_MINUTES, + randomUUID(), ZonedDateTime.now().truncatedTo(ChronoUnit.HOURS), listedCaseData, judicialRoles, MAGISTRATES_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", ItClock.today().toString()); + singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", LocalDate.now().toString()); } private static HearingData randomHearingWithAdjournmentFromDateWithParameters(final UUID courtCentreId, final UUID courtRoomId, final List judicialRoles) { final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), - ItClock.today(), HEARING_ESTIMATE_MINUTES, - courtRoomId, ItClock.nowUtc().truncatedTo(ChronoUnit.HOURS), listedCaseData, + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now(), HEARING_ESTIMATE_MINUTES, + courtRoomId, ZonedDateTime.now().truncatedTo(ChronoUnit.HOURS), listedCaseData, judicialRoles, MAGISTRATES_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", ItClock.today().toString()); + singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", LocalDate.now().toString()); } private static HearingData randomHearingWithAdjournmentFromDateWithCourt(final UUID courtCentreId, final UUID courtRoomUUID, final List judicialRoles, String court) { final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), courtCentreId, court, PTP_HEARING_TYPE, ItClock.today(), - ItClock.today().plusDays(7), HEARING_ESTIMATE_MINUTES, - randomUUID(), ItClock.nowUtc(), listedCaseData, + return new HearingData(randomUUID(), courtCentreId, court, PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now().plusDays(7), HEARING_ESTIMATE_MINUTES, + randomUUID(), ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), singletonList(randomCourtApplicationPartyNeed()), - ItClock.today(), null, 7); + LocalDate.now(), null, 7); } private static HearingData randomHearingWithAdjournmentFromDate(final UUID courtCentreId, final UUID courtRoomId, final List judicialRoles, final String jurisdictionType) { final List listedCaseData = manyRandomListingCases(2); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), - ItClock.today(), HEARING_ESTIMATE_MINUTES, - courtRoomId, ItClock.nowUtc(), listedCaseData, + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now(), HEARING_ESTIMATE_MINUTES, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, jurisdictionType, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", ItClock.today().toString()); + singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", LocalDate.now().toString()); } private static HearingData randomHearingWithoutReportingRestriction(final UUID courtCentreId, final UUID courtRoomId, final List judicialRoles, final String jurisdictionType) { final List listedCaseData = manyRandomListingCasesWithoutReportingRestriction(2); - return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), - ItClock.today(), HEARING_ESTIMATE_MINUTES, - courtRoomId, ItClock.nowUtc(), listedCaseData, + return new HearingData(randomUUID(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), + LocalDate.now(), HEARING_ESTIMATE_MINUTES, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, jurisdictionType, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), - singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", ItClock.today().toString()); + singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court", LocalDate.now().toString()); } private static HearingData randomHearing(final UUID courtCentreId, final LocalDate hearingEndDate, final UUID courtRoomId, final List judicialRoles, final UUID hearingId) { final List listedCaseData = manyRandomListingCases(2); - return new HearingData(hearingId, courtCentreId, PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(hearingId, courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court"); @@ -1395,9 +1267,9 @@ private static HearingData randomHearing(final UUID courtCentreId, final LocalDa private static HearingData randomHearing(final LocalDate hearingEndDate, final UUID courtCentreId, final UUID courtRoomId, final List judicialRoles, final CaseAndDefendantData caseAndDefendantData) { final List listedCaseData = manyRandomListingCases(1, caseAndDefendantData); - return new HearingData(caseAndDefendantData.getHearingId(), courtCentreId, PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(caseAndDefendantData.getHearingId(), courtCentreId, PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, caseAndDefendantData.getJurisdictionType(), STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court"); @@ -1433,9 +1305,9 @@ judicialRoles, isBlank(jurisdictionType) ? CROWN_JURISDICTION : jurisdictionType private static HearingData randomUnAllocatedHearing(final LocalDate hearingEndDate, final UUID courtRoomId, final List judicialRoles, final CaseAndDefendantData caseAndDefendantData) { final List listedCaseData = manyRandomListingCases(1, caseAndDefendantData); - return new HearingData(caseAndDefendantData.getHearingId(), getRandomCourtCenterId(), PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(caseAndDefendantData.getHearingId(), getRandomCourtCenterId(), PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, - null, ItClock.nowUtc(), listedCaseData, + null, ZonedDateTime.now(), listedCaseData, judicialRoles, caseAndDefendantData.getJurisdictionType(), STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court"); @@ -1445,15 +1317,15 @@ private static HearingData randomHearingForWeekCommencingDate(final UUID hearing final List listedCaseData = manyRandomListingCases(2); return weekCommencingStartDate == null ? - new HearingData(hearingId, getRandomCourtCenterId(), PTP_HEARING_TYPE, ItClock.today(), + new HearingData(hearingId, getRandomCourtCenterId(), PTP_HEARING_TYPE, now(), hearingEndDate, HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), singletonList(randomCourtApplicationPartyNeed()), STRING.next()) : new HearingData(hearingId, getRandomCourtCenterId(), STRING.next(), PTP_HEARING_TYPE, startDate, hearingEndDate, HEARING_ESTIMATE_MINUTES, - courtRoomId, ItClock.nowUtc(), listedCaseData, + courtRoomId, ZonedDateTime.now(), listedCaseData, judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationData(listedCaseData.get(0).getCaseId())), singletonList(randomCourtApplicationPartyNeed()), @@ -1461,9 +1333,9 @@ private static HearingData randomHearingForWeekCommencingDate(final UUID hearing } private static HearingData randomHearingWithLegalEntity(final LocalDate hearingEndDate, final UUID courtRoomId, final List judicialRoles) { - return new HearingData(randomUUID(), getRandomCourtCenterId(), PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(randomUUID(), getRandomCourtCenterId(), PTP_HEARING_TYPE, LocalDate.now(), hearingEndDate, HEARING_ESTIMATE_MINUTES,ESTIMATED_DURATION, - courtRoomId, ItClock.nowUtc(), manyRandomListingCasesWithLegalEntity(1), + courtRoomId, ZonedDateTime.now(), manyRandomListingCasesWithLegalEntity(1), judicialRoles, CROWN_JURISDICTION, STRING.next(), singletonList(randomCourtApplicationDataWithLegalEntity(randomUUID())), singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court"); @@ -1473,9 +1345,9 @@ private static HearingData randomHearingStandaloneApplication(final boolean with final CourtApplicationData courtApplicationData = withSubject ? randomCourtApplicationDataWithSubject(null) : randomCourtApplicationData(null); - return new HearingData(randomUUID(), getRandomCourtCenterId(), PTP_HEARING_TYPE, ItClock.today(), + return new HearingData(randomUUID(), getRandomCourtCenterId(), PTP_HEARING_TYPE, LocalDate.now(), null, HEARING_ESTIMATE_MINUTES, ESTIMATED_DURATION, - null, ItClock.nowUtc(), null, + null, ZonedDateTime.now(), null, null, CROWN_JURISDICTION, STRING.next(), singletonList(courtApplicationData), singletonList(randomCourtApplicationPartyNeed()), "Carmarthen Magistrates Court"); @@ -1503,27 +1375,6 @@ private static CourtApplicationData randomCourtApplicationData(final CourtApplic STRING.next(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, STRING.next(), courtApplicationData.getOffenceId()); } - private static CourtApplicationPartyData randomYoungCourtApplicationPartyData(final boolean isRespondent) { - return new CourtApplicationPartyData(randomUUID(), STRING.next(), isRespondent, STRING.next(), - CourtApplicationPartyType.PERSON, null, randomAddress(), - randomUUID(), ItClock.today().minusYears(15)); - } - - private static CourtApplicationData randomCourtApplicationDataWithYoungRespondent(final UUID linkedCaseId) { - return new CourtApplicationData(randomUUID(), linkedCaseId, randomUUID(), - new CourtApplicationPartyData(randomUUID(), STRING.next(), Boolean.FALSE, STRING.next(), CourtApplicationPartyType.PERSON, null, randomAddress()), - randomYoungCourtApplicationPartyData(Boolean.TRUE), - STRING.next(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, STRING.next(), randomUUID()); - } - - private static CourtApplicationData randomCourtApplicationDataWithYoungSubject(final UUID linkedCaseId) { - return new CourtApplicationData(randomUUID(), linkedCaseId, randomUUID(), - new CourtApplicationPartyData(randomUUID(), STRING.next(), Boolean.FALSE, STRING.next(), CourtApplicationPartyType.PERSON, null, randomAddress()), - new CourtApplicationPartyData(randomUUID(), STRING.next(), Boolean.TRUE, STRING.next(), CourtApplicationPartyType.PERSON, null, randomAddress()), - randomYoungCourtApplicationPartyData(Boolean.FALSE), - STRING.next(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, STRING.next(), randomUUID()); - } - private static CourtApplicationData randomCourtApplicationDataWithLegalEntity(final UUID linkedCaseId) { return new CourtApplicationData(randomUUID(), linkedCaseId, randomUUID(), new CourtApplicationPartyData(randomUUID(), STRING.next(), Boolean.FALSE, STRING.next(), CourtApplicationPartyType.PERSON_DEFENDANT, new LegalEntityDefendantData(UUID.randomUUID(), getOrganisationData()), randomAddress()), diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java index bfca77eb2..25403187f 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/CourtSchedulerServiceStub.java @@ -44,7 +44,6 @@ import uk.gov.moj.cpp.listing.steps.data.JudicialRoleData; import uk.gov.moj.cpp.listing.steps.data.NonDefaultDayData; import uk.gov.moj.cpp.listing.steps.data.UpdatedHearingData; -import uk.gov.moj.cpp.listing.it.util.ItClock; public class CourtSchedulerServiceStub { @@ -53,16 +52,17 @@ public class CourtSchedulerServiceStub { private static final String PROVISIONAL_BOOKING = "/provisionalBooking"; private static final String HEARING_SLOTS = "/hearingslots"; + private static final String JUDICIARIES_SEARCH_AVAILABLE = "/judiciaries/search-available"; private static final String VALIDATE_SESSION_AVAILABILITY = "/validate-session-availability"; - private static final String SEARCH_COURT_SCHEDULES_BY_ID = "/courtschedule/search.court-schedules-by-id"; - private static final String CROWN_FALLBACK_SEARCH_BOOK = "/crownfallbacksearchandbook/hearingslots"; - private static final String CROWN_FALLBACK_SEARCH_BOOK_TYPE = "application/vnd.courtscheduler.crown.fallback.search.book.hearing.slots+json"; private static final String COURTSCHEDULER_GET_HEARING_SLOTS_TYPE = "application/vnd.courtscheduler.get.hearing.slots+json"; private static final String COURTSCHEDULER_VALIDATE_SESSION_AVAILABILITY_TYPE = "application/vnd.courtscheduler.validate.session.availability+json"; + private static final String COURTSCHEDULER_SEARCH_AVAILABLE_JUDICIARIES_TYPE = + "application/vnd.courtscheduler.search.available.judiciaries+json"; public static final String COURTSCHEDULER_GET_PROVISIONAL_BOOKING_TYPE = "application/vnd.courtscheduler.get.provisional.booking+json"; public static final String ROTASL_GET_HEARING_SLOTS_RESPONSE_JSON_WITH_JUDICIARIES = "stub-data/rotasl.get.hearing.slots.with-judiciaries.json"; public static final String LISTING_GET_HEARING_SLOTS_RESPONSE_JSON_WITH_JUDICIARIES_AND_SLOTTIMES = "stub-data/listing.get.hearing.slots.with-judiciaries-and-slotstarttimes.json"; public static final String LISTING_SEARCH_HEARING_SLOTS_JSON = "stub-data/listing.search.hearing.slots.json"; + public static final String LISTING_SEARCH_AVAILABLE_JUDICIARIES_JSON = "stub-data/listing.search.available.judiciaries.json"; public static final String LISTING_SEARCH_HEARING_EMPTY_SLOTS_JSON = "stub-data/listing.search.hearing.slots.empty.json"; public static final String STUB_DATA_PROVISIONAL_BOOKING_SAMPLE_DATA_SINGLE_COURT_SCHEDULE_COUNT_BASED_JSON = "stub-data/provisionalBookingSampleDataSingleCourtScheduleCountBased.json"; public static final String STUB_DATA_PROVISIONAL_BOOKING_SAMPLE_DATA_MULTIPLE_COURT_SCHEDULES_COUNT_BASED_JSON = "stub-data/provisionalBookingSampleDataMultipleCourtSchedulesCountBased.json"; @@ -112,19 +112,6 @@ private static void verifyDeleteAvailableHearingSlotsStubCommandInvokedNTimes(fi }); } - public static void verifyHearingSlotsSearchCalledWithJurisdiction(final String jurisdiction) { - Awaitility.await().atMost(15, SECONDS).pollInterval(POLL_INTERVAL).until(() -> { - final RequestPatternBuilder requestPatternBuilder = WireMock.getRequestedFor(urlPathMatching(COURT_SCHEDULER_ENDPOINT + HEARING_SLOTS)) - .withQueryParam("jurisdiction", WireMock.equalTo(jurisdiction)); - try { - WireMock.verify(WireMock.moreThanOrExactly(1), requestPatternBuilder); - } catch (VerificationException e) { - return false; - } - return true; - }); - } - public static void stubValidateSessionAvailability() { stubFor(post(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + VALIDATE_SESSION_AVAILABILITY))) .withHeader("Content-Type", containing(COURTSCHEDULER_VALIDATE_SESSION_AVAILABILITY_TYPE)) @@ -136,340 +123,31 @@ public static void stubValidateSessionAvailability() { )); } - /** - * Stub a successful response from /courtschedule/search.court-schedules-by-id for the given - * courtScheduleId. Returned wire shape mirrors what the real courtscheduler emits via - * {@code CourtSchedulerApi.searchCourtSchedulesById} - FLAT: each courtSchedules[] element - * is a single CourtSchedule with isDraft at the top level. The schema example in - * courtscheduler-api shows a misleading nested "sessions" structure copied from a different - * endpoint; never match the schema shape, match the wire shape. - * - * @param courtScheduleId the id under query - * @param isDraft draft state to report - drives whether - * {@code listing.query.court.schedule.draft.status} returns - * {@code anyDraft=true} (strip) or {@code anyDraft=false} (preserve) - */ - /** - * Stub courtscheduler's search-court-schedules-by-id response with an explicit choice of - * draft-field name. Real-world Jackson serialisation of CourtSchedule emits one of: - * - {@code "isDraft": } (from the setter convention) - * - {@code "draft": } (from the boolean-getter "is" prefix stripping) - * The parser must accept either, so tests assert both. - * - * @param courtScheduleId the id under query - * @param draftKey wire field name to emit - either "isDraft" or "draft" - * @param draft value for that field - */ - public static void stubSearchCourtSchedulesByIdWithKey(final String courtScheduleId, - final String draftKey, - final boolean draft) { - final String body = "{\"courtSchedules\":[{\"courtScheduleId\":\"" + courtScheduleId - + "\",\"" + draftKey + "\":" + draft + "}]}"; - stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + SEARCH_COURT_SCHEDULES_BY_ID))) - .willReturn(aResponse().withStatus(OK.getStatusCode()) - .withBody(body) + public static void stubValidateSessionAvailabilityFailure() { + stubFor(post(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + VALIDATE_SESSION_AVAILABILITY))) + .withHeader("Content-Type", containing(COURTSCHEDULER_VALIDATE_SESSION_AVAILABILITY_TYPE)) + .willReturn(aResponse().withStatus(BAD_REQUEST.getStatusCode()) + .withBody("{\"validationResult\":{\"status\":\"FAILURE\",\"validationError\":\"duration is required\"}}") .withHeader(CONTENT_TYPE, APPLICATION_JSON) )); } - /** - * Stub /courtschedule/search.court-schedules-by-id to return a 500. Exercises the listing - * adapter's fail-closed path (anyDraft=true on courtscheduler error). - */ - public static void stubSearchCourtSchedulesByIdServerError() { - stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + SEARCH_COURT_SCHEDULES_BY_ID))) - .willReturn(aResponse().withStatus(500) - .withBody("internal server error") + public static void stubSearchAvailableJudiciaries() { + stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + JUDICIARIES_SEARCH_AVAILABLE))) + .withQueryParam("search", matching("ai")) + .withHeader("Accept", containing(COURTSCHEDULER_SEARCH_AVAILABLE_JUDICIARIES_TYPE)) + .willReturn(aResponse().withStatus(OK.getStatusCode()) + .withBody(getPayload(LISTING_SEARCH_AVAILABLE_JUDICIARIES_JSON)) .withHeader(CONTENT_TYPE, APPLICATION_JSON) )); } - /** - * Stub {@code search.court-schedules-by-id} so a CROWN bookingReference (which IS the courtScheduleId) - * resolves to a single session echoing the supplied courtHouse / room / date. The listing command resolves - * the bookingReference here (see {@code CourtScheduleEnrichmentService.promoteCrownBookingReferenceToBookedSlot}). - * Scoped by the {@code courtScheduleIds} query param so it answers only for this hearing's bookingReference - * and never pollutes other tests (WireMock stubs persist across IT classes in a suite). - */ - public static void stubSearchCourtSchedulesByIdSession(final String courtScheduleId, - final UUID courtHouseId, - final UUID courtRoomId, - final LocalDate sessionDate, - final ZonedDateTime hearingStartTime, - final boolean isDraft) { - final StringBuilder session = new StringBuilder(); - session.append("{\"courtScheduleId\":\"").append(courtScheduleId).append("\""); - if (courtHouseId != null) { - session.append(",\"courtHouseId\":\"").append(courtHouseId).append("\""); - } - if (courtRoomId != null) { - session.append(",\"courtRoomId\":\"").append(courtRoomId).append("\""); - } - if (sessionDate != null) { - session.append(",\"sessionDate\":\"").append(sessionDate).append("\""); - } - if (hearingStartTime != null) { - session.append(",\"hearingStartTime\":\"").append(hearingStartTime).append("\""); - } - session.append(",\"isDraft\":").append(isDraft).append("}"); - final String body = "{\"courtSchedules\":[" + session + "]}"; - - stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + SEARCH_COURT_SCHEDULES_BY_ID))) - .atPriority(2) - .withQueryParam("courtScheduleIds", containing(courtScheduleId)) - .willReturn(aResponse().withStatus(OK.getStatusCode()) - .withBody(body) - .withHeader(CONTENT_TYPE, APPLICATION_JSON))); - } - - // --- Crown fallback search-and-book stubs (Option C: courtCentreId-only wire) --- - - /** - * Stub a successful 200 response from /crownfallbacksearchandbook/hearingslots. - * - * @param hearingId hearingId echoed back in the response - * @param courtScheduleId the booked session id to return (as if courtscheduler picked it) - * @param isDraft draft state of the picked session — drives ALLOCATED/UNALLOCATED downstream - * @param source expected source label on the incoming request; verified via request-body match - */ - public static void stubCrownFallbackSearchAndBookSuccess(final String hearingId, - final String courtScheduleId, - final boolean isDraft, - final String source) { - final String sessionDate = ItClock.today().plusDays(1).toString(); - final String startTime = sessionDate + "T09:00:00Z"; - final String endTime = sessionDate + "T17:00:00Z"; - final String body = format( - "{\"hearingId\":\"%s\",\"courtScheduleId\":\"%s\",\"courtRoomId\":731816," + - "\"sessionDate\":\"%s\",\"sessionStartTime\":\"%s\",\"sessionEndTime\":\"%s\"," + - "\"durationInMinutes\":10,\"isDraft\":%s,\"businessType\":\"CR\"," + - "\"source\":\"%s\",\"overbooked\":false}", - hearingId, courtScheduleId, sessionDate, startTime, endTime, isDraft, source); - - stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + CROWN_FALLBACK_SEARCH_BOOK))) - .withQueryParam("source", matching(source)) - .willReturn(aResponse().withStatus(OK.getStatusCode()) - .withBody(body) - .withHeader(CONTENT_TYPE, CROWN_FALLBACK_SEARCH_BOOK_TYPE))); - } - - /** Stub 404 "no session found" — listing-side translates to CrownFallbackNoSessionException. */ - public static void stubCrownFallbackSearchAndBookNotFound() { - stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + CROWN_FALLBACK_SEARCH_BOOK))) - .willReturn(aResponse() - .withStatus(404) - .withHeader(CONTENT_TYPE, APPLICATION_JSON))); - } - - /** Stub 400 "invalid request" — used for defensive coverage; the listing-side multi-day guard fires first. */ - public static void stubCrownFallbackSearchAndBookBadRequest() { - stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + CROWN_FALLBACK_SEARCH_BOOK))) - .willReturn(aResponse() - .withStatus(BAD_REQUEST.getStatusCode()) - .withBody("{\"error\":\"durationInMinutes exceeds single-day cap\"}") - .withHeader(CONTENT_TYPE, APPLICATION_JSON))); - } - - /** Verify the Crown fallback endpoint was called with the expected source label. */ - public static void verifyCrownFallbackSearchAndBookCalledWithSource(final String source) { - Awaitility.await().atMost(15, SECONDS).pollInterval(POLL_INTERVAL).until(() -> { - try { - WireMock.verify(WireMock.getRequestedFor(urlPathMatching( - COURT_SCHEDULER_ENDPOINT + CROWN_FALLBACK_SEARCH_BOOK)) - .withQueryParam("source", matching(source))); - return true; - } catch (VerificationException e) { - return false; - } - }); - } - - /** Verify the Crown fallback endpoint was NEVER called (regression guard for MAGS / already-allocated CROWN). */ - public static void verifyCrownFallbackSearchAndBookNeverCalled() { - WireMock.verify(0, WireMock.getRequestedFor(urlPathMatching( - COURT_SCHEDULER_ENDPOINT + CROWN_FALLBACK_SEARCH_BOOK))); - } - - // --- Multi-day search-and-book stubs (CROWN update multi-day path) --- - - /** - * Stub a successful response from GET /multidaysearchandbook/hearingslots returning the supplied - * court schedule sessions. Used to drive the CROWN multi-day update path — the listing service - * passes the starting courtScheduleId + total duration, courtscheduler returns N consecutive - * sessions that together cover the duration. - */ - public static void stubMultiDaySearchAndBook(final List courtScheduleIds, - final UUID courtHouseId, - final UUID courtRoomId, - final LocalDate firstSessionDate, - final boolean isDraft) { - final StringBuilder body = new StringBuilder(); - body.append("{\"courtSchedules\":["); - for (int i = 0; i < courtScheduleIds.size(); i++) { - if (i > 0) { - body.append(","); - } - final LocalDate sessionDate = firstSessionDate.plusDays(i); - body.append("{") - .append("\"courtScheduleId\":\"").append(courtScheduleIds.get(i)).append("\",") - .append("\"courtHouseId\":\"").append(courtHouseId).append("\",") - .append("\"courtRoomId\":\"").append(courtRoomId).append("\",") - .append("\"sessionDate\":\"").append(sessionDate).append("\",") - .append("\"hearingStartTime\":\"").append(sessionDate).append("T09:00:00Z\",") - .append("\"isDraft\":").append(isDraft) - .append("}"); - } - body.append("]}"); - - stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + "/multidaysearchandbook/hearingslots"))) - .atPriority(1) - .willReturn(aResponse().withStatus(OK.getStatusCode()) - .withBody(body.toString()) - .withHeader(CONTENT_TYPE, APPLICATION_JSON))); - } - - /** - * Verify that GET /multidaysearchandbook/hearingslots was called with the expected courtScheduleId - * + total duration. Proves the CROWN update path correctly routed multi-day through the CourtSchedule-first - * flow and didn't regress to the startDate→endDate expansion. - */ - public static void verifyMultiDaySearchAndBookCalled(final String courtScheduleId, final int durationInMinutes) { - Awaitility.await().atMost(15, SECONDS).pollInterval(POLL_INTERVAL).until(() -> { - try { - WireMock.verify(WireMock.getRequestedFor(urlPathMatching( - COURT_SCHEDULER_ENDPOINT + "/multidaysearchandbook/hearingslots")) - .withQueryParam("courtScheduleId", WireMock.equalTo(courtScheduleId)) - .withQueryParam("durationInMinutes", WireMock.equalTo(String.valueOf(durationInMinutes)))); - return true; - } catch (VerificationException e) { - return false; - } - }); - } - - /** Regression guard: CROWN update without a courtScheduleId must NOT trigger multi-day search-and-book. */ - public static void verifyMultiDaySearchAndBookNeverCalled() { - WireMock.verify(0, WireMock.getRequestedFor(urlPathMatching( - COURT_SCHEDULER_ENDPOINT + "/multidaysearchandbook/hearingslots"))); - } - - // --- Extend multi-day hearing stubs (SPRDT-901: CROWN update-hearing-for-listing multi-day path) --- - - private static final String EXTEND_MULTIDAY = "/extendmultidayhearing/hearingslots"; - private static final String COURTSCHEDULER_EXTEND_MULTIDAY_TYPE = - "application/vnd.courtscheduler.extend.multiday.hearing+json"; - - /** - * Stub a successful POST to /extendmultidayhearing/hearingslots returning the supplied court schedule - * sessions, scoped to the supplied hearingId. SPRDT-901 routes CROWN multi-day updates here instead of - * the GET-based /multidaysearchandbook — courtscheduler receives the full duration and returns N - * sessions to use as the rebuilt hearingDays. - * - *

Scoping: WireMock stubs persist across IT classes in the same suite. Without a body - * matcher, this stub would intercept every other IT that extends a CROWN hearing into multi-day - * (e.g. HearingCsvReportIT) and return these synthetic courtSchedules — corrupting their hearingDays. - * The hearingId body match makes the stub apply only to the test's own hearing. - */ - public static void stubExtendMultiDayHearing(final String hearingId, - final List courtScheduleIds, - final UUID courtHouseId, - final UUID courtRoomId, - final LocalDate firstSessionDate, - final boolean isDraft) { - final StringBuilder body = new StringBuilder(); - body.append("{\"courtSchedules\":["); - for (int i = 0; i < courtScheduleIds.size(); i++) { - if (i > 0) { - body.append(","); - } - final LocalDate sessionDate = firstSessionDate.plusDays(i); - // Wire-shape note: the courtscheduler RAML example for this endpoint - // (courtscheduler.extend.multiday.hearing.response.json) shows "hearingStartTime", but the - // real endpoint serialises the courtscheduler DOMAIN CourtSchedule, which only has - // "sessionStartTime" (java.util.Date). Listing's buildHearingDaysFromMultiDaySessions reads - // getSessionStartTime() — emitting the RAML's field name leaves HearingDay.startTime null - // and NPEs downstream ("HearingDay.getStartTime() is null"). Match the wire, not the schema. - // (stubMultiDaySearchAndBook above is DIFFERENT: that endpoint's consumer reads hearingStartTime.) - body.append("{") - .append("\"courtScheduleId\":\"").append(courtScheduleIds.get(i)).append("\",") - .append("\"courtHouseId\":\"").append(courtHouseId).append("\",") - .append("\"courtRoomId\":\"").append(courtRoomId).append("\",") - .append("\"sessionDate\":\"").append(sessionDate).append("\",") - .append("\"sessionStartTime\":\"").append(sessionDate).append("T09:00:00.000Z\",") - .append("\"isDraft\":").append(isDraft) - .append("}"); - } - body.append("]}"); - - stubFor(post(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + EXTEND_MULTIDAY))) - .withHeader(CONTENT_TYPE, containing(COURTSCHEDULER_EXTEND_MULTIDAY_TYPE)) - .withRequestBody(containing("\"hearingId\":\"" + hearingId + "\"")) - .willReturn(aResponse().withStatus(OK.getStatusCode()) - .withBody(body.toString()) - .withHeader(CONTENT_TYPE, APPLICATION_JSON))); - } - - /** - * Verify that POST /extendmultidayhearing/hearingslots was called with a body containing the - * supplied hearingId and durationInMinutes. Proves SPRDT-901 routing: the CROWN multi-day update - * was sent to courtscheduler's new extension endpoint with the full requested duration. - */ - public static void verifyExtendMultiDayHearingCalled(final String hearingId, final int durationInMinutes) { - Awaitility.await().atMost(15, SECONDS).pollInterval(POLL_INTERVAL).until(() -> { - try { - WireMock.verify(WireMock.postRequestedFor(urlPathMatching( - COURT_SCHEDULER_ENDPOINT + EXTEND_MULTIDAY)) - .withHeader(CONTENT_TYPE, containing(COURTSCHEDULER_EXTEND_MULTIDAY_TYPE)) - .withRequestBody(containing("\"hearingId\":\"" + hearingId + "\"")) - .withRequestBody(containing("\"durationInMinutes\":" + durationInMinutes))); - return true; - } catch (VerificationException e) { - return false; - } - }); - } - - /** Regression guard: single-day CROWN updates / non-CROWN updates must NOT call /extendmultidayhearing. */ - public static void verifyExtendMultiDayHearingNeverCalled() { - WireMock.verify(0, WireMock.postRequestedFor(urlPathMatching( - COURT_SCHEDULER_ENDPOINT + EXTEND_MULTIDAY))); - } - - /** - * SPRDT-902: stub a 422 typed-failure response from /extendmultidayhearing/hearingslots, - * scoped to a specific hearingId. Body shape mirrors the courtscheduler RAML error contract. - */ - public static void stubExtendMultiDayHearingFailure(final String hearingId, - final int statusCode, - final String errorCode, - final List unavailableDates) { - final StringBuilder body = new StringBuilder("{\"errorCode\":\"").append(errorCode).append("\""); - if (unavailableDates != null && !unavailableDates.isEmpty()) { - body.append(",\"unavailableDates\":["); - for (int i = 0; i < unavailableDates.size(); i++) { - if (i > 0) { - body.append(","); - } - body.append("\"").append(unavailableDates.get(i)).append("\""); - } - body.append("]"); - } - body.append("}"); - - stubFor(post(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + EXTEND_MULTIDAY))) - .withHeader(CONTENT_TYPE, containing(COURTSCHEDULER_EXTEND_MULTIDAY_TYPE)) - .withRequestBody(containing("\"hearingId\":\"" + hearingId + "\"")) - .willReturn(aResponse().withStatus(statusCode) - .withBody(body.toString()) - .withHeader(CONTENT_TYPE, APPLICATION_JSON))); - } - - public static void stubValidateSessionAvailabilityFailure() { - stubFor(post(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + VALIDATE_SESSION_AVAILABILITY))) - .withHeader("Content-Type", containing(COURTSCHEDULER_VALIDATE_SESSION_AVAILABILITY_TYPE)) + public static void stubSearchAvailableJudiciariesBadRequest() { + stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + JUDICIARIES_SEARCH_AVAILABLE))) + .withQueryParam("search", matching("x")) + .withHeader("Accept", containing(COURTSCHEDULER_SEARCH_AVAILABLE_JUDICIARIES_TYPE)) .willReturn(aResponse().withStatus(BAD_REQUEST.getStatusCode()) - .withBody("{\"validationResult\":{\"status\":\"FAILURE\",\"validationError\":\"duration is required\"}}") - .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody("search text too short") )); } @@ -481,6 +159,7 @@ public static void stubGetAvailableHearingSlots(boolean isEmpty) { .withQueryParam("panel", matching("ADULT")) .withQueryParam("oucodeL2Code", matching("Z01KR05")) .withQueryParam("sessionEndDate", matching("2020-10-11")) + .withQueryParam("jurisdiction", matching("MAGISTRATES")) .withHeader("Accept", containing(CourtSchedulerServiceStub.COURTSCHEDULER_GET_HEARING_SLOTS_TYPE)) .willReturn(aResponse().withStatus(OK.getStatusCode()) .withBody(getPayload(isEmpty ? CourtSchedulerServiceStub.LISTING_SEARCH_HEARING_EMPTY_SLOTS_JSON : CourtSchedulerServiceStub.LISTING_SEARCH_HEARING_SLOTS_JSON)) @@ -506,6 +185,7 @@ public static void stubGetAvailableHearingSlotsWithOverbookedSlots(boolean showO .withQueryParam("oucodeL2Code", matching("Z01KR05")) .withQueryParam("sessionEndDate", matching("2020-10-11")) .withQueryParam("showOverbookedSlots", matching(String.valueOf(showOverbookedSlots))) + .withQueryParam("jurisdiction", matching("MAGISTRATES")) .withHeader("Accept", containing(COURTSCHEDULER_GET_HEARING_SLOTS_TYPE)) .willReturn(aResponse().withStatus(OK.getStatusCode()) .withBody(getPayload(showOverbookedSlots ? LISTING_SEARCH_HEARING_SLOTS_JSON : LISTING_SEARCH_HEARING_EMPTY_SLOTS_JSON)) @@ -564,7 +244,7 @@ public static void stubProvisionalBookingWithCustomParams(final Map 0 ? "true" : "false").append(",\n"); - hearingSlotsJson.append(" \"benchChairman\": ").append(i == 0 ? "true" : "false").append("\n"); + hearingSlotsJson.append(" \"isDeputy\": ").append(i > 0 ? "true" : "false").append(",\n"); + hearingSlotsJson.append(" \"isBenchChairman\": ").append(i == 0 ? "true" : "false").append("\n"); hearingSlotsJson.append(" }"); isFirstJudiciary = false; } @@ -748,7 +428,6 @@ public static JsonObject stubGetAvailableHearingSlotsWithQueryParams(final Updat .withQueryParam("pageSize", matching(".*")) .withQueryParam("pageNumber", matching(".*")) .withQueryParam("courtRoomId", matching(".*")) - .withHeader("Accept", containing(CourtSchedulerServiceStub.COURTSCHEDULER_GET_HEARING_SLOTS_TYPE)) .willReturn(aResponse().withStatus(OK.getStatusCode()) .withBody(payloadString) .withHeader(CONTENT_TYPE, APPLICATION_JSON) @@ -800,6 +479,7 @@ public static void stubSessionEndDateEmptyRequest() { .withQueryParam("pageSize", matching("20")) .withQueryParam("panel", matching("ADULT")) .withQueryParam("oucodeL2Code", matching("Z01KR05")) + .withQueryParam("jurisdiction", matching("MAGISTRATES")) .withHeader("Accept", containing(CourtSchedulerServiceStub.COURTSCHEDULER_GET_HEARING_SLOTS_TYPE)) .willReturn(aResponse().withStatus(BAD_REQUEST.getStatusCode()) .withBody("Mandatory Search Criteria sessionEndDate cannot be null") @@ -827,34 +507,6 @@ public static void stubListHearingInCourtSessions(final String hearingId, final )); } - /** - * Scoped variant of {@link #stubListHearingInCourtSessions(String, String, ZonedDateTime)}: matches only the - * PUT whose request body carries this {@code courtScheduleId}. Needed when one command lists several CROWN - * hearings (e.g. list-next-hearings-v2 with two next hearings) — each hearing's list call must resolve to its - * own session rather than the last broad stub registered winning for all of them. - */ - public static void stubListHearingInCourtSessionsForCourtSchedule(final String hearingId, final String courtScheduleId, final ZonedDateTime hearingStartTime) { - final String payload = "{\n" + - " \"hearings\": [\n" + - " {\n" + - " \"hearingId\": \"" + hearingId + "\",\n" + - " \"courtScheduleId\": \"" + courtScheduleId + "\",\n" + - " \"hearingStartTime\": \"" + hearingStartTime.toString() + "\",\n" + - " \"duration\": 20\n" + - " }\n" + - " ]\n" + - "}"; - - stubFor(WireMock.put(WireMock.urlPathEqualTo(format("%s", CourtSchedulerServiceStub.COURT_SCHEDULER_ENDPOINT + "/list/hearingslots"))) - .atPriority(2) - .withHeader("content-type", containing("application/vnd.courtscheduler.list.hearings-in-court-sessions+json")) - .withRequestBody(containing(courtScheduleId)) - .willReturn(aResponse().withStatus(OK.getStatusCode()) - .withBody(payload) - .withHeader(CONTENT_TYPE, APPLICATION_JSON) - )); - } - public static void stubListHearingInCourtSessions(final String hearingId, final String courtScheduleId, final String hearingStartTime) { final String payload = "{\n" + " \"hearings\": [\n" + @@ -894,9 +546,9 @@ public static void stubListHearingInCourtSessionsWithJudiciary(final String hear } JudicialRoleData judiciary = judiciaries.get(i); payload.append(" {\n"); - payload.append(" \"judiciaryId\": \"").append(judiciary.getJudicialId()).append("\",\n"); + payload.append(" \"id\": \"").append(judiciary.getJudicialId()).append("\",\n"); payload.append(" \"rotaJudiciaryId\": \"MA").append(format("%04d", 2000 + i)).append("\",\n"); - payload.append(" \"title\": \"Mr\",\n"); + payload.append(" \"titlePrefix\": \"Mr\",\n"); payload.append(" \"surname\": \"DefaultSurname").append(i + 1).append("\",\n"); payload.append(" \"courtScheduleId\": \"").append(courtScheduleId).append("\",\n"); payload.append(" \"judiciaryType\": \"").append(judiciary.getJudicialRoleType().getJudiciaryType()).append("\",\n"); @@ -917,8 +569,8 @@ public static void stubListHearingInCourtSessionsWithJudiciary(final String hear payload.append(" \"active\": false,\n"); payload.append(" \"createdOn\": \"2025-03-12T20:27:00.724+00:00\",\n"); payload.append(" \"updatedOn\": \"2025-03-12T20:27:00.724+00:00\",\n"); - payload.append(" \"deputy\": ").append(judiciary.getIsDeputy().orElse(false)).append(",\n"); - payload.append(" \"benchChairman\": ").append(judiciary.getIsBenchChairman().orElse(false)).append("\n"); + payload.append(" \"isDeputy\": ").append(judiciary.getIsDeputy().orElse(false)).append(",\n"); + payload.append(" \"isBenchChairman\": ").append(judiciary.getIsBenchChairman().orElse(false)).append("\n"); payload.append(" }"); } } @@ -996,9 +648,9 @@ public static void stubListHearingInCourtSessionsWithMultipleSchedules(final Upd } JudicialRoleData judiciary = judiciaries.get(i); hearingsJson.append(" {\n"); - hearingsJson.append(" \"judiciaryId\": \"").append(judiciary.getJudicialId()).append("\",\n"); + hearingsJson.append(" \"id\": \"").append(judiciary.getJudicialId()).append("\",\n"); hearingsJson.append(" \"rotaJudiciaryId\": \"MA").append(format("%04d", 2000 + i)).append("\",\n"); - hearingsJson.append(" \"title\": \"Mr\",\n"); + hearingsJson.append(" \"titlePrefix\": \"Mr\",\n"); hearingsJson.append(" \"surname\": \"DefaultSurname").append(i + 1).append("\",\n"); hearingsJson.append(" \"courtScheduleId\": \"").append(courtScheduleId).append("\",\n"); hearingsJson.append(" \"judiciaryType\": \"").append(judiciary.getJudicialRoleType().getJudiciaryType()).append("\",\n"); @@ -1019,8 +671,8 @@ public static void stubListHearingInCourtSessionsWithMultipleSchedules(final Upd hearingsJson.append(" \"active\": false,\n"); hearingsJson.append(" \"createdOn\": \"2025-03-12T20:27:00.724+00:00\",\n"); hearingsJson.append(" \"updatedOn\": \"2025-03-12T20:27:00.724+00:00\",\n"); - hearingsJson.append(" \"deputy\": ").append(judiciary.getIsDeputy().orElse(false)).append(",\n"); - hearingsJson.append(" \"benchChairman\": ").append(judiciary.getIsBenchChairman().orElse(false)).append("\n"); + hearingsJson.append(" \"isDeputy\": ").append(judiciary.getIsDeputy().orElse(false)).append(",\n"); + hearingsJson.append(" \"isBenchChairman\": ").append(judiciary.getIsBenchChairman().orElse(false)).append("\n"); hearingsJson.append(" }"); } @@ -1076,9 +728,9 @@ public static void stubListHearingInCourtSessionsWithMultipleSchedulesWithJudici } JudicialRoleData judiciary = judiciaries.get(i); hearingsJson.append(" {\n"); - hearingsJson.append(" \"judiciaryId\": \"").append(judiciary.getJudicialId()).append("\",\n"); + hearingsJson.append(" \"id\": \"").append(judiciary.getJudicialId()).append("\",\n"); hearingsJson.append(" \"rotaJudiciaryId\": \"MA").append(format("%04d", 2000 + i)).append("\",\n"); - hearingsJson.append(" \"title\": \"Mr\",\n"); + hearingsJson.append(" \"titlePrefix\": \"Mr\",\n"); hearingsJson.append(" \"surname\": \"DefaultSurname").append(i + 1).append("\",\n"); hearingsJson.append(" \"courtScheduleId\": \"").append(courtScheduleId).append("\",\n"); hearingsJson.append(" \"judiciaryType\": \"").append(judiciary.getJudicialRoleType().getJudiciaryType()).append("\",\n"); @@ -1099,8 +751,8 @@ public static void stubListHearingInCourtSessionsWithMultipleSchedulesWithJudici hearingsJson.append(" \"active\": false,\n"); hearingsJson.append(" \"createdOn\": \"2025-03-12T20:27:00.724+00:00\",\n"); hearingsJson.append(" \"updatedOn\": \"2025-03-12T20:27:00.724+00:00\",\n"); - hearingsJson.append(" \"deputy\": ").append(judiciary.getIsDeputy().orElse(false)).append(",\n"); - hearingsJson.append(" \"benchChairman\": ").append(judiciary.getIsBenchChairman().orElse(false)).append("\n"); + hearingsJson.append(" \"isDeputy\": ").append(judiciary.getIsDeputy().orElse(false)).append(",\n"); + hearingsJson.append(" \"isBenchChairman\": ").append(judiciary.getIsBenchChairman().orElse(false)).append("\n"); hearingsJson.append(" }"); } @@ -1279,8 +931,8 @@ public static void stubSearchBookHearingSlotsForCrown(final String hearingId, fi " \"hearingId\": \"" + hearingId + "\",\n" + " \"courtScheduleId\": \"" + UUID.randomUUID() + "\",\n" + " \"courtRoomId\": \"" + courtRoomId + "\",\n" + - " \"hearingDate\": \"" + ItClock.today().plusDays(5) + "\",\n" + - " \"hearingStartTime\": \"" + ItClock.nowUtc().plusDays(5).withHour(10).withMinute(0).withSecond(0).withNano(0) + "\",\n" + + " \"hearingDate\": \"" + LocalDate.now().plusDays(5) + "\",\n" + + " \"hearingStartTime\": \"" + ZonedDateTime.now(java.time.ZoneOffset.UTC).plusDays(5).withHour(10).withMinute(0).withSecond(0).withNano(0) + "\",\n" + " \"duration\": 30\n" + " }\n" + "}"; @@ -1294,37 +946,6 @@ public static void stubSearchBookHearingSlotsForCrown(final String hearingId, fi )); } - /** - * searchAndBook stub for the CROWN "update removing the court room → unallocated" scenario. - * Mirrors {@link #stubSearchBookHearingSlotsForCrown} (lenient hearingId+courtCentreId matchers so - * it matches regardless of the request's date/duration/start-time params) but reports the booked - * session as {@code isDraft:true}. The update path ({@code handleCrownUpdateSearchAndBook}) takes - * only {@code courtScheduleId}+{@code isDraft} from this response and lets the aggregate unallocate - * — clearing the previously-allocated court room. A courtRoomId is still emitted purely to satisfy - * the parser ({@code searchAndBookSlots} reads it unconditionally); it is discarded downstream. - */ - public static void stubSearchBookHearingSlotsForCrownDraft(final String hearingId, final String courtCentreId) { - final String payload = "{\n" + - " \"hearingSlots\": {\n" + - " \"hearingId\": \"" + hearingId + "\",\n" + - " \"courtScheduleId\": \"" + UUID.randomUUID() + "\",\n" + - " \"courtRoomId\": \"" + courtCentreId + "\",\n" + - " \"hearingDate\": \"" + ItClock.today().plusDays(5) + "\",\n" + - " \"hearingStartTime\": \"" + ItClock.nowUtc().plusDays(5).withHour(10).withMinute(0).withSecond(0).withNano(0) + "\",\n" + - " \"duration\": 30,\n" + - " \"isDraft\": true\n" + - " }\n" + - "}"; - - stubFor(get(WireMock.urlPathEqualTo(format("%s", COURT_SCHEDULER_ENDPOINT + "/searchlist/hearingslots"))) - .withQueryParam("hearingId", matching(hearingId)) - .withQueryParam("courtCentreId", matching(courtCentreId)) - .willReturn(aResponse().withStatus(OK.getStatusCode()) - .withBody(payload) - .withHeader(CONTENT_TYPE, APPLICATION_JSON) - )); - } - /** * Registers low-priority catch-all stubs for all court-scheduler endpoints. * These prevent 60s timeouts when the enrichment service makes calls that @@ -1346,6 +967,13 @@ public static void stubCourtSchedulerCatchAll() { .withBody("{\"hearingSlots\":[]}") .withHeader(CONTENT_TYPE, APPLICATION_JSON))); + // GET /judiciaries/search-available — search available judiciaries (proxy) + stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + JUDICIARIES_SEARCH_AVAILABLE))) + .atPriority(10) + .willReturn(aResponse().withStatus(OK.getStatusCode()) + .withBody("{\"judiciaries\":[]}") + .withHeader(CONTENT_TYPE, APPLICATION_JSON))); + // GET /courtschedule/search.court-schedules-by-id stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + "/courtschedule/search.court-schedules-by-id"))) .atPriority(10) @@ -1373,66 +1001,137 @@ public static void stubCourtSchedulerCatchAll() { .willReturn(aResponse().withStatus(NO_CONTENT.getStatusCode()))); } - /** - * Minimal draft-status stub: each session carries ONLY courtScheduleId + isDraft. - * Sufficient for LIST-path consumers (allocation decision reads just the flag). - * For UPDATE-path consumers use the overload below — the single-day update's - * sanityCheckAndEnrichCrown overwrites hearingDate with the session's sessionDate, - * so a session without one would null the date and log a CROWN sanity ERROR. - * - *

HISTORY: until 2026-06-07 this emitted the envelope key "hearingSlots" — the real - * courtscheduler wire (and listing's fetchCourtSchedulesByIds) uses "courtSchedules", - * so every caller silently parsed an EMPTY list and enrichment degraded with - * "CROWN single-day update: failed to fetch court schedules" WARNs.

- */ public static void stubGetCourtSchedulesByIdWithDraftStatus(final List courtScheduleIds, final boolean isDraft) { - final StringBuilder schedulesJson = new StringBuilder(); - for (int i = 0; i < courtScheduleIds.size(); i++) { - if (i > 0) { - schedulesJson.append(","); - } - schedulesJson.append("{\"courtScheduleId\":\"").append(courtScheduleIds.get(i)).append("\",") - .append("\"isDraft\":").append(isDraft).append("}"); - } - stubCourtSchedulesByIdResponse("{\"courtSchedules\":[" + schedulesJson + "]}"); - } + final StringBuilder hearingSlotsJson = new StringBuilder(); + hearingSlotsJson.append("{\n"); + hearingSlotsJson.append(" \"hearingSlots\": [\n"); - /** - * Full draft-status stub for UPDATE-path flows: sessions carry sessionDate, courtHouseId, - * courtRoomId and hearingStartTime so sanityCheckAndEnrichCrown can re-derive the hearing - * day without nulling its date. Values MUST agree with the update payload's nonDefaultDays - * (same date/centre/room) or the enrichment will log a CROWN sanity date-mismatch ERROR - * and shift the projected hearing day. - */ - public static void stubGetCourtSchedulesByIdWithDraftStatus(final List courtScheduleIds, - final boolean isDraft, - final LocalDate sessionDate, - final UUID courtHouseId, - final UUID courtRoomId, - final ZonedDateTime hearingStartTime) { - final StringBuilder schedulesJson = new StringBuilder(); for (int i = 0; i < courtScheduleIds.size(); i++) { if (i > 0) { - schedulesJson.append(","); - } - schedulesJson.append("{\"courtScheduleId\":\"").append(courtScheduleIds.get(i)).append("\"") - .append(",\"courtHouseId\":\"").append(courtHouseId).append("\""); - if (courtRoomId != null) { - schedulesJson.append(",\"courtRoomId\":\"").append(courtRoomId).append("\""); + hearingSlotsJson.append(",\n"); } - schedulesJson.append(",\"sessionDate\":\"").append(sessionDate).append("\"") - .append(",\"hearingStartTime\":\"").append(hearingStartTime).append("\"") - .append(",\"isDraft\":").append(isDraft).append("}"); + hearingSlotsJson.append(" {\n"); + hearingSlotsJson.append(" \"courtScheduleId\": \"").append(courtScheduleIds.get(i)).append("\",\n"); + hearingSlotsJson.append(" \"isDraft\": ").append(isDraft).append("\n"); + hearingSlotsJson.append(" }"); } - stubCourtSchedulesByIdResponse("{\"courtSchedules\":[" + schedulesJson + "]}"); - } - private static void stubCourtSchedulesByIdResponse(final String body) { + hearingSlotsJson.append("\n ]\n"); + hearingSlotsJson.append("}"); + stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + "/courtschedule/search.court-schedules-by-id"))) .withQueryParam("courtScheduleIds", matching(".*")) .withHeader("Accept", containing("application/vnd.courtscheduler.search.court-schedules-by-id+json")) .willReturn(aResponse().withStatus(OK.getStatusCode()) - .withBody(body) + .withBody(hearingSlotsJson.toString()) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + )); + } + + public static void stubGetCourtSchedulesByIdWithJudiciary(final String courtScheduleId, + final String judiciaryId, + final String judiciaryType, + final boolean isBenchChairman, + final boolean isDeputy) { + stubGetCourtSchedulesByIdWithJudiciary(courtScheduleId, judiciaryId, judiciaryType, isBenchChairman, isDeputy, + null, null, null, null, null, null, null); + } + + public static void stubGetCourtSchedulesByIdWithJudiciary(final String courtScheduleId, + final String judiciaryId, + final String judiciaryType, + final boolean isBenchChairman, + final boolean isDeputy, + final Integer seqId, + final String titlePrefix, + final String titleJudicialPrefix, + final String titleJudicialPrefixWelsh, + final String personId, + final java.util.List specialisms, + final String requestedName) { + stubGetCourtSchedulesByIdWithJudiciary(courtScheduleId, judiciaryId, judiciaryType, isBenchChairman, isDeputy, + seqId, titlePrefix, titleJudicialPrefix, titleJudicialPrefixWelsh, personId, specialisms, requestedName, + null, null, null); + } + + public static void stubGetCourtSchedulesByIdWithJudiciary(final String courtScheduleId, + final String judiciaryId, + final String judiciaryType, + final boolean isBenchChairman, + final boolean isDeputy, + final Integer seqId, + final String titlePrefix, + final String titleJudicialPrefix, + final String titleJudicialPrefixWelsh, + final String personId, + final java.util.List specialisms, + final String requestedName, + final String surname, + final String forenames, + final String emailAddress) { + final StringBuilder judiciaryJson = new StringBuilder() + .append(" \"id\": \"").append(judiciaryId).append("\",\n") + .append(" \"judiciaryType\": \"").append(judiciaryType).append("\",\n") + .append(" \"isBenchChairman\": ").append(isBenchChairman).append(",\n") + .append(" \"isDeputy\": ").append(isDeputy); + if (seqId != null) { + judiciaryJson.append(",\n \"seqId\": ").append(seqId); + } + if (titlePrefix != null) { + judiciaryJson.append(",\n \"titlePrefix\": \"").append(titlePrefix).append("\""); + } + if (titleJudicialPrefix != null) { + judiciaryJson.append(",\n \"titleJudicialPrefix\": \"").append(titleJudicialPrefix).append("\""); + } + if (titleJudicialPrefixWelsh != null) { + judiciaryJson.append(",\n \"titleJudicialPrefixWelsh\": \"").append(titleJudicialPrefixWelsh).append("\""); + } + if (personId != null) { + judiciaryJson.append(",\n \"personId\": \"").append(personId).append("\""); + } + if (specialisms != null && !specialisms.isEmpty()) { + judiciaryJson.append(",\n \"specialisms\": [") + .append(specialisms.stream().map(s -> "\"" + s + "\"").collect(java.util.stream.Collectors.joining(","))) + .append("]"); + } + if (requestedName != null) { + judiciaryJson.append(",\n \"requestedName\": \"").append(requestedName).append("\""); + } + if (surname != null) { + judiciaryJson.append(",\n \"surname\": \"").append(surname).append("\""); + } + if (forenames != null) { + judiciaryJson.append(",\n \"forenames\": \"").append(forenames).append("\""); + } + if (emailAddress != null) { + judiciaryJson.append(",\n \"emailAddress\": \"").append(emailAddress).append("\""); + } + + final String responseBody = "{\n" + + " \"courtSchedules\": [\n" + + " {\n" + + " \"courtRoomId\": \"" + java.util.UUID.randomUUID() + "\",\n" + + " \"courtRoomName\": \"Court Room 1\",\n" + + " \"sessions\": [\n" + + " {\n" + + " \"courtScheduleId\": \"" + courtScheduleId + "\",\n" + + " \"sessionDate\": \"2026-01-15\",\n" + + " \"judiciaries\": [\n" + + " {\n" + + judiciaryJson + "\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + + stubFor(get(urlPathMatching(format("%s", COURT_SCHEDULER_ENDPOINT + "/courtschedule/search.court-schedules-by-id"))) + .withQueryParam("courtScheduleIds", matching(".*" + courtScheduleId + ".*")) + .atPriority(5) + .willReturn(aResponse().withStatus(OK.getStatusCode()) + .withBody(responseBody) .withHeader(CONTENT_TYPE, APPLICATION_JSON) )); } diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/HmiAllocationStubHelper.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/HmiAllocationStubHelper.java deleted file mode 100644 index e2b0553eb..000000000 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/HmiAllocationStubHelper.java +++ /dev/null @@ -1,48 +0,0 @@ -package uk.gov.moj.cpp.listing.utils; - -import static java.util.UUID.randomUUID; -import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubListHearingInCourtSessions; -import static uk.gov.moj.cpp.listing.utils.CourtSchedulerServiceStub.stubProvisionalBookingWithCustomParams; - -import uk.gov.moj.cpp.listing.steps.data.HearingData; - -import java.time.LocalDate; -import java.time.ZonedDateTime; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -/** - * Aligns court-scheduler WireMock stubs with the hearing under test (start time, court centre, room). - * Using {@code ZonedDateTime.now()} at a fixed clock time causes pipeline-only allocation failures. - */ -public final class HmiAllocationStubHelper { - - private static final String DEFAULT_COURT_SCHEDULE_ID = "8e837de0-743a-4a2c-9db3-b2e678c48729"; - - private HmiAllocationStubHelper() { - } - - public static void stubForAllocatedListing(final HearingData hearingData) { - stubForAllocatedListing(hearingData, DEFAULT_COURT_SCHEDULE_ID); - } - - public static void stubForAllocatedListing(final HearingData hearingData, final String courtScheduleId) { - final ZonedDateTime hearingStartTime = hearingData.getHearingStartTime(); - final LocalDate hearingDate = hearingStartTime.toLocalDate(); - final UUID courtCentreId = hearingData.getCourtCentreId(); - final UUID courtroomId = hearingData.getCourtRoomId(); - final UUID bookingId = randomUUID(); - - final Map stubParams = new HashMap<>(); - stubParams.put("SESSION_DATE", hearingDate.toString()); - stubParams.put("COURT_CENTRE_ID", courtCentreId.toString()); - stubParams.put("COURT_SCHEDULE_ID", courtScheduleId); - stubParams.put("COURT_ROOM_ID", courtroomId.toString()); - stubParams.put("BOOKING_ID", bookingId.toString()); - stubParams.put("HEARING_START_TIME", hearingStartTime.toString()); - - stubProvisionalBookingWithCustomParams(stubParams); - stubListHearingInCourtSessions(hearingData.getId().toString(), courtScheduleId, hearingStartTime); - } -} diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/QueueUtil.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/QueueUtil.java index 541a02624..cf683473c 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/QueueUtil.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/QueueUtil.java @@ -20,7 +20,7 @@ public class QueueUtil { - private static final long RETRIEVE_TIMEOUT = 5000;//1sec makes HearingIT.updateHearingResultsWhenCourtRoomNotSelected:375 + private static final long RETRIEVE_TIMEOUT = 5000; private static final long MESSAGE_RETRIEVE_TRIAL_TIMEOUT = 60000; public static final QueueUtil publicEvents = new QueueUtil(); @@ -91,10 +91,7 @@ public static JsonPath retrieveMessage(final JmsMessageConsumerClient consumer, } } } while (MESSAGE_RETRIEVE_TRIAL_TIMEOUT > (System.currentTimeMillis() - startTime)); - // B6: fail loudly instead of returning null (which produced opaque NPEs at every call site). - // Mirrors the 1-arg overload — a missing/filtered-out event is a clear, actionable failure. - throw new java.util.NoSuchElementException( - "No JMS message matching [" + matchers + "] received within " + MESSAGE_RETRIEVE_TRIAL_TIMEOUT + "ms"); + return null; } public static void clearAllMessages(JmsMessageConsumerClient consumer) { diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/ReferenceDataStub.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/ReferenceDataStub.java index 10c8265a2..22307832a 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/ReferenceDataStub.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/ReferenceDataStub.java @@ -36,8 +36,6 @@ public class ReferenceDataStub { private static final String REFERENCE_DATA_COURT_MAPPINGS_QUERY_URL = "/referencedata-service/query/api/rest/referencedata/cp-xhibit-court-mappings"; private static final String REFERENCE_DATA_COURT_MAPPINGS_MEDIA_TYPE = "application/vnd.referencedata.query.cp-xhibit-court-mappings+json"; - private static final String REFERENCE_DATA_CP_XHIBIT_MAGS_COURT_MAPPING_QUERY_URL = "/referencedata-service/query/api/rest/referencedata/cp-xhibit-mags-court-mapping"; - private static final String REFERENCE_DATA_CP_XHIBIT_MAGS_COURT_MAPPING_MEDIA_TYPE = "application/vnd.referencedata.query.cp-xhibit-mags-court-mapping+json"; private static final String REFERENCE_DATA_CP_XHIBIT_COURTROOM_MAPPINGS_QUERY_URL = "/referencedata-service/query/api/rest/referencedata/cp-xhibit-courtroom-mappings"; private static final String REFERENCE_DATA_CP_XHIBIT_COURTROOM_MAPPINGS_MEDIA_TYPE = "application/vnd.referencedata.query.cp-xhibit-courtroom-mappings+json"; private static final String REFERENCE_DATA_COURT_CENTRE_QUERY_URL = "/referencedata-service/query/api/rest/referencedata/courtrooms/.*"; @@ -127,19 +125,6 @@ public static void stubGetReferenceDataCourtMappings(final CourtCentreData court .withBody(payload))); } - /** - * Stub {@code referencedata.query.cp-xhibit-mags-court-mapping} for a specific {@code oucode} (WireMock matches query param). - */ - public static void stubGetReferenceDataCpXhibitMagsCourtMappingForOucode(final String oucode, final String responseBody) { - stubPingForReferenceDataService(); - stubFor(get(urlPathMatching(REFERENCE_DATA_CP_XHIBIT_MAGS_COURT_MAPPING_QUERY_URL)) - .withQueryParam("oucode", equalTo(oucode)) - .willReturn(aResponse().withStatus(SC_OK) - .withHeader("CPPID", randomUUID().toString()) - .withHeader("Content-Type", REFERENCE_DATA_CP_XHIBIT_MAGS_COURT_MAPPING_MEDIA_TYPE) - .withBody(responseBody))); - } - public static void stubGetReferenceDataXhibitCourtRoomMappings(final UUID courtRoomUUID) { InternalEndpointMockUtils.stubPingFor("referencedata-service"); @@ -178,39 +163,13 @@ public static void stubGetReferenceDataCpCourtRooms() { .withBody(payload))); } - /** - * The courtrooms array in referencedata.query.courtroom.json is HARDCODED to the four pool rooms - * (COURT_ROOM_IDS). These methods historically also did .replace("COURT_ROOM_ID", ...) — a silent - * no-op, because the template contains no such placeholder. Tests passing a custom (non-pool) - * courtRoomId therefore got refdata WITHOUT their room, and any flow resolving the cp courtroom - * number (HearingDaysEnrichmentService.getCpCourtRoomNumber via ReferenceDataCache) failed with - * InvalidReferenceDataException "Cannot find court room uuid ..." → HTTP 500 + undertow ERROR. - * This helper makes the parameter real: a custom room is injected as an extra courtrooms entry; - * pool rooms (already in the template) and null pass through unchanged. - */ - private static String withCourtRoom(final String payload, final UUID courtRoomId) { - if (courtRoomId == null || payload.contains(courtRoomId.toString())) { - return payload; - } - final String injectedRoom = "\"courtrooms\": [\n" + - " {\n" + - " \"courtroomId\": 1966,\n" + - " \"id\": \"" + courtRoomId + "\",\n" + - " \"venueName\": \"STUB CUSTOM COURT\",\n" + - " \"venueNameWelsh\": \"STUB CUSTOM COURT\",\n" + - " \"courtroomName\": \"Courtroom 06\",\n" + - " \"courtroomNameWelsh\": \"Ystafell y Llys 06\"\n" + - " },"; - return payload.replace("\"courtrooms\": [", injectedRoom); - } - public static void stubGetReferenceDataCourtCentre(final CourtCentreData courtReferenceData) { stubPingForReferenceDataService(); - String payload = withCourtRoom(getPayload("stub-data/referencedata.query.courtroom.json") + String payload = getPayload("stub-data/referencedata.query.courtroom.json") .replace("COURT_CENTRE_ID", courtReferenceData.getCourtCentreId().toString()) .replace("DEFAULT_START_TIME", courtReferenceData.getDefaultStartTime().toString()) - .replace("DEFAULT_DURATION_HOURS_MINS", courtReferenceData.getDefaultDurationHoursMins()), - courtReferenceData.getCourtRoomId()); + .replace("DEFAULT_DURATION_HOURS_MINS", courtReferenceData.getDefaultDurationHoursMins()) + .replace("COURT_ROOM_ID", courtReferenceData.getCourtRoomId() != null ? courtReferenceData.getCourtRoomId().toString() : getRandomCourtRoomId().toString()); stubFor(get(urlPathMatching(REFERENCE_DATA_COURT_CENTRE_QUERY_URL)) .willReturn(aResponse().withStatus(SC_OK) @@ -223,11 +182,11 @@ public static void stubGetReferenceDataCourtCentres(CourtCentreData... courtCent stubPingForReferenceDataService(); final JsonArrayBuilder jsonArrBuilder = createArrayBuilder(); stream(courtCenters).toList().forEach( cc -> { - String payload = withCourtRoom(getPayload("stub-data/referencedata.query.courtroom.json") + String payload = getPayload("stub-data/referencedata.query.courtroom.json") .replace("COURT_CENTRE_ID", cc.getCourtCentreId().toString()) .replace("DEFAULT_START_TIME", cc.getDefaultStartTime().toString()) - .replace("DEFAULT_DURATION_HOURS_MINS", cc.getDefaultDurationHoursMins()), - cc.getCourtRoomId()); + .replace("DEFAULT_DURATION_HOURS_MINS", cc.getDefaultDurationHoursMins()) + .replace("COURT_ROOM_ID", cc.getCourtRoomId() != null ? cc.getCourtRoomId().toString() : getRandomCourtRoomId().toString()); jsonArrBuilder.add(createReader(new java.io.StringReader(payload)).readObject()); }); final JsonObject rootJsonObj = createObjectBuilder().add("organisationunits", jsonArrBuilder.build()).build(); @@ -257,11 +216,11 @@ public static void stubGetReferenceDataCourtCentreById(final CourtCentreData cou final String urlPath = String.format(REFERENCE_DATA_COURT_ROOM_QUERY_URL, courtReferenceData.getCourtCentreId()); - String payload = withCourtRoom(getPayload("stub-data/referencedata.query.courtroom.json") + String payload = getPayload("stub-data/referencedata.query.courtroom.json") .replace("COURT_CENTRE_ID", courtReferenceData.getCourtCentreId().toString()) .replace("DEFAULT_START_TIME", courtReferenceData.getDefaultStartTime().toString()) - .replace("DEFAULT_DURATION_HOURS_MINS", courtReferenceData.getDefaultDurationHoursMins()), - courtReferenceData.getCourtRoomId()); + .replace("DEFAULT_DURATION_HOURS_MINS", courtReferenceData.getDefaultDurationHoursMins()) + .replace("COURT_ROOM_ID", courtReferenceData.getCourtRoomId() != null ? courtReferenceData.getCourtRoomId().toString() : getRandomCourtRoomId().toString()); stubFor(get(urlPathMatching(urlPath)) .willReturn(aResponse().withStatus(SC_OK) @@ -315,35 +274,6 @@ public static void stubGetReferenceDataOrganisationUnitById(UUID courtCentreId) .withBody(payload))); } - /** - * Low-priority catch-all for organisation-units/{anyUuid}. The EVENT_PROCESSOR's public - * hearing-confirmed V2 factory (PublicHearingFactory.buildCourtCentre via - * ReferenceDataService.getOrganizationUnitById) fetches the organisation unit for EVERY - * hearing allocation; an unmatched request returns 404 → NULL payload envelope → - * IncompatibleJsonPayloadTypeException → ~10x JMS redelivery storm → DLQ, for any test - * allocating under a centre without an explicit org-unit stub. - * - *

Safe as a catch-all because consumers only read oucode/oucodeL3Name — the courtCentre - * id in the built public event comes from the EVENT, not this response. Tests needing - * centre-specific values still win via their explicit stubs (default priority 5 < 10).

- */ - public static void stubGetReferenceDataOrganisationUnitCatchAll() { - InternalEndpointMockUtils.stubPingFor("referencedata-service"); - - final String urlPath = String.format(REFERENCE_DATA_ORGANISATION_UNIT_QUERY_URL, "[0-9a-fA-F-]{36}"); - - // Fixed dummy id: the response id is ignored by all consumers (see javadoc) - String payload = getPayload("stub-data/referencedata.query.organisation-unit.json") - .replace("COURT_CENTRE_ID", "f8254db1-1683-483e-afb4-a4d911484209"); - - stubFor(get(urlPathMatching(urlPath)) - .atPriority(10) - .willReturn(aResponse().withStatus(SC_OK) - .withHeader("CPPID", randomUUID().toString()) - .withHeader("Content-Type", REFERENCE_DATA_ORGANISATION_UNIT_MEDIA_TYPE) - .withBody(payload))); - } - public static void stubGetReferenceDataJudiciaries(final UUID judiciaryId) { stubPingForReferenceDataService(); String payload = getPayload("stub-data/referencedata.query.judiciaries.json") diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/WebDavStub.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/WebDavStub.java index 995891ca2..29ea6f55b 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/WebDavStub.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/utils/WebDavStub.java @@ -40,17 +40,4 @@ public static Optional getSentXml() { final LoggedRequest loggedRequest = putRequests.get(putRequests.size() - 1); return ofNullable(loggedRequest.getBodyAsString()); } - - /** - * Drain helper: blocks until at least {@code expectedCount} court-list XML PUTs have reached - * the xhibit-gateway stub. Tests that trigger asynchronous court-list exports should call this - * before finishing — otherwise the in-flight PUT crosses the next test's WireMock reset() and - * fails with 404 -> ERROR "Failed to put file" attributed to the wrong test's window. - */ - public static void awaitCourtListXmlFilesSent(final int expectedCount) { - org.awaitility.Awaitility.await() - .pollInterval(uk.gov.moj.cpp.listing.it.util.RestPollerHelper.POLL_INTERVAL) - .atMost(15, java.util.concurrent.TimeUnit.SECONDS) - .until(() -> findAll(putRequestedFor(urlPathMatching(XHIBIT_GATEWAY_SEND_TO_XHIBIT_PATH_REG_EX))).size() >= expectedCount); - } } diff --git a/listing-integration-test/src/test/resources/endpoint.properties b/listing-integration-test/src/test/resources/endpoint.properties index 8d9ef3764..14d95c79b 100644 --- a/listing-integration-test/src/test/resources/endpoint.properties +++ b/listing-integration-test/src/test/resources/endpoint.properties @@ -63,7 +63,6 @@ listing.delete-previous-hearings-and-create-new-hearing=listing-service/command/ listing.search.hearings.by.allocated.jurisdiction-type.court-session.business-type=listing-service/query/api/rest/listing/hearings/range-search/?allocated={0}&jurisdictionType={1}&courtSession={2}&businessType={3} listing.query.cache-refdata-courtroom.refresh=listing-service/query/api/rest/listing/cache-refdata-courtrooms/refresh listing.query.validate-session-availability=listing-service/query/api/rest/listing/sessionAvailabilityValidation -listing.query.court-schedule-draft-status=listing-service/query/api/rest/listing/courtScheduleDraftStatus listing.query.add-courtroom=listing-service/query/api/rest/listing/cache-refdata-courtrooms/add listing.query.close-courtroom=listing-service/query/api/rest/listing/cache-refdata-courtrooms/close listing.query.download-hearing-csv-report=listing-service/query/api/rest/listing/hearings/download-hearing-csv-report?courtCentreId={0}&startDate={1}&numberOfWeeks={2} \ No newline at end of file diff --git a/listing-integration-test/src/test/resources/stub-data/listing.get.hearing.slots.with-judiciaries-and-slotstarttimes.json b/listing-integration-test/src/test/resources/stub-data/listing.get.hearing.slots.with-judiciaries-and-slotstarttimes.json index 39627ec68..8b1a0556d 100644 --- a/listing-integration-test/src/test/resources/stub-data/listing.get.hearing.slots.with-judiciaries-and-slotstarttimes.json +++ b/listing-integration-test/src/test/resources/stub-data/listing.get.hearing.slots.with-judiciaries-and-slotstarttimes.json @@ -23,28 +23,28 @@ "availableDuration": 0, "judiciaries": [ { - "judiciaryId": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", + "id": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false }, { - "judiciaryId": "d424f73e-4a19-396d-a00c-f4c38d1c864e", + "id": "d424f73e-4a19-396d-a00c-f4c38d1c864e", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": false, - "benchChairman": true + "isDeputy": false, + "isBenchChairman": true }, { - "judiciaryId": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", + "id": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false } ], "slotStartTimes": [ diff --git a/listing-integration-test/src/test/resources/stub-data/listing.search.available.judiciaries.json b/listing-integration-test/src/test/resources/stub-data/listing.search.available.judiciaries.json new file mode 100644 index 000000000..aa8574c58 --- /dev/null +++ b/listing-integration-test/src/test/resources/stub-data/listing.search.available.judiciaries.json @@ -0,0 +1,14 @@ +{ + "judiciaries": [ + { + "id": "9f39f876-3ff6-32b5-926e-c588e36a87b8", + "titlePrefix": "Mr", + "titleJudicialPrefix": "Recorder", + "surname": "Ainsworth", + "forenames": "Mark J", + "judiciaryType": "Recorder", + "emailAddress": "mark.ainsworth@ejudiciary.net", + "specialisms": ["MURDER", "ATTEMPTED_MURDER"] + } + ] +} diff --git a/listing-integration-test/src/test/resources/stub-data/referencedata.query.cp-xhibit-court-mappings.json b/listing-integration-test/src/test/resources/stub-data/referencedata.query.cp-xhibit-court-mappings.json index c08aa92c7..b8377cb85 100644 --- a/listing-integration-test/src/test/resources/stub-data/referencedata.query.cp-xhibit-court-mappings.json +++ b/listing-integration-test/src/test/resources/stub-data/referencedata.query.cp-xhibit-court-mappings.json @@ -71,30 +71,6 @@ "crestCourtFullName": "LIVERPOOL HILTON HOTEL", "crestCourtSiteCode": "C", "courtType": "CROWN_COURT" - }, - { - "id": "7b1f4c2e-9d3a-3f88-9c41-2e85a0b6d914", - "oucode": "C06MC00", - "crestCourtId": "435", - "crestCourtSiteId": "435", - "crestCourtSiteName": "MANCHESTER", - "crestCourtName": "MANCHESTER", - "crestCourtShortName": "MANCH", - "crestCourtFullName": "MANCHESTER", - "crestCourtSiteCode": "C", - "courtType": "CROWN_COURT" - }, - { - "id": "4e9d72a1-6c5b-3a17-8f02-9b34c8e1f527", - "oucode": "C10NE00", - "crestCourtId": "439", - "crestCourtSiteId": "439", - "crestCourtSiteName": "NEWCASTLE UPON TYNE", - "crestCourtName": "NEWCASTLE", - "crestCourtShortName": "NEWCA", - "crestCourtFullName": "NEWCASTLE UPON TYNE", - "crestCourtSiteCode": "C", - "courtType": "CROWN_COURT" } ] } \ No newline at end of file diff --git a/listing-integration-test/src/test/resources/stub-data/referencedata.query.cp-xhibit-mags-court-mapping-c05lv00.json b/listing-integration-test/src/test/resources/stub-data/referencedata.query.cp-xhibit-mags-court-mapping-c05lv00.json deleted file mode 100644 index 254773ea0..000000000 --- a/listing-integration-test/src/test/resources/stub-data/referencedata.query.cp-xhibit-mags-court-mapping-c05lv00.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "id": "a1b2c3d4-e5f6-4789-a012-3456789abcde", - "oucode": "C05LV00", - "crestCourtId": "433", - "crestCourtSiteId": "433", - "crestCourtSiteName": "LIVERPOOL", - "crestCourtName": "LIVERPOOL", - "crestCourtShortName": "LIVER", - "crestCourtFullName": "LIVERPOOL", - "crestCourtSiteCode": "4567", - "courtType": "MAGISTRATES" -} diff --git a/listing-integration-test/src/test/resources/stub-data/referencedata.query.organisation-unit.json b/listing-integration-test/src/test/resources/stub-data/referencedata.query.organisation-unit.json deleted file mode 100644 index 5eae61f6a..000000000 --- a/listing-integration-test/src/test/resources/stub-data/referencedata.query.organisation-unit.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "id": "COURT_CENTRE_ID", - "oucode": "B04KO00", - "lja": "1790", - "oucodeL1Code": "B", - "courtId": "278", - "oucodeL1Name": "Magistrates' Courts", - "oucodeL3Name": "Stub Custom Court Centre", - "oucodeL3WelshName": "Llys Ynadon Stub", - "address1": "Lawson Street", - "address2": "Preston", - "address3": "Lancs.", - "postcode": "PR1 2QT", - "defaultStartTime": "10:00:00", - "defaultDurationHrs": "07:00:00", - "oucodeL2Code": "4", - "oucodeL2Name": "Lancashire", - "region": "North West", - "courtrooms": [ - { - "id": "1d0199f8-8812-48a2-b13c-837e1c03ff19", - "venueName": "DONCASTER COMBINED COURT", - "courtroomId": 852, - "courtroomName": "Courtroom 02" - }, - { - "id": "18982e9c-2475-36a4-a852-09ab720acfc9", - "venueName": "AYLESBURY COMBINED COURT", - "courtroomId": 3859, - "courtroomName": "Courtroom 03" - }, - { - "id": "28b922c3-0396-3c68-970f-5b805c7ab1bb", - "venueName": "WOOD GREEN COMBINED COURT", - "courtroomId": 2874, - "courtroomName": "Courtroom 04" - }, - { - "id": "02d9847e-00e9-3c6c-b25c-1adbf5355a52", - "venueName": "GUILDFORD COMBINED COURT", - "courtroomId": 985, - "courtroomName": "Courtroom 05", - "venueId": 375 - }] -} diff --git a/listing-integration-test/src/test/resources/stub-data/rotasl.get.hearing.slots.with-judiciaries.json b/listing-integration-test/src/test/resources/stub-data/rotasl.get.hearing.slots.with-judiciaries.json index e07fccf25..c7a0753ca 100644 --- a/listing-integration-test/src/test/resources/stub-data/rotasl.get.hearing.slots.with-judiciaries.json +++ b/listing-integration-test/src/test/resources/stub-data/rotasl.get.hearing.slots.with-judiciaries.json @@ -23,28 +23,28 @@ "availableDuration": 0, "judiciaries": [ { - "judiciaryId": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", + "id": "0fd0233e-a8f9-3dc8-8bef-4f6b1d4799f3", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false }, { - "judiciaryId": "d424f73e-4a19-396d-a00c-f4c38d1c864e", + "id": "d424f73e-4a19-396d-a00c-f4c38d1c864e", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": false, - "benchChairman": true + "isDeputy": false, + "isBenchChairman": true }, { - "judiciaryId": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", + "id": "f0cd595c-be23-3539-9aed-7f1aab1d1e19", "courtScheduleId": "0d9c4454-46db-305d-bb48-dcf423a6d2a5", "courtListingProfileId": "CS2339681", "judiciaryType": "MAGISTRATE", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false } ], "slotStartTimes": [] diff --git a/listing-integration-test/src/test/resources/test-data/CROWN/update-hearing-for-listing/update-hearing-for-listing-crown-multiday-no-courtscheduleid.json b/listing-integration-test/src/test/resources/test-data/CROWN/update-hearing-for-listing/update-hearing-for-listing-crown-multiday-no-courtscheduleid.json deleted file mode 100644 index 830ffca68..000000000 --- a/listing-integration-test/src/test/resources/test-data/CROWN/update-hearing-for-listing/update-hearing-for-listing-crown-multiday-no-courtscheduleid.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "courtCentreId": "%%COURT_CENTRE_ID%%", - "courtRoomId": "%%COURT_ROOM_ID%%", - "selectedCourtCentre": { - "id": "%%COURT_CENTRE_ID%%", - "courtRoomId": "%%COURT_ROOM_ID%%", - "courtCentreName": "Test Court Centre", - "ouCode": "B01LY00" - }, - "type": { - "id": "4a0e892d-c0c5-3c51-95b8-704d8c781776", - "description": "Plea" - }, - "startDate": "%%START_DATE%%", - "endDate": "%%END_DATE%%", - "nonSittingDays": [], - "nonDefaultDays": [ - { - "startTime": "%%START_TIME%%", - "courtCentreId": "%%COURT_CENTRE_ID%%", - "roomId": "%%COURT_ROOM_ID%%", - "duration": 1080 - } - ], - "judiciary": [], - "jurisdictionType": "CROWN", - "hearingLanguage": "ENGLISH", - "publicListNote": "", - "hasVideoLink": false, - "sendNotificationToParties": false -} diff --git a/listing-integration-test/src/test/resources/test-data/CROWN/update-hearing-for-listing/update-hearing-for-listing-crown-multiday-with-nonsitting.json b/listing-integration-test/src/test/resources/test-data/CROWN/update-hearing-for-listing/update-hearing-for-listing-crown-multiday-with-nonsitting.json deleted file mode 100644 index 780e310cd..000000000 --- a/listing-integration-test/src/test/resources/test-data/CROWN/update-hearing-for-listing/update-hearing-for-listing-crown-multiday-with-nonsitting.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "courtCentreId": "%%COURT_CENTRE_ID%%", - "courtRoomId": "%%COURT_ROOM_ID%%", - "selectedCourtCentre": { - "id": "%%COURT_CENTRE_ID%%", - "courtRoomId": "%%COURT_ROOM_ID%%", - "courtCentreName": "Test Court Centre", - "ouCode": "B01LY00" - }, - "type": { - "id": "4a0e892d-c0c5-3c51-95b8-704d8c781776", - "description": "Plea" - }, - "startDate": "%%START_DATE%%", - "endDate": "%%END_DATE%%", - "nonSittingDays": [ - "%%NON_SITTING_DAY%%" - ], - "nonDefaultDays": [ - { - "startTime": "%%START_TIME%%", - "courtCentreId": "%%COURT_CENTRE_ID%%", - "roomId": "%%COURT_ROOM_ID%%", - "duration": 1080 - } - ], - "judiciary": [], - "jurisdictionType": "CROWN", - "hearingLanguage": "ENGLISH", - "publicListNote": "", - "hasVideoLink": false, - "sendNotificationToParties": false -} diff --git a/listing-json/pom.xml b/listing-json/pom.xml index acaae83e4..a33a8fb7d 100644 --- a/listing-json/pom.xml +++ b/listing-json/pom.xml @@ -4,7 +4,7 @@ listing-parent uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT listing-json diff --git a/listing-json/src/main/resources/json/schema/applicantRespondent.json b/listing-json/src/main/resources/json/schema/applicantRespondent.json index e093e9310..cd1637801 100644 --- a/listing-json/src/main/resources/json/schema/applicantRespondent.json +++ b/listing-json/src/main/resources/json/schema/applicantRespondent.json @@ -32,13 +32,6 @@ }, "address": { "$ref": "http://justice.gov.uk/core/courts/address.json" - }, - "masterDefendantId": { - "$ref": "http://justice.gov.uk/listing/events/listing-definitions.json#/definitions/uuid" - }, - "dateOfBirth": { - "description": "The date of birth of the party", - "type": "string" } }, "required": [ diff --git a/listing-json/src/main/resources/json/schema/commands/hearingDay.json b/listing-json/src/main/resources/json/schema/commands/hearingDay.json index d12d9c5ce..03ecf7c0e 100644 --- a/listing-json/src/main/resources/json/schema/commands/hearingDay.json +++ b/listing-json/src/main/resources/json/schema/commands/hearingDay.json @@ -36,6 +36,10 @@ }, "hasSharedResults": { "type": "boolean" + }, + "isDraft": { + "description": "Whether the court schedule session is in draft state", + "type": "boolean" } }, "required": [ diff --git a/listing-json/src/main/resources/json/schema/court-schedule-judiciary.json b/listing-json/src/main/resources/json/schema/court-schedule-judiciary.json index a86e8120b..2f3de2c3c 100644 --- a/listing-json/src/main/resources/json/schema/court-schedule-judiciary.json +++ b/listing-json/src/main/resources/json/schema/court-schedule-judiciary.json @@ -3,13 +3,19 @@ "id": "http://justice.gov.uk/listing/events/court-schedule-judiciary.json", "type": "object", "properties": { - "judiciaryId": { + "id": { "type": "string" }, "rotaJudiciaryId": { "type": "string" }, - "title": { + "titlePrefix": { + "type": "string" + }, + "titleJudicialPrefix": { + "type": "string" + }, + "titleJudicialPrefixWelsh": { "type": "string" }, "forenames": { @@ -38,6 +44,21 @@ }, "isDeputy": { "$ref": "boolean" + }, + "seqId": { + "type": "integer" + }, + "personId": { + "type": "string" + }, + "requestedName": { + "type": "string" + }, + "specialisms": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ diff --git a/listing-json/src/main/resources/json/schema/hearing-day.json b/listing-json/src/main/resources/json/schema/hearing-day.json index 82dd95acb..5e391bb7c 100644 --- a/listing-json/src/main/resources/json/schema/hearing-day.json +++ b/listing-json/src/main/resources/json/schema/hearing-day.json @@ -33,6 +33,10 @@ }, "courtRoomId": { "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" + }, + "isDraft": { + "description": "Whether the court schedule session is in draft state", + "type": "boolean" } }, "required": [ diff --git a/listing-json/src/main/resources/json/schema/hearing.json b/listing-json/src/main/resources/json/schema/hearing.json index 0cf96d583..b4a61cd9d 100644 --- a/listing-json/src/main/resources/json/schema/hearing.json +++ b/listing-json/src/main/resources/json/schema/hearing.json @@ -30,6 +30,11 @@ "$ref": "http://justice.gov.uk/listing/events/judicialRole.json" } }, + "judiciarySource": { + "description": "Indicates whether judiciary on this hearing is hearing-level ('HEARING') or session-level fallback ('SESSION')", + "type": "string", + "enum": ["HEARING", "SESSION"] + }, "reportingRestrictionReason": { "type": "string" }, diff --git a/listing-performance-test/pom.xml b/listing-performance-test/pom.xml index c33fd3378..3f70fb308 100644 --- a/listing-performance-test/pom.xml +++ b/listing-performance-test/pom.xml @@ -5,7 +5,7 @@ uk.gov.moj.cpp.listing listing-parent - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT listing-performance-test diff --git a/listing-phase2-test-plan.md b/listing-phase2-test-plan.md new file mode 100644 index 000000000..4bfbb9694 --- /dev/null +++ b/listing-phase2-test-plan.md @@ -0,0 +1,248 @@ +# Crown Court Scheduler Phase-2 -- Listing Context Test Plan + +**Branch:** `team/ccsph2n` vs `origin/main` +**Repo:** `cpp-context-listing` +**Confluence:** [Crown Court Scheduler Phase-2](https://tools.hmcts.net/confluence/spaces/CPPTI/pages/1933989390/Crown+Court+Scheduler+Phase-2) +**Date Generated:** 2026-03-25 + +--- + +## Table of Contents + +1. [New & Modified Endpoints](#1-new--modified-endpoints) +2. [Prerequisite SQL Insert Statements](#2-prerequisite-sql-insert-statements) +3. [Curl Commands for Each Endpoint](#3-curl-commands-for-each-endpoint) +4. [Test Scenarios](#4-test-scenarios) +5. [Unit Test Coverage Summary](#5-unit-test-coverage-summary) +6. [Integration Test Coverage Summary](#6-integration-test-coverage-summary) + +--- + +## 1. New & Modified Endpoints + +### 1.1 NEW -- Validate Session Availability (Listing Wrapper) + +| # | Method | Path | Action Name | Permission | +|---|--------|------|-------------|------------| +| 1 | POST | `/sessionAvailabilityValidation` | `listing.validate.session.availability` | Court Clerks, Court Administrators, Crown Court Admin, Listing Officers, Legal Advisers, Court Associate | + +This endpoint wraps the courtscheduler's `/validate-session-availability` endpoint, forwarding the payload via `CourtSchedulerServiceAdapter.validateSessionAvailability()`. + +**Request Schema:** `listing.validate.session.availability.json` + +| Field | Type | Required | +|-------|------|----------| +| `courtScheduleIdList` | array of `{courtScheduleId: UUID}` | YES (minItems: 1) | +| `duration` | integer | NO | + +**Response Schema (on failure):** `listing.validate.session.availability.response.json` + +| Field | Type | Required | +|-------|------|----------| +| `validationResult.status` | string (SUCCESS/FAILURE) | YES | +| `validationResult.conflictingCourtScheduleId` | UUID | NO | +| `validationResult.validationError` | string | NO | + +### 1.2 MODIFIED -- Hearing Slots Search + +| # | Method | Path | Action Name | Changes | +|---|--------|------|-------------|---------| +| 2 | GET | `/hearingSlots` | `listing.search.hearing.slots` | New query params: `status` (DRAFT/FINAL/ALL, default ALL), `isWeekCommencing` (boolean) | + +When `isWeekCommencing=true`, the listing endpoint returns an empty response directly without calling courtscheduler (results=0, pageCount=0, empty hearingSlots array, empty notes array). + +### 1.3 MODIFIED -- Command-Side Services (Crown Court Enrichment) + +| Service | Changes | +|---------|---------| +| `CourtScheduleEnrichmentService` | Crown Court enrichment: multi-day search & book via courtscheduler, court schedule lookup by IDs, Crown-specific session enrichment | +| `HearingEnrichmentOrchestrator` | Added Crown jurisdiction branch (same enrichment pipeline as Magistrates) for both `enrichListCourtHearing` and `enrichUpdateHearingForListing` | +| `HearingDaysEnrichmentService` | Crown hearing day enrichment support | + +### 1.4 MODIFIED -- Common Services + +| Service | Changes | +|---------|---------| +| `HearingSlotsService` | Added `multiDaySearchAndBook(params)` (GET to courtscheduler `/multidaysearchandbook/hearingslots`), `getCourtSchedulesById(ids)` | +| `CourtSchedulerServiceAdapter` | Added `validateSessionAvailability(JsonObject)` wrapper method | + +### 1.5 Domain Changes + +| Class | Changes | +|-------|---------| +| `CourtSchedule` (domain-common) | Added `isDraft` field with getter | +| `HearingDay` (domain-common) | Added `courtScheduleId` field with getter/setter | +| `Hearing` (aggregate) | Updated for Crown hearing enrichment logic | +| `HearingDay` (aggregate) | Added `courtScheduleId` support, Crown allocation logic | + +--- + +## 2. Prerequisite SQL Insert Statements + +The listing context calls the courtscheduler service via HTTP, so prerequisites need to be seeded in the **courtscheduler** database (`scsl`). + +```sql +-- In SCSL database (courtscheduler): +INSERT INTO court_schedule ( + id, oucode, court_room_id, court_listing_profile_id, court_house_id, + session_start, session_end, session_end_time, business_type, court_session, + max_slot, max_duration, available_slot, available_duration, + is_draft, is_slot_based, jurisdiction, panel, created_on, updated_on +) VALUES +('f8254db1-1683-483e-afb3-b87fde5a0a26', 'B01LY00', 'room-001', 'LP001', 'courthouse-001', + '2026-04-06 10:00:00', '2026-04-06 16:00:00', '2026-04-06 16:00:00', 'LNG', 'AD', + 10, 360, 10, 360, false, false, 'CROWN', 'ADULT', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), +('9e4932f7-97b2-3010-b942-ddd2624e4dd8', 'B01LY00', 'room-001', 'LP001', 'courthouse-001', + '2026-04-07 10:00:00', '2026-04-07 16:00:00', '2026-04-07 16:00:00', 'LNG', 'AD', + 10, 360, 10, 360, false, false, 'CROWN', 'ADULT', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); +``` + +```sql +-- Cleanup +DELETE FROM court_schedule WHERE id IN ( + 'f8254db1-1683-483e-afb3-b87fde5a0a26', + '9e4932f7-97b2-3010-b942-ddd2624e4dd8' +); +``` + +--- + +## 3. Curl Commands for Each Endpoint + +> **Base URL:** `http://localhost:8080/listing-query-api/query/api/rest/listing` +> **Required Header:** `CJSCPPUID: {user-id}` + +### 3.1 Validate Session Availability (NEW) + +```bash +# Happy path +curl -X POST \ + 'http://localhost:8080/listing-query-api/query/api/rest/listing/sessionAvailabilityValidation' \ + -H 'Content-Type: application/vnd.listing.validate.session.availability+json' \ + -H 'CJSCPPUID: test-user-001' \ + -d '{ + "courtScheduleIdList": [ + {"courtScheduleId": "f8254db1-1683-483e-afb3-b87fde5a0a26"}, + {"courtScheduleId": "9e4932f7-97b2-3010-b942-ddd2624e4dd8"} + ], + "duration": 30 + }' + +# Failure case - non-existent court schedule +curl -X POST \ + 'http://localhost:8080/listing-query-api/query/api/rest/listing/sessionAvailabilityValidation' \ + -H 'Content-Type: application/vnd.listing.validate.session.availability+json' \ + -H 'CJSCPPUID: test-user-001' \ + -d '{ + "courtScheduleIdList": [ + {"courtScheduleId": "00000000-0000-0000-0000-000000000000"} + ], + "duration": 30 + }' +``` + +### 3.2 Hearing Slots Search (MODIFIED -- new params) + +```bash +# With status=FINAL +curl -X GET \ + 'http://localhost:8080/listing-query-api/query/api/rest/listing/hearingSlots?panel=ADULT&sessionStartDate=2026-04-06&sessionEndDate=2026-04-10&pageSize=20&pageNumber=1&status=FINAL' \ + -H 'Accept: application/vnd.listing.search.hearing.slots+json' \ + -H 'CJSCPPUID: test-user-001' + +# isWeekCommencing=true (empty response, no courtscheduler call) +curl -X GET \ + 'http://localhost:8080/listing-query-api/query/api/rest/listing/hearingSlots?panel=ADULT&sessionStartDate=2026-04-06&sessionEndDate=2026-04-10&pageSize=20&pageNumber=1&isWeekCommencing=true' \ + -H 'Accept: application/vnd.listing.search.hearing.slots+json' \ + -H 'CJSCPPUID: test-user-001' +``` + +--- + +## 4. Test Scenarios + +### 4.1 Validate Session Availability (NEW) + +| # | Scenario | Precondition | Expected | HTTP | +|---|----------|-------------- |----------|------| +| SA1 | Valid payload with courtScheduleIdList + duration | Court schedules exist in courtscheduler | 200 OK | 200 | +| SA2 | Courtscheduler returns error | Non-existent court schedule ID | 400 Bad Request passthrough | 400 | +| SA3 | Missing courtScheduleIdList | -- | 400 or schema validation error | 400 | + +### 4.2 Hearing Slots Search -- New Parameters (MODIFIED) + +| # | Scenario | Precondition | Expected | HTTP | +|---|----------|-------------- |----------|------| +| HS1 | Filter by status=DRAFT | Mix of draft and final | 200, only draft + notes | 200 | +| HS2 | Filter by status=FINAL | Mix | 200, only final + notes | 200 | +| HS3 | Filter by status=ALL (default) | Mix | 200, all sessions + notes | 200 | +| HS4 | isWeekCommencing=true | Any | 200, empty response (results=0, hearingSlots=[], notes=[]) | 200 | + +### 4.3 Crown Court Enrichment (Command Side) + +| # | Scenario | Precondition | Expected | +|---|----------|-------------- |----------| +| CR1 | List-court-hearing for Crown with courtScheduleIds | Hearing with courtScheduleId on each day | Enriched with court schedule info, judiciaries | +| CR2 | List-court-hearing for Crown without courtScheduleIds | Crown hearing without courtScheduleId | Search-and-book called, sessions assigned | +| CR3 | Update-hearing for Crown allocated | Assigned (non-draft) sessions | Court schedule info enriched | +| CR4 | Update-hearing for Crown unallocated | Draft sessions | Enriched, no courtroom info | +| CR5 | Multi-day Crown hearing enrichment | 3+ day hearing | Multi-day search-and-book called | +| CR6 | Week commencing Crown hearing | Week commencing dates | No enrichment, end date calculated | +| CR7 | Unsupported jurisdiction type | Neither MAGISTRATES nor CROWN | IllegalArgumentException | + +--- + +## 5. Unit Test Coverage Summary + +### 5.1 New Unit Test Files + +| File | Tests | Coverage Focus | +|------|-------|----------------| +| `DefaultQueryApiSessionAvailabilityValidationResourceTest.java` | 2 | Resource delegates to adapter, forwards success and error responses | +| `CourtScheduleTest.java` | 7 | Domain: isDraft field, builder | +| `HearingDayTest.java` (domain-common) | 6 | Domain: courtScheduleId field | +| `HearingDayTest.java` (aggregate) | 8 | Aggregate: courtScheduleId, Crown allocation | + +### 5.2 Modified Unit Test Files + +| File | Tests | Changes | +|------|-------|---------| +| `CourtScheduleEnrichmentServiceTest.java` | 44 | Crown enrichment, multi-day, court schedule by ID, judiciary for Crown | +| `HearingEnrichmentOrchestratorTest.java` | 21 | Crown jurisdiction, non-sitting days, sequence recalc | +| `CourtSchedulerServiceAdapterTest.java` | 10 | validateSessionAvailability delegation | +| `HearingSlotsServiceTest.java` | 26 | validateSessionAvailability, multiDaySearchAndBook, getCourtSchedulesById | +| `DefaultQueryApiHearingSlotsResourceTest.java` | 7 | status, isWeekCommencing; empty response for isWeekCommencing=true | +| `HearingAggregateTest.java` | 124 | Crown allocation/unallocation, courtScheduleId | + +--- + +## 6. Integration Test Coverage Summary + +### 6.1 New Integration Test Files + +| File | Tests | Coverage Focus | +|------|-------|----------------| +| `SessionAvailabilityValidationIT.java` | 2 | POST to `/sessionAvailabilityValidation` — success (stubbed 200) and failure (stubbed 400) | + +### 6.2 Modified Integration Tests + +| File | Changes | +|------|---------| +| `HearingIT.java` | Updated for Crown hearing flows | +| Various scenario test data | New Crown test data JSON files | + +### 6.3 Test Utilities + +| File | Purpose | +|------|---------| +| `CourtSchedulerServiceStub.java` | New: `stubValidateSessionAvailability()` (200), `stubValidateSessionAvailabilityFailure()` (400) | + +--- + +## Key Architectural Notes + +1. **Listing wraps Courtscheduler:** `HearingSlotsService` makes HTTP calls to courtscheduler REST API +2. **isWeekCommencing short-circuit:** Returns empty response at listing layer without calling courtscheduler +3. **Status defaults to ALL:** Listing defaults `status` to "ALL" before forwarding +4. **Crown enrichment pipeline:** Same as Magistrates: HearingDays → Duration → CourtSchedule +5. **Access control:** `listing.validate.session.availability` accessible to Court Clerks, Court Administrators, Crown Court Admin, Listing Officers, Legal Advisers, Court Associates diff --git a/listing-query/listing-query-api/pom.xml b/listing-query/listing-query-api/pom.xml index 6f8eed7b2..d587f63b5 100644 --- a/listing-query/listing-query-api/pom.xml +++ b/listing-query/listing-query-api/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.listing listing-query - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT listing-query-api diff --git a/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/DefaultQueryApiHearingSlotsResource.java b/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/DefaultQueryApiHearingSlotsResource.java index f8b522f4e..ecc5d0883 100644 --- a/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/DefaultQueryApiHearingSlotsResource.java +++ b/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/DefaultQueryApiHearingSlotsResource.java @@ -1,9 +1,6 @@ package uk.gov.justice.api.resource; -import static java.util.UUID.fromString; -import static java.util.stream.Collectors.toMap; -import static uk.gov.justice.services.common.converter.LocalDates.from; - +import org.apache.http.HttpStatus; import uk.gov.justice.services.common.converter.ListToJsonArrayConverter; import uk.gov.justice.services.core.annotation.Adapter; import uk.gov.justice.services.core.annotation.Component; @@ -11,20 +8,21 @@ import uk.gov.moj.cpp.listing.common.service.CourtSchedulerServiceAdapter; import uk.gov.moj.cpp.listing.query.view.service.NotesService; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - import javax.inject.Inject; import uk.gov.justice.services.messaging.JsonObjects; import javax.json.JsonArray; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; -import org.apache.http.HttpStatus; +import static java.util.UUID.fromString; +import static java.util.stream.Collectors.toMap; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.*; +import static uk.gov.justice.services.common.converter.LocalDates.from; @SuppressWarnings({"squid:S1612"}) @Adapter(Component.QUERY_API) @@ -54,9 +52,11 @@ public Response getHearingSlots(final String panel, final Boolean showOverbookedSlots, final String pageSize, final String pageNumber, - final Integer availableDurationMins) { + final Integer availableDurationMins, + final String status, + final String jurisdiction) { - final Map params = buildParamsMap(panel, sessionStartDate, sessionEndDate, hearingStartTime, oucodeL2Code, ouCode, courtRoomId, courtRoomNumber, businessType, courtSession, isSlotBased, showOverbookedSlots, pageSize, pageNumber, availableDurationMins); + final Map params = buildParamsMap(panel, sessionStartDate, sessionEndDate, hearingStartTime, oucodeL2Code, ouCode, courtRoomId, courtRoomNumber, businessType, courtSession, isSlotBased, showOverbookedSlots, pageSize, pageNumber, availableDurationMins, status, jurisdiction); final Response response = courtSchedulerServiceAdapter.hearingSlotsSearch(params); if(response.getStatusInfo().getStatusCode() != HttpStatus.SC_OK ){ return response; @@ -88,7 +88,9 @@ private Map buildParamsMap(final String panel, final Boolean showOverbookedSlots, final String pageSize, final String pageNumber, - final Integer availableDurationMins) { + final Integer availableDurationMins, + final String status, + final String jurisdiction) { final Map params = new HashMap<>(); params.put(PANEL, panel); params.put(SESSION_START_DATE, sessionStartDate); @@ -109,7 +111,9 @@ private Map buildParamsMap(final String panel, params.put(PAGE_SIZE, pageSize); params.put(PAGE_NUMBER, pageNumber); if(availableDurationMins != null) - params.put(DURATION, String.valueOf(availableDurationMins)); + params.put(AVAILABLE_DURATION_MINS, String.valueOf(availableDurationMins)); + params.put(STATUS, status != null ? status : "ALL"); + params.put(JURISDICTION, jurisdiction); return params.entrySet().stream().filter(entry -> entry.getValue() != null).collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); } @@ -122,8 +126,8 @@ private static JsonObjectBuilder buildJsonBuilderFromJsonObject(final JsonObject private JsonArray convertToNotes(JsonArray hearings){ final List notes = hearings.stream().map(h -> (JsonObject) h). - map( h -> new NoteUUIDService.ListingNotesCollection(fromString(h.getString("courtRoomId")), from(h.getString("sessionDate")))). - collect(Collectors.toList()); + map( h -> new NoteUUIDService.ListingNotesCollection(fromString(h.getString("courtRoomId")), from(h.getString("sessionDate")))) + .toList(); return listToJsonArrayConverter.convert(notesService.findNotes(notes)); } } diff --git a/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/DefaultQueryApiSessionAvailabilityValidationResource.java b/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/DefaultQueryApiSessionAvailabilityValidationResource.java new file mode 100644 index 000000000..ca95b80ba --- /dev/null +++ b/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/DefaultQueryApiSessionAvailabilityValidationResource.java @@ -0,0 +1,21 @@ +package uk.gov.justice.api.resource; + +import uk.gov.justice.services.core.annotation.Adapter; +import uk.gov.justice.services.core.annotation.Component; +import uk.gov.moj.cpp.listing.common.service.CourtSchedulerServiceAdapter; + +import javax.inject.Inject; +import javax.json.JsonObject; +import javax.ws.rs.core.Response; + +@Adapter(Component.QUERY_API) +public class DefaultQueryApiSessionAvailabilityValidationResource implements QueryApiSessionAvailabilityValidationResource { + + @Inject + private CourtSchedulerServiceAdapter courtSchedulerServiceAdapter; + + @Override + public Response validateSessionAvailability(final JsonObject requestPayload) { + return courtSchedulerServiceAdapter.validateSessionAvailability(requestPayload); + } +} diff --git a/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/QueryApiHearingSlotsResource.java b/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/QueryApiHearingSlotsResource.java index 832863027..8f49cbe06 100644 --- a/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/QueryApiHearingSlotsResource.java +++ b/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/QueryApiHearingSlotsResource.java @@ -1,5 +1,23 @@ package uk.gov.justice.api.resource; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.AVAILABLE_DURATION_MINS; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.BUSINESS_TYPE; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.COURT_ROOM_ID; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.COURT_ROOM_NUMBER; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.COURT_SESSION; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.HEARING_START_TIME; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.IS_SLOT_BASED; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.JURISDICTION; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.OUCODE; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.OU_L2_CODE; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.PAGE_NUMBER; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.PAGE_SIZE; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.PANEL; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.SESSION_END_DATE; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.SESSION_START_DATE; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.SHOW_OVERBOOKED_SLOTS; +import static uk.gov.justice.api.resource.SessionAvailabilityValidationQueryParamConstants.STATUS; + import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; @@ -8,22 +26,6 @@ @Path("hearingSlots") public interface QueryApiHearingSlotsResource { - String PANEL = "panel"; - String SESSION_START_DATE = "sessionStartDate"; - String SESSION_END_DATE = "sessionEndDate"; - String HEARING_START_TIME = "hearingStartTime"; - String OU_L2_CODE = "oucodeL2Code"; - String OUCODE = "ouCode"; - String COURT_ROOM_ID = "courtRoomId"; - String COURT_ROOM_NUMBER = "courtRoomNumber"; - String BUSINESS_TYPE = "businessType"; - String COURT_SESSION = "courtSession"; - String IS_SLOT_BASED = "isSlotBased"; - String SHOW_OVERBOOKED_SLOTS = "showOverbookedSlots"; - String PAGE_SIZE = "pageSize"; - String PAGE_NUMBER = "pageNumber"; - String AVAILABLE_DURATION_MINS = "availableDurationMins"; - String DURATION = "duration"; @GET @Produces("application/vnd.listing.search.hearing.slots+json") @@ -41,5 +43,7 @@ Response getHearingSlots(@QueryParam(PANEL) String panel, @QueryParam(SHOW_OVERBOOKED_SLOTS) Boolean showOverbookedSlots, @QueryParam(PAGE_SIZE) String pageSize, @QueryParam(PAGE_NUMBER) String pageNumber, - @QueryParam(AVAILABLE_DURATION_MINS) Integer availableDurationMins); -} \ No newline at end of file + @QueryParam(AVAILABLE_DURATION_MINS) Integer availableDurationMins, + @QueryParam(STATUS) String status, + @QueryParam(JURISDICTION) String jurisdiction); +} diff --git a/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/QueryApiSessionAvailabilityValidationResource.java b/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/QueryApiSessionAvailabilityValidationResource.java new file mode 100644 index 000000000..be7e50095 --- /dev/null +++ b/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/QueryApiSessionAvailabilityValidationResource.java @@ -0,0 +1,20 @@ +package uk.gov.justice.api.resource; + +import javax.json.JsonObject; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +@Path("sessionAvailabilityValidation") +public interface QueryApiSessionAvailabilityValidationResource { + + @POST + @Consumes("application/vnd.listing.validate.session.availability+json") + @Produces({ + "application/vnd.listing.validate.session.availability.response+json", + "application/json" + }) + Response validateSessionAvailability(JsonObject requestPayload); +} diff --git a/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/SessionAvailabilityValidationQueryParamConstants.java b/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/SessionAvailabilityValidationQueryParamConstants.java new file mode 100644 index 000000000..ad2912132 --- /dev/null +++ b/listing-query/listing-query-api/src/main/java/uk/gov/justice/api/resource/SessionAvailabilityValidationQueryParamConstants.java @@ -0,0 +1,33 @@ +package uk.gov.justice.api.resource; + +public final class SessionAvailabilityValidationQueryParamConstants { + + public static final String PANEL = "panel"; + public static final String SESSION_START_DATE = "sessionStartDate"; + public static final String SESSION_END_DATE = "sessionEndDate"; + public static final String HEARING_START_TIME = "hearingStartTime"; + public static final String OU_L2_CODE = "oucodeL2Code"; + public static final String OUCODE = "ouCode"; + public static final String COURT_ROOM_ID = "courtRoomId"; + public static final String COURT_ROOM_NUMBER = "courtRoomNumber"; + public static final String BUSINESS_TYPE = "businessType"; + public static final String COURT_SESSION = "courtSession"; + public static final String IS_SLOT_BASED = "isSlotBased"; + public static final String SHOW_OVERBOOKED_SLOTS = "showOverbookedSlots"; + public static final String PAGE_SIZE = "pageSize"; + public static final String PAGE_NUMBER = "pageNumber"; + public static final String AVAILABLE_DURATION_MINS = "availableDurationMins"; + public static final String DURATION = "duration"; + public static final String STATUS = "status"; + public static final String COURT_SCHEDULE_ID = "courtScheduleId"; + public static final String JURISDICTION = "jurisdiction"; + public static final String COURT_SCHEDULE_ID_LIST = "courtScheduleIdList"; + public static final String VALIDATION_RESULT = "validationResult"; + public static final String VALIDATION_STATUS = "status"; + public static final String VALIDATION_ERROR = "validationError"; + public static final String FAILURE = "FAILURE"; + + private SessionAvailabilityValidationQueryParamConstants() { + // utility class + } +} diff --git a/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/api/SearchAvailableJudiciariesQueryHandler.java b/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/api/SearchAvailableJudiciariesQueryHandler.java new file mode 100644 index 000000000..798036137 --- /dev/null +++ b/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/api/SearchAvailableJudiciariesQueryHandler.java @@ -0,0 +1,68 @@ +package uk.gov.moj.cpp.listing.query.api; + +import static uk.gov.justice.services.messaging.Envelope.metadataFrom; +import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom; + +import uk.gov.justice.services.core.annotation.Component; +import uk.gov.justice.services.core.annotation.Handles; +import uk.gov.justice.services.core.annotation.ServiceComponent; +import uk.gov.justice.services.messaging.JsonEnvelope; +import uk.gov.moj.cpp.listing.common.service.CourtSchedulerSearchService; + +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; +import javax.json.JsonObject; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + +@ServiceComponent(Component.QUERY_API) +public class SearchAvailableJudiciariesQueryHandler { + + private static final String ACTION_NAME = "listing.search.available.judiciaries"; + + private static final String SEARCH = "search"; + private static final String JUDICIARY_GROUP = "judiciaryGroup"; + private static final String LIMIT = "limit"; + private static final String DATES = "dates"; + private static final String COURT_HOUSE_ID = "courtHouseId"; + private static final String COURT_SCHEDULE_IDS = "courtScheduleIds"; + private static final String IGNORE_AVAILABILITY = "ignoreAvailability"; + + @SuppressWarnings("java:S6813") // field injection matches other listing-query-api handlers + @Inject + private CourtSchedulerSearchService courtSchedulerSearchService; + + @Handles(ACTION_NAME) + public JsonEnvelope searchAvailableJudiciaries(final JsonEnvelope query) { + final JsonObject payload = query.payloadAsJsonObject(); + final Map params = buildQueryParams(payload); + final Response response = courtSchedulerSearchService.searchAvailableJudiciaries(params); + if (Response.Status.OK.getStatusCode() != response.getStatus()) { + throw new WebApplicationException( + Response.status(response.getStatus()).entity(response.getEntity()).build()); + } + return envelopeFrom(metadataFrom(query.metadata()).withName(ACTION_NAME), (JsonObject) response.getEntity()); + } + + private static Map buildQueryParams(final JsonObject payload) { + final Map raw = new HashMap<>(); + putIfNonBlank(raw, SEARCH, payload.getString(SEARCH, null)); + putIfNonBlank(raw, JUDICIARY_GROUP, payload.getString(JUDICIARY_GROUP, null)); + putIfNonBlank(raw, LIMIT, payload.getString(LIMIT, null)); + putIfNonBlank(raw, DATES, payload.getString(DATES, null)); + putIfNonBlank(raw, COURT_HOUSE_ID, payload.getString(COURT_HOUSE_ID, null)); + putIfNonBlank(raw, COURT_SCHEDULE_IDS, payload.getString(COURT_SCHEDULE_IDS, null)); + if (payload.containsKey(IGNORE_AVAILABILITY) && !payload.isNull(IGNORE_AVAILABILITY)) { + raw.put(IGNORE_AVAILABILITY, String.valueOf(payload.getBoolean(IGNORE_AVAILABILITY))); + } + return raw; + } + + private static void putIfNonBlank(final Map map, final String key, final String value) { + if (value != null && !value.isBlank()) { + map.put(key, value); + } + } +} diff --git a/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/api/service/AlphabeticalCourtListService.java b/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/api/service/AlphabeticalCourtListService.java index 8fbc37d69..b82b08b1e 100644 --- a/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/api/service/AlphabeticalCourtListService.java +++ b/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/api/service/AlphabeticalCourtListService.java @@ -79,7 +79,6 @@ public class AlphabeticalCourtListService { private static final String RESPONDENTS = "respondents"; private static final String COURT_APPLICATION_PARTY_TYPE = "courtApplicationPartyType"; private static final String APPLICANT = "applicant"; - private static final String SUBJECT = "subject"; @Inject private CourtCentreFactory courtCentreFactory; @@ -151,13 +150,6 @@ private void addDefendantsFromHearing(final JsonObject hearing, final boolean we private Stream getAlphabeticalListDefendantFromCourtApplication(final JsonObject courtApplication, final CourtRoomDetails courtRoomDetails, final String hearingStartTime, final boolean welsh) { final String applicationReference = courtApplication.getString(APPLICATION_REFERENCE); - if (courtApplication.containsKey(SUBJECT)) { - final JsonObject subject = courtApplication.getJsonObject(SUBJECT); - if (!subject.getBoolean(RESTRICT_FROM_COURT_LIST, FALSE)) { - return Stream.of(getAlphabeticalListDefendantFromDefendantEquivalent(subject, hearingStartTime, applicationReference, courtRoomDetails, welsh)); - } - return Stream.empty(); - } if (courtApplication.containsKey(RESPONDENTS) && !courtApplication.getJsonArray(RESPONDENTS).isEmpty() && courtApplication.getJsonArray(RESPONDENTS).getValuesAs(JsonObject.class).stream() .anyMatch(respondent -> CourtApplicationPartyType.valueOf(respondent.getString(COURT_APPLICATION_PARTY_TYPE)) == PERSON)) { diff --git a/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/api/service/ReferenceDataService.java b/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/api/service/ReferenceDataService.java index 1639581b1..4d192b585 100644 --- a/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/api/service/ReferenceDataService.java +++ b/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/api/service/ReferenceDataService.java @@ -9,7 +9,6 @@ import uk.gov.justice.services.messaging.Envelope; import uk.gov.justice.services.messaging.JsonEnvelope; -import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import javax.json.JsonArray; import javax.json.JsonObject; @@ -26,7 +25,6 @@ import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom; @SuppressWarnings({"squid:CallToDeprecatedMethod"}) -@ApplicationScoped public class ReferenceDataService { private static final Logger LOGGER = LoggerFactory.getLogger(ReferenceDataService.class); private static final String REFERENCEDATA_QUERY_COURTROOM = "referencedata.query.courtroom"; diff --git a/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/document/generator/DocumentGeneratorClient.java b/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/document/generator/DocumentGeneratorClient.java index fc566e321..89caec025 100644 --- a/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/document/generator/DocumentGeneratorClient.java +++ b/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/document/generator/DocumentGeneratorClient.java @@ -7,23 +7,25 @@ import java.io.IOException; import java.util.UUID; -import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import javax.json.JsonObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ApplicationScoped public class DocumentGeneratorClient { private static final Logger LOGGER = LoggerFactory.getLogger(DocumentGeneratorClient.class); - @Inject private DocumentGeneratorClientProducer documentGeneratorClientProducer; - @Inject private SystemUserProvider systemUserProvider; + @Inject + public DocumentGeneratorClient(final DocumentGeneratorClientProducer documentGeneratorClientProducer , final SystemUserProvider systemUserProvider){ + this.documentGeneratorClientProducer = documentGeneratorClientProducer; + this.systemUserProvider = systemUserProvider; + } + public byte[] generateDocument(final JsonObject documentPayload, final String templateName){ final UUID systemUserId = systemUserProvider.getContextSystemUserId().orElseThrow(() -> new DocumentGenerationFailedException("Could not find systemId ")); try { diff --git a/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/document/generator/StandardPublicCourtListTemplateAssembler.java b/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/document/generator/StandardPublicCourtListTemplateAssembler.java index e4855cd44..c2aa979c6 100644 --- a/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/document/generator/StandardPublicCourtListTemplateAssembler.java +++ b/listing-query/listing-query-api/src/main/java/uk/gov/moj/cpp/listing/query/document/generator/StandardPublicCourtListTemplateAssembler.java @@ -493,11 +493,6 @@ private String getNameSuffixForRestrictedCase(final long restrictedCount, final } private List createDefendantsEquivalentFromCourtApplication(final JsonObject courtApplication, final boolean restrictedListRequired) { - if (courtApplication.containsKey(SUBJECT)) { - final JsonObject subject = courtApplication.getJsonObject(SUBJECT); - final boolean subjectRestricted = subject.getBoolean(RESTRICT_FROM_COURT_LIST, FALSE) && restrictedListRequired; - return ImmutableList.of(createDefendantEquivalentFromCourtApplication(courtApplication, subject, subjectRestricted, EMPTY, restrictedListRequired)); - } if (courtApplication.containsKey(RESPONDENTS) && !courtApplication.getJsonArray(RESPONDENTS).isEmpty() && courtApplication.getJsonArray(RESPONDENTS).getValuesAs(JsonObject.class).stream() .anyMatch(respondent -> CourtApplicationPartyType.valueOf(respondent.getString(COURT_APPLICATION_PARTY_TYPE)) == PERSON)) { diff --git a/listing-query/listing-query-api/src/main/resources/uk/gov/moj/cpp/listing/queryapi/accesscontrol/listing-query-api.drl b/listing-query/listing-query-api/src/main/resources/uk/gov/moj/cpp/listing/queryapi/accesscontrol/listing-query-api.drl index 39a1032e6..1b0aead02 100644 --- a/listing-query/listing-query-api/src/main/resources/uk/gov/moj/cpp/listing/queryapi/accesscontrol/listing-query-api.drl +++ b/listing-query/listing-query-api/src/main/resources/uk/gov/moj/cpp/listing/queryapi/accesscontrol/listing-query-api.drl @@ -104,6 +104,24 @@ rule "Query - API - listing.search.hearing.slots" $outcome.setSuccess(true); end +rule "Query - API - listing.search.available.judiciaries" + when + $outcome: Outcome(); + $action: Action(name == "listing.search.available.judiciaries"); + eval(userAndGroupProvider.isMemberOfAnyOfTheSuppliedGroups($action, RuleConstants.LISTING_OFFICERS, RuleConstants.CROWN_COURT_ADMIN, RuleConstants.COURT_ADMINISTRATORS, RuleConstants.COURT_CLERKS, RuleConstants.LEGAL_ADVISERS, RuleConstants.COURT_ASSOCIATE)); + then + $outcome.setSuccess(true); +end + +rule "Query - API - listing.validate.session.availability" + when + $outcome: Outcome(); + $action: Action(name == "listing.validate.session.availability"); + eval(userAndGroupProvider.isMemberOfAnyOfTheSuppliedGroups($action, RuleConstants.COURT_CLERKS, RuleConstants.COURT_ADMINISTRATORS, RuleConstants.CROWN_COURT_ADMIN, RuleConstants.LISTING_OFFICERS, RuleConstants.LEGAL_ADVISERS, RuleConstants.COURT_ASSOCIATE)); + then + $outcome.setSuccess(true); +end + rule "Query - API - listing.unscheduled.search.hearings" when $outcome: Outcome(); diff --git a/listing-query/listing-query-api/src/raml/json/listing.search.available.judiciaries.json b/listing-query/listing-query-api/src/raml/json/listing.search.available.judiciaries.json new file mode 100644 index 000000000..aa8574c58 --- /dev/null +++ b/listing-query/listing-query-api/src/raml/json/listing.search.available.judiciaries.json @@ -0,0 +1,14 @@ +{ + "judiciaries": [ + { + "id": "9f39f876-3ff6-32b5-926e-c588e36a87b8", + "titlePrefix": "Mr", + "titleJudicialPrefix": "Recorder", + "surname": "Ainsworth", + "forenames": "Mark J", + "judiciaryType": "Recorder", + "emailAddress": "mark.ainsworth@ejudiciary.net", + "specialisms": ["MURDER", "ATTEMPTED_MURDER"] + } + ] +} diff --git a/listing-query/listing-query-api/src/raml/json/listing.search.hearing.slots.json b/listing-query/listing-query-api/src/raml/json/listing.search.hearing.slots.json index 50fb64f48..657bbc474 100644 --- a/listing-query/listing-query-api/src/raml/json/listing.search.hearing.slots.json +++ b/listing-query/listing-query-api/src/raml/json/listing.search.hearing.slots.json @@ -21,25 +21,25 @@ "availableDuration": 24, "judiciaries": [ { - "judiciaryId": "MA4034", + "id": "MA4034", "courtScheduleId": "9e12bb19-da11-4437-b1a7-5169899f2d26", "courtListingProfileId": "ITCS2130334", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false }, { - "judiciaryId": "MA15757", + "id": "MA15757", "courtScheduleId": "9e12bb19-da11-4437-b1a7-5169899f2d26", "courtListingProfileId": "ITCS2130334", - "deputy": false, - "benchChairman": true + "isDeputy": false, + "isBenchChairman": true }, { - "judiciaryId": "MA15754", + "id": "MA15754", "courtScheduleId": "9e12bb19-da11-4437-b1a7-5169899f2d26", "courtListingProfileId": "ITCS2130334", - "deputy": true, - "benchChairman": false + "isDeputy": true, + "isBenchChairman": false } ], "slotStartTimes": [ diff --git a/listing-query/listing-query-api/src/raml/json/listing.validate.session.availability.json b/listing-query/listing-query-api/src/raml/json/listing.validate.session.availability.json new file mode 100644 index 000000000..69446483a --- /dev/null +++ b/listing-query/listing-query-api/src/raml/json/listing.validate.session.availability.json @@ -0,0 +1,11 @@ +{ + "courtScheduleIdList": [ + { + "courtScheduleId": "f8254db1-1683-483e-afb3-b87fde5a0a26" + }, + { + "courtScheduleId": "9e4932f7-97b2-3010-b942-ddd2624e4dd8" + } + ], + "duration": 30 +} diff --git a/listing-query/listing-query-api/src/raml/json/listing.validate.session.availability.response.json b/listing-query/listing-query-api/src/raml/json/listing.validate.session.availability.response.json new file mode 100644 index 000000000..dc7dc1015 --- /dev/null +++ b/listing-query/listing-query-api/src/raml/json/listing.validate.session.availability.response.json @@ -0,0 +1,7 @@ +{ + "validationResult": { + "status": "FAILURE", + "conflictingCourtScheduleId": "123e4567-e89b-12d3-a456-426655440000", + "validationError": "The court schedule conflicts with another court schedule" + } +} diff --git a/listing-query/listing-query-api/src/raml/json/schema/listing.search.available.judiciaries.json b/listing-query/listing-query-api/src/raml/json/schema/listing.search.available.judiciaries.json new file mode 100644 index 000000000..8a0d602af --- /dev/null +++ b/listing-query/listing-query-api/src/raml/json/schema/listing.search.available.judiciaries.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/listing/query/listing.search.available.judiciaries.json", + "type": "object", + "properties": { + "judiciaries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"$ref": "#/definitions/uuid"}, + "seqId": {"type": "integer"}, + "titlePrefix": {"type": "string"}, + "titlePrefixWelsh": {"type": "string"}, + "titleJudicialPrefix": {"type": "string"}, + "titleJudicialPrefixWelsh": {"type": "string"}, + "titleSuffix": {"type": "string"}, + "titleSuffixWelsh": {"type": "string"}, + "surname": {"type": "string"}, + "forenames": {"type": "string"}, + "judiciaryType": {"type": "string"}, + "personId": {"type": "string"}, + "validFrom": {"type": "string"}, + "validTo": {"type": "string"}, + "emailAddress": {"type": "string"}, + "cpUserId": {"type": "string"}, + "specialisms": { + "type": "array", + "items": { + "type": "string", + "enum": ["MURDER", "ATTEMPTED_MURDER", "SEXUAL_OFFENCE", "TERRORISM"] + } + } + }, + "required": ["id", "surname"] + } + } + }, + "required": ["judiciaries"], + "definitions": { + "uuid": { + "type": "string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + } + } +} diff --git a/listing-query/listing-query-api/src/raml/json/schema/listing.validate.session.availability.json b/listing-query/listing-query-api/src/raml/json/schema/listing.validate.session.availability.json new file mode 100644 index 000000000..08e32ba58 --- /dev/null +++ b/listing-query/listing-query-api/src/raml/json/schema/listing.validate.session.availability.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/listing/query/listing.validate.session.availability.json", + "type": "object", + "properties": { + "courtScheduleIdList": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "courtScheduleId": { + "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" + } + }, + "required": [ + "courtScheduleId" + ], + "additionalProperties": false + } + }, + "duration": { + "type": "integer" + } + }, + "required": [ + "courtScheduleIdList" + ], + "additionalProperties": false +} diff --git a/listing-query/listing-query-api/src/raml/json/schema/listing.validate.session.availability.response.json b/listing-query/listing-query-api/src/raml/json/schema/listing.validate.session.availability.response.json new file mode 100644 index 000000000..682546830 --- /dev/null +++ b/listing-query/listing-query-api/src/raml/json/schema/listing.validate.session.availability.response.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://justice.gov.uk/listing/query/listing.validate.session.availability.response.json", + "type": "object", + "properties": { + "validationResult": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "SUCCESS", + "FAILURE" + ] + }, + "conflictingCourtScheduleId": { + "$ref": "http://justice.gov.uk/domain/core/common/definitions.json#/definitions/uuid" + }, + "validationError": { + "type": "string" + } + }, + "required": [ + "status" + ], + "additionalProperties": false + } + }, + "required": [ + "validationResult" + ], + "additionalProperties": false +} diff --git a/listing-query/listing-query-api/src/raml/listing-query-api.raml b/listing-query/listing-query-api/src/raml/listing-query-api.raml index c2c9dab4d..03ce29bde 100644 --- a/listing-query/listing-query-api/src/raml/listing-query-api.raml +++ b/listing-query/listing-query-api/src/raml/listing-query-api.raml @@ -429,6 +429,17 @@ protocols: [ HTTP, HTTPS ] description: Available duration of the hearing in minutes type: integer required: false + status: + description: Publish status filter (DRAFT/FINAL/ALL). Default is ALL + type: string + required: false + default: ALL + enum: [DRAFT, FINAL, ALL] + jurisdiction: + description: Jurisdiction type + type: string + required: true + enum: [CROWN, MAGISTRATES] responses: 200: description: OK @@ -437,6 +448,75 @@ protocols: [ HTTP, HTTPS ] example: !include json/listing.search.hearing.slots.json schema: !include json/schema/listing.search.hearing.slots.json +/judiciaries/search-available: + get: + description: | + Pass-through proxy to courtscheduler search for reference-data judiciaries with optional availability filtering. + ... + (mapping): + responseType: application/vnd.listing.search.available.judiciaries+json + name: listing.search.available.judiciaries + ... + queryParameters: + search: + description: Typeahead text (minimum 2 characters) + type: string + required: true + judiciaryGroup: + description: Optional judiciary group / type filter + type: string + required: false + limit: + description: Maximum judiciaries to return from reference data (default 50) + type: string + required: false + dates: + description: Comma-separated ISO dates (yyyy-MM-dd) when filtering by calendar dates without session types + type: string + required: false + courtHouseId: + description: Required with dates when availability is applied (same courthouse as sessions) + type: string + required: false + courtScheduleIds: + description: Comma-separated court schedule ids; implies session-type-aware availability and one courthouse + type: string + required: false + ignoreAvailability: + description: When true, skips internal availability filtering + type: boolean + required: false + default: false + responses: + 200: + description: OK + body: + application/vnd.listing.search.available.judiciaries+json: + schema: !include json/schema/listing.search.available.judiciaries.json + example: !include json/listing.search.available.judiciaries.json + +/sessionAvailabilityValidation: + post: + description: | + ... + (mapping): + requestType: application/vnd.listing.validate.session.availability+json + name: listing.validate.session.availability + ... + body: + application/vnd.listing.validate.session.availability+json: + example: !include json/listing.validate.session.availability.json + schema: !include json/schema/listing.validate.session.availability.json + responses: + 200: + description: OK + 400: + description: Validation failed + body: + application/vnd.listing.validate.session.availability.response+json: + example: !include json/listing.validate.session.availability.response.json + schema: !include json/schema/listing.validate.session.availability.response.json + /hearings/unscheduled: get: description: | diff --git a/listing-query/listing-query-api/src/test/java/uk/gov/justice/api/resource/DefaultQueryApiHearingSlotsResourceTest.java b/listing-query/listing-query-api/src/test/java/uk/gov/justice/api/resource/DefaultQueryApiHearingSlotsResourceTest.java index 53cfda050..cd0dc48d6 100644 --- a/listing-query/listing-query-api/src/test/java/uk/gov/justice/api/resource/DefaultQueryApiHearingSlotsResourceTest.java +++ b/listing-query/listing-query-api/src/test/java/uk/gov/justice/api/resource/DefaultQueryApiHearingSlotsResourceTest.java @@ -34,6 +34,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; @@ -51,6 +53,9 @@ class DefaultQueryApiHearingSlotsResourceTest { @Mock private NotesService notesService; + @Captor + private ArgumentCaptor> paramsCaptor; + private Response response; private List notes; @@ -91,7 +96,9 @@ void searchHearingSlots() { null, "20", "1", - 20); + 20, + null, + "MAGISTRATES"); verify(courtSchedulerServiceAdapter).hearingSlotsSearch(any(Map.class)); verify(notesService).findNotes(any(List.class)); @@ -123,7 +130,9 @@ void searchHearingSlots_WithHearingStartTime() { null, "20", "1", - 20); + 20, + null, + "MAGISTRATES"); verify(courtSchedulerServiceAdapter).hearingSlotsSearch(any(Map.class)); verify(notesService).findNotes(any(List.class)); @@ -154,7 +163,9 @@ void shouldReturnListingNotesWhenRelevantListingNotesExist(){ null, "20", "1", - 20); + 20, + null, + "MAGISTRATES"); verify(courtSchedulerServiceAdapter).hearingSlotsSearch(any(Map.class)); verify(notesService).findNotes(any(List.class)); @@ -176,6 +187,63 @@ void shouldReturnListingNotesWhenRelevantListingNotesExist(){ } + @Test + void searchHearingSlots_passesStatusAndJurisdictionToAdapter() { + when(courtSchedulerServiceAdapter.hearingSlotsSearch(any(Map.class))).thenReturn(response); + when(notesService.findNotes(any(List.class))).thenReturn(new ArrayList()); + + queryApiHearingSlotsResource.getHearingSlots("ADULT", + "2017-10-11", + "2020-10-11", + null, + "BAOOUS", + "BAOOUS", + "001c067d-eaca-4ce5-ad90-a366ef3e4bb6", + "1234", + "BYS", + "AM", + null, + null, + "20", + "1", + 20, + "FINAL", + "MAGISTRATES"); + + verify(courtSchedulerServiceAdapter).hearingSlotsSearch(paramsCaptor.capture()); + final Map params = paramsCaptor.getValue(); + assertEquals("FINAL", params.get("status")); + assertEquals("MAGISTRATES", params.get("jurisdiction")); + } + + @Test + void searchHearingSlots_defaultsStatusToAllWhenNull() { + when(courtSchedulerServiceAdapter.hearingSlotsSearch(any(Map.class))).thenReturn(response); + when(notesService.findNotes(any(List.class))).thenReturn(new ArrayList()); + + queryApiHearingSlotsResource.getHearingSlots("ADULT", + "2017-10-11", + "2020-10-11", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "20", + "1", + null, + null, + null); + + verify(courtSchedulerServiceAdapter).hearingSlotsSearch(paramsCaptor.capture()); + final Map params = paramsCaptor.getValue(); + assertEquals("ALL", params.get("status")); + } + private JsonObject createJsonObject() { final String payload = FileUtil.getPayload(AZURE_RESULT); return new StringToJsonObjectConverter().convert(payload); diff --git a/listing-query/listing-query-api/src/test/java/uk/gov/justice/api/resource/DefaultQueryApiSessionAvailabilityValidationResourceTest.java b/listing-query/listing-query-api/src/test/java/uk/gov/justice/api/resource/DefaultQueryApiSessionAvailabilityValidationResourceTest.java new file mode 100644 index 000000000..fb7fac072 --- /dev/null +++ b/listing-query/listing-query-api/src/test/java/uk/gov/justice/api/resource/DefaultQueryApiSessionAvailabilityValidationResourceTest.java @@ -0,0 +1,71 @@ +package uk.gov.justice.api.resource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import uk.gov.moj.cpp.listing.common.service.CourtSchedulerServiceAdapter; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.ws.rs.core.Response; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DefaultQueryApiSessionAvailabilityValidationResourceTest { + + @Mock + private CourtSchedulerServiceAdapter courtSchedulerServiceAdapter; + + @InjectMocks + private DefaultQueryApiSessionAvailabilityValidationResource resource; + + @Captor + private ArgumentCaptor payloadCaptor; + + @Test + void shouldForwardPayloadToAdapterAndReturnResponse() { + final JsonObject requestPayload = Json.createObjectBuilder() + .add("courtScheduleIdList", Json.createArrayBuilder() + .add(Json.createObjectBuilder().add("courtScheduleId", "f8254db1-1683-483e-afb3-b87fde5a0a26"))) + .add("duration", 30) + .build(); + final Response adapterResponse = Response.status(Response.Status.OK).entity(Json.createObjectBuilder().build()).build(); + + when(courtSchedulerServiceAdapter.validateSessionAvailability(requestPayload)).thenReturn(adapterResponse); + + final Response result = resource.validateSessionAvailability(requestPayload); + + verify(courtSchedulerServiceAdapter).validateSessionAvailability(payloadCaptor.capture()); + assertEquals(requestPayload, payloadCaptor.getValue()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus()); + } + + @Test + void shouldReturnAdapterErrorResponseUnchanged() { + final JsonObject requestPayload = Json.createObjectBuilder() + .add("courtScheduleIdList", Json.createArrayBuilder() + .add(Json.createObjectBuilder().add("courtScheduleId", "f8254db1-1683-483e-afb3-b87fde5a0a26"))) + .build(); + final JsonObject errorBody = Json.createObjectBuilder() + .add("validationResult", Json.createObjectBuilder() + .add("status", "FAILURE") + .add("validationError", "duration is required")) + .build(); + final Response adapterResponse = Response.status(Response.Status.BAD_REQUEST).entity(errorBody).build(); + + when(courtSchedulerServiceAdapter.validateSessionAvailability(requestPayload)).thenReturn(adapterResponse); + + final Response result = resource.validateSessionAvailability(requestPayload); + + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus()); + assertEquals(errorBody, result.getEntity()); + } +} diff --git a/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/QueryAccessControlTest.java b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/QueryAccessControlTest.java index fec1534d4..75f73e8a6 100644 --- a/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/QueryAccessControlTest.java +++ b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/QueryAccessControlTest.java @@ -116,6 +116,12 @@ public void shouldBeAsExpectedForListingSearchHearing() { COURT_CLERKS, COURT_ADMINISTRATORS, CROWN_COURT_ADMIN, LISTING_OFFICERS, LEGAL_ADVISERS, SYSTEM_USERS, COURT_ASSOCIATE,OPERATIONAL_DELIVERY_ADMIN); } + @Test + void shouldBeAsExpectedForSearchAvailableJudiciaries() { + assertAccessAsExpected("listing.search.available.judiciaries", + LISTING_OFFICERS, CROWN_COURT_ADMIN, COURT_ADMINISTRATORS, COURT_CLERKS, LEGAL_ADVISERS, COURT_ASSOCIATE); + } + @Test public void shouldAllowAuthorisedUserToGetAllocatedAndUnallocatedHearings() { final Action action = createActionFor(ACTION_ALLOCATED_AND_UNALLOCATED_HEARINGS); diff --git a/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/api/HearingQueryApiTest.java b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/api/HearingQueryApiTest.java index 3a550dfde..cb4987dc1 100644 --- a/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/api/HearingQueryApiTest.java +++ b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/api/HearingQueryApiTest.java @@ -59,6 +59,7 @@ import java.io.StringReader; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -138,9 +139,14 @@ public class HearingQueryApiTest { @BeforeEach public void setup() { - apiMethodsToHandlerNames = stream(HearingQueryApi.class.getMethods()) + apiMethodsToHandlerNames = new HashMap<>(stream(HearingQueryApi.class.getMethods()) .filter(method -> method.getAnnotation(Handles.class) != null) - .collect(toMap(Method::getName, method -> method.getAnnotation(Handles.class).value())); + .collect(toMap(Method::getName, method -> method.getAnnotation(Handles.class).value()))); + stream(SearchAvailableJudiciariesQueryHandler.class.getMethods()) + .filter(method -> method.getAnnotation(Handles.class) != null) + .forEach(method -> apiMethodsToHandlerNames.put( + method.getName(), + method.getAnnotation(Handles.class).value())); } @Test diff --git a/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/api/SearchAvailableJudiciariesQueryHandlerTest.java b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/api/SearchAvailableJudiciariesQueryHandlerTest.java new file mode 100644 index 000000000..66858e4b2 --- /dev/null +++ b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/api/SearchAvailableJudiciariesQueryHandlerTest.java @@ -0,0 +1,79 @@ +package uk.gov.moj.cpp.listing.query.api; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom; +import static uk.gov.justice.services.messaging.spi.DefaultJsonMetadata.metadataBuilder; +import static uk.gov.justice.services.test.utils.core.matchers.JsonEnvelopeMatcher.jsonEnvelope; +import static uk.gov.justice.services.test.utils.core.matchers.JsonEnvelopeMetadataMatcher.metadata; +import static uk.gov.justice.services.test.utils.core.matchers.JsonEnvelopePayloadMatcher.payloadIsJson; + +import uk.gov.justice.services.messaging.JsonEnvelope; +import uk.gov.moj.cpp.listing.common.service.CourtSchedulerSearchService; + +import java.util.UUID; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + +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; + +@ExtendWith(MockitoExtension.class) +class SearchAvailableJudiciariesQueryHandlerTest { + + @Mock + private CourtSchedulerSearchService courtSchedulerSearchService; + + @InjectMocks + private SearchAvailableJudiciariesQueryHandler handler; + + @Test + void searchAvailableJudiciaries_returnsEnvelopeWhenCourtSchedulerOk() { + final JsonObject payload = Json.createObjectBuilder().add("search", "ai").build(); + final JsonEnvelope query = envelopeFrom( + metadataBuilder().withId(UUID.randomUUID()).withName("listing.search.available.judiciaries"), + payload); + final JsonObject courtSchedulerBody = Json.createObjectBuilder() + .add("judiciaries", Json.createArrayBuilder().build()) + .build(); + when(courtSchedulerSearchService.searchAvailableJudiciaries(any())) + .thenReturn(Response.ok(courtSchedulerBody).build()); + + final JsonEnvelope result = handler.searchAvailableJudiciaries(query); + + assertThat(result, jsonEnvelope( + metadata().withName("listing.search.available.judiciaries"), + payloadIsJson(allOf(withJsonPath("$.judiciaries.length()", equalTo(0)))))); + verify(courtSchedulerSearchService).searchAvailableJudiciaries(argThat(m -> + "ai".equals(m.get("search")))); + } + + @Test + void searchAvailableJudiciaries_propagatesNonOkStatus() { + final JsonObject payload = Json.createObjectBuilder().add("search", "x").build(); + final JsonEnvelope query = envelopeFrom( + metadataBuilder().withId(UUID.randomUUID()).withName("listing.search.available.judiciaries"), + payload); + when(courtSchedulerSearchService.searchAvailableJudiciaries(any())) + .thenReturn(Response.status(Response.Status.BAD_REQUEST).entity("bad").build()); + + final WebApplicationException ex = assertThrows(WebApplicationException.class, + () -> handler.searchAvailableJudiciaries(query)); + + assertThat(ex.getResponse().getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode())); + } +} diff --git a/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/api/service/AlphabeticalCourtListServiceTest.java b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/api/service/AlphabeticalCourtListServiceTest.java index 198519027..ffd53aab3 100644 --- a/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/api/service/AlphabeticalCourtListServiceTest.java +++ b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/api/service/AlphabeticalCourtListServiceTest.java @@ -991,60 +991,6 @@ private JsonEnvelope buildRequestEnvelopeWithRestrictedDefendant() { queryPayload); } - @Test - public void shouldUseSubjectAsDefendantWhenCourtApplicationHasSubject() { - final JsonEnvelope envelope = buildRequestEnvelopeWithSubjectInCourtApplication(); - when(courtCentreFactory.getCourtCentre(COURT_CENTRE_ID, envelope)).thenReturn(getCourtCentreDetails(false)); - - final Optional listJson = service.buildAlphabeticalCourtListData(envelope, COURT_CENTRE_ID.toString()); - - assertThat(listJson.orElse(createObjectBuilder().build()).toString(), isJson(allOf( - withJsonPath("$.defendants", hasSize(1)), - withJsonPath("$.defendants[0].defendantFullName", equalTo(upperCase(LAST_NAME_6) + "," + SPACE + FIRST_NAME_6)), - withJsonPath("$.defendants[0].caseReference", equalTo(APPLICATION_REFERENCE_1)) - ))); - verify(courtCentreFactory).getCourtCentre(eq(COURT_CENTRE_ID), eq(envelope)); - } - - private JsonEnvelope buildRequestEnvelopeWithSubjectInCourtApplication() { - final JsonObject queryPayload = createObjectBuilder() - .add("hearings", createArrayBuilder().add(createObjectBuilder() - .add("hearingDate", to(HEARING_DATE)) - .add("hearingsByHearingDate", createArrayBuilder().add(createObjectBuilder() - .add("hearing", getHearingBuilder(emptyMap()) - .add("allocated", true) - .add("hearingDays", generateHearingDays(START_DATE_TIME_1)) - .add("courtApplications", generateCourtApplicationsWithSubject()) - ) - )) - .build()).build()).build(); - - return envelopeFrom( - metadataOf(randomUUID(), QUERY_NAME) - .withUserId(randomUUID().toString()) - .build(), - queryPayload); - } - - private JsonArrayBuilder generateCourtApplicationsWithSubject() { - return createArrayBuilder().add(createObjectBuilder() - .add("id", randomUUID().toString()) - .add("applicationReference", APPLICATION_REFERENCE_1) - .add("restrictFromCourtList", FALSE) - .add("respondents", createArrayBuilder()) - .add("applicant", createObjectBuilder() - .add("firstName", FIRST_NAME_4) - .add("lastName", LAST_NAME_4) - .add("restrictFromCourtList", FALSE) - .add("courtApplicationPartyType", "PERSON")) - .add("subject", createObjectBuilder() - .add("firstName", FIRST_NAME_6) - .add("lastName", LAST_NAME_6) - .add("restrictFromCourtList", FALSE) - .add("courtApplicationPartyType", "PERSON")) - ); - } - private CourtCentreDetails getCourtCentreDetails(final Boolean welsh) { final CourtRoomDetails courtRoomDetails = courtRoomDetails() .withCourtRoomName(COURT_ROOM_NAME).withWelshCourtRoomName(COURT_ROOM_NAME_WELSH) diff --git a/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/document/generator/DocumentGeneratorClientTest.java b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/document/generator/DocumentGeneratorClientTest.java index d5ce7c944..30113b7e9 100644 --- a/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/document/generator/DocumentGeneratorClientTest.java +++ b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/document/generator/DocumentGeneratorClientTest.java @@ -19,7 +19,6 @@ import org.junit.jupiter.api.Assertions; 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; @@ -37,11 +36,9 @@ public class DocumentGeneratorClientTest { @Mock private JsonObject documentPayload; - @InjectMocks - private DocumentGeneratorClient client; - @Test public void shouldGenerateDocument() throws IOException { + final DocumentGeneratorClient client = new DocumentGeneratorClient(documentGeneratorClientProducer, systemUserProvider); when(documentGeneratorClientProducer.documentGeneratorClient()).thenReturn(documentGeneratorClient); when(documentGeneratorClient.generatePdfDocument(any(JsonObject.class), any(String.class), any(UUID.class))).thenReturn(new byte[10]); when(systemUserProvider.getContextSystemUserId()).thenReturn(Optional.of(randomUUID())); @@ -51,6 +48,7 @@ public void shouldGenerateDocument() throws IOException { @Test public void shouldGenerateDocumentThrowException() throws IOException { + final DocumentGeneratorClient client = new DocumentGeneratorClient(documentGeneratorClientProducer, systemUserProvider); when(documentGeneratorClientProducer.documentGeneratorClient()).thenReturn(documentGeneratorClient); when(documentGeneratorClient.generatePdfDocument(any(JsonObject.class), any(String.class), any(UUID.class))).thenThrow(new IOException()); when(systemUserProvider.getContextSystemUserId()).thenReturn(Optional.of(randomUUID())); diff --git a/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/document/generator/PublicCourtListAssemblerTest.java b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/document/generator/PublicCourtListAssemblerTest.java index 7451edcc0..89938f834 100644 --- a/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/document/generator/PublicCourtListAssemblerTest.java +++ b/listing-query/listing-query-api/src/test/java/uk/gov/moj/cpp/listing/query/document/generator/PublicCourtListAssemblerTest.java @@ -294,28 +294,6 @@ public void shouldBuildOnlyApplicationHearingPublicCourtListDataWithRestrictedDe assertRestrictedDefendant(publicListData, true); } - @Test - public void shouldUseSubjectAsDefendantWhenCourtApplicationHasSubject() { - when(courtCentreFactory.getCourtCentre(eq(COURT_CENTRE_ID), any(JsonEnvelope.class))) - .thenReturn(generateCourtCentreDetails(NOT_WELSH)); - when(referenceDataService.getJudiciariesByIdList(eq(singletonList(JUDICIARY_ID)), any(JsonEnvelope.class))) - .thenReturn(generateJudiciaryEnvelope()); - - final String jsonString = getFileContentWithCommonFieldsReplaced("stubbed.queryView.getOnlyApplicationHearingPublicCourtListDataWithSubject.json") - .replace("JUDICIARY_ID", JUDICIARY_ID.toString()); - final JsonObject publicListData = publicListService.assemble(buildJsonEnvelope(convertToJsonObject(jsonString)), - COURT_CENTRE_ID.toString(), COURT_ROOM_1_ID.toString(), CourtListType.PUBLIC, FALSE, FALSE).get(); - - final JsonObject hearingDateJo = publicListData.getJsonArray("hearingDates").getJsonObject(0); - final JsonObject timeslot = hearingDateJo.getJsonArray("courtRooms").getJsonObject(0).getJsonArray("timeslots").getJsonObject(0); - final JsonObject hearing = timeslot.getJsonArray("hearings").getJsonObject(0); - - assertThat(hearing.getJsonArray("defendants").size(), is(1)); - final JsonObject defendant = hearing.getJsonArray("defendants").getJsonObject(0); - assertThat(defendant.getString("firstName"), is(FIRST_NAME3)); - assertThat(defendant.getString("surname"), is(LAST_NAME3)); - } - @Test public void shouldBuildDataForPublicCourtListBST() { when(courtCentreFactory.getCourtCentre(eq(COURT_CENTRE_ID), any(JsonEnvelope.class))) diff --git a/listing-query/listing-query-api/src/test/resources/listing.validate.session.availability.json b/listing-query/listing-query-api/src/test/resources/listing.validate.session.availability.json new file mode 100644 index 000000000..69446483a --- /dev/null +++ b/listing-query/listing-query-api/src/test/resources/listing.validate.session.availability.json @@ -0,0 +1,11 @@ +{ + "courtScheduleIdList": [ + { + "courtScheduleId": "f8254db1-1683-483e-afb3-b87fde5a0a26" + }, + { + "courtScheduleId": "9e4932f7-97b2-3010-b942-ddd2624e4dd8" + } + ], + "duration": 30 +} diff --git a/listing-query/listing-query-api/src/test/resources/stubbed.queryView.getOnlyApplicationHearingPublicCourtListDataWithSubject.json b/listing-query/listing-query-api/src/test/resources/stubbed.queryView.getOnlyApplicationHearingPublicCourtListDataWithSubject.json deleted file mode 100644 index a9cf7085b..000000000 --- a/listing-query/listing-query-api/src/test/resources/stubbed.queryView.getOnlyApplicationHearingPublicCourtListDataWithSubject.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "hearings": [ - { - "judiciary": [ - { - "judicialId": "JUDICIARY_ID" - } - ], - "courtCentreId": "COURT_CENTRE_ID", - "hearingsByCourtCentreId": [ - { - "hearingDate": "START_DATE1", - "hearingsByHearingDate": [ - { - "hearing": { - "id": "27c27688-2a0c-407e-bcad-f40de196b295", - "type": { - "id": "633224da-6db3-4df5-8097-62cee52f7a61", - "description": "Bail Application" - }, - "endDate": "2024-11-01", - "allocated": true, - "judiciary": [], - "startDate": "2024-11-01", - "courtRoomId": "COURT_ROOM_ID", - "hearingDays": [ - { - "endTime": "2018-11-20T14:37:00.000Z", - "sequence": 1, - "startTime": "2018-11-21T12:37:00.000Z", - "hearingDate": "2024-11-01", - "durationMinutes": 1 - } - ], - "listedCases": [], - "courtCentreId": "f8254db1-1683-483e-afb3-b87fde5a0a26", - "isSlotsBooked": true, - "nonDefaultDays": [ - { - "oucode": "B01LY00", - "roomId": "b4562684-9209-3ec4-a544-7f80dabd94d8", - "session": "PM", - "duration": 1, - "startTime": "2024-11-01T14:00:00.000Z", - "courtRoomId": 2331, - "courtCentreId": "f8254db1-1683-483e-afb3-b87fde5a0a26", - "courtScheduleId": "59080b16-9c7b-3a20-9492-1545c672ba80" - } - ], - "nonSittingDays": [], - "hearingLanguage": "ENGLISH", - "estimatedMinutes": 20, - "jurisdictionType": "MAGISTRATES", - "courtApplications": [ - { - "id": "bb009505-7ed8-43ca-862a-392e6407b199", - "offences": [], - "applicant": { - "id": "e13d9811-7657-4b2e-a0e6-b1e82642acea", - "address": { - "address1": "799", - "address2": "StreetDescription", - "address3": "Locality2O", - "postcode": "CR0 1XG" - }, - "lastName": "LAST_NAME1", - "firstName": "FIRST_NAME1", - "isRespondent": false, - "restrictFromCourtList": false, - "courtApplicationPartyType": "PERSON_DEFENDANT" - }, - "subject": { - "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "lastName": "LAST_NAME3", - "firstName": "FIRST_NAME3", - "restrictFromCourtList": false, - "courtApplicationPartyType": "PERSON_DEFENDANT" - }, - "respondents": [], - "linkedCaseIds": [ - "b27e91d5-2278-433a-a8dc-1cd62e365902" - ], - "applicationType": "Application for bail following committal/sending/transfer in custody to appear at Crown Court", - "applicationReference": "CASE_REFERENCE1", - "restrictFromCourtList": false, - "applicationParticulars": "aa", - "restrictCourtApplicationType": false - } - ], - "courtCentreDetails": { - "id": "COURT_CENTRE_ID", - "defaultDuration": 420, - "defaultStartTime": "10:00:00" - } - } - } - ] - } - ] - } - ] -} diff --git a/listing-query/listing-query-view/pom.xml b/listing-query/listing-query-view/pom.xml index e4ae9bf27..bc85c1326 100644 --- a/listing-query/listing-query-view/pom.xml +++ b/listing-query/listing-query-view/pom.xml @@ -4,7 +4,7 @@ uk.gov.moj.cpp.listing listing-query - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT listing-query-view diff --git a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/HearingQueryView.java b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/HearingQueryView.java index b287971a5..0b0d01d94 100644 --- a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/HearingQueryView.java +++ b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/HearingQueryView.java @@ -64,6 +64,7 @@ import uk.gov.moj.cpp.listing.query.view.hearing.HearingToJsonConverter; import uk.gov.moj.cpp.listing.query.view.service.JsonNodeReader; import uk.gov.moj.cpp.listing.query.view.service.NotesService; +import uk.gov.moj.cpp.listing.query.view.service.SessionJudiciaryEnrichmentService; import uk.gov.moj.cpp.listing.query.view.service.csv.HearingCsvReportService; import java.io.IOException; @@ -192,6 +193,9 @@ public class HearingQueryView { @Inject private HearingCsvReportService hearingCsvReportService; + @Inject + private SessionJudiciaryEnrichmentService sessionJudiciaryEnrichmentService; + public static final String TYPE = "type"; public static final String LISTING_ALLOCATED_AND_UNALLOCATED_HEARINGS = "listing.allocated.and.unallocated.hearings"; @@ -239,6 +243,8 @@ public JsonEnvelope searchHearings(final JsonEnvelope query) { LOGGER.info("number of records from query - {}", filteredHearings.size()); + sessionJudiciaryEnrichmentService.enrichWithSessionJudiciary(filteredHearings); + final List notes = notesService.findNotes(allocated, courtRoomId, searchDate, filteredHearings); return envelopeFrom(metadataFrom(query.metadata()).withName("listing.search.hearings"), diff --git a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java index 59c067506..1614cbe5d 100644 --- a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java +++ b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQuery.java @@ -32,6 +32,7 @@ import uk.gov.moj.cpp.listing.query.view.dto.RangeSearchQueryParams; import uk.gov.moj.cpp.listing.query.view.hearing.HearingJsonListConverterFilterEjectCases; import uk.gov.moj.cpp.listing.query.view.service.NotesService; +import uk.gov.moj.cpp.listing.query.view.service.SessionJudiciaryEnrichmentService; import java.time.DayOfWeek; import java.time.Instant; @@ -91,6 +92,9 @@ public class RangeSearchQuery { @Inject private PaginationParameterFactory paginationParameterFactory; + @Inject + private SessionJudiciaryEnrichmentService sessionJudiciaryEnrichmentService; + public JsonEnvelope rangeSearchHearingsForJudgeList(final JsonEnvelope query) { final String courtCentreId = query.payloadAsJsonObject().getString(COURT_CENTRE_ID, null); final String courtRoomId = query.payloadAsJsonObject().getString(COURT_ROOM_ID, null); @@ -168,6 +172,7 @@ private JsonEnvelope buildHearingsResponse(final JsonEnvelope query, final Long totalCount, final HearingIdsResponse hearingIdsResponse, final PaginationParameter paginationParameter) { + sessionJudiciaryEnrichmentService.enrichWithSessionJudiciary(hearings); final List notes = notesService.findNotes(allocated, courtRoomId, startDate, hearings); return envelopeFrom(metadataFrom(query.metadata()).withName("listing.search.hearings"), diff --git a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/courtlist/CourtListsBuilder.java b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/courtlist/CourtListsBuilder.java index e73642917..c7ee81650 100644 --- a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/courtlist/CourtListsBuilder.java +++ b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/courtlist/CourtListsBuilder.java @@ -54,7 +54,7 @@ public CourtListsBuilder prepareEmptyCourtSiteHearings(final UUID courtCentreId) public CourtListsBuilder assignHearingsToCourtSitesUsingCourtRoom(final UUID courtCentreId, final List flatHearings) { for (final FlatHearing flatHearing : flatHearings) { if(LOGGER.isInfoEnabled()) { - LOGGER.info("courtCentreId={}, courtRoomId={}, flatHearingId={}", courtCentreId, !flatHearing.getCourtRoomId().isEmpty()?flatHearing.getCourtRoomId().get():"No Value", nonNull(flatHearing.getCaseHearings()) && flatHearing.getCaseHearings().containsKey("id")?flatHearing.getCaseHearings().getString("id"):"No Value"); + LOGGER.info("courtCentreId={}, courtRoomId={}, flatHearingId={}", courtCentreId, flatHearing.getCourtRoomId().map(UUID::toString).orElse("No Value"), nonNull(flatHearing.getCaseHearings()) && flatHearing.getCaseHearings().containsKey("id")?flatHearing.getCaseHearings().getString("id"):"No Value"); } final String crestCourtSiteCode = getCrestCourtSiteCodeForCourtRoom(courtCentreId, flatHearing.getCourtRoomId()); crestCourtSiteCodeHearingsMap.get(crestCourtSiteCode).add(flatHearing); diff --git a/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/service/SessionJudiciaryEnrichmentService.java b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/service/SessionJudiciaryEnrichmentService.java new file mode 100644 index 000000000..5756fbbff --- /dev/null +++ b/listing-query/listing-query-view/src/main/java/uk/gov/moj/cpp/listing/query/view/service/SessionJudiciaryEnrichmentService.java @@ -0,0 +1,280 @@ +package uk.gov.moj.cpp.listing.query.view.service; + +import static java.util.Objects.isNull; + +import uk.gov.justice.services.common.converter.jackson.ObjectMapperProducer; +import uk.gov.moj.cpp.listing.common.service.HearingSlotsService; +import uk.gov.moj.cpp.listing.persistence.entity.Hearing; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.ws.rs.core.Response; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApplicationScoped +@SuppressWarnings({"squid:S6813"}) +public class SessionJudiciaryEnrichmentService { + + private static final Logger LOGGER = LoggerFactory.getLogger(SessionJudiciaryEnrichmentService.class); + + static final String JUDICIARY_SOURCE_HEARING = "HEARING"; + static final String JUDICIARY_SOURCE_SESSION = "SESSION"; + + private static final String JUDICIARY = "judiciary"; + private static final String JUDICIARY_SOURCE = "judiciarySource"; + private static final String HEARING_DAYS = "hearingDays"; + private static final String COURT_SCHEDULE_ID = "courtScheduleId"; + private static final String COURT_SCHEDULES = "courtSchedules"; + private static final String SESSIONS = "sessions"; + private static final String COURT_SCHEDULE_IDS_PARAM = "courtScheduleIds"; + + private static final String JUDICIARY_ID = "id"; + private static final String JUDICIAL_ID = "judicialId"; + private static final String JUDICIARY_TYPE = "judiciaryType"; + private static final String IS_BENCH_CHAIRMAN = "isBenchChairman"; + private static final String IS_DEPUTY = "isDeputy"; + private static final String SEQ_ID = "seqId"; + private static final String TITLE_JUDICIAL_PREFIX = "titleJudicialPrefix"; + private static final String TITLE_JUDICIAL_PREFIX_WELSH = "titleJudicialPrefixWelsh"; + private static final String PERSON_ID = "personId"; + private static final String SPECIALISMS = "specialisms"; + private static final String REQUESTED_NAME = "requestedName"; + private static final String TITLE_PREFIX = "titlePrefix"; + private static final String SURNAME = "surname"; + private static final String FORENAMES = "forenames"; + private static final String EMAIL_ADDRESS = "emailAddress"; + + private final ObjectMapper mapper = new ObjectMapperProducer().objectMapper(); + + @Inject + HearingSlotsService hearingSlotsService; + + public void enrichWithSessionJudiciary(final List hearings) { + if (hearings == null || hearings.isEmpty()) { + return; + } + + final List needsFallback = hearings.stream() + .filter(h -> isJudiciaryEmpty(h.getProperties())) + .toList(); + + hearings.stream() + .filter(h -> !isJudiciaryEmpty(h.getProperties())) + .forEach(h -> setJudiciarySource(h.getProperties(), JUDICIARY_SOURCE_HEARING)); + + if (needsFallback.isEmpty()) { + return; + } + + final Set allCourtScheduleIds = new LinkedHashSet<>(); + for (final Hearing hearing : needsFallback) { + allCourtScheduleIds.addAll(extractCourtScheduleIds(hearing.getProperties())); + } + + if (allCourtScheduleIds.isEmpty()) { + LOGGER.debug("No courtScheduleIds found in hearingDays for {} hearing(s) needing judiciary fallback", needsFallback.size()); + needsFallback.forEach(h -> setJudiciarySource(h.getProperties(), JUDICIARY_SOURCE_SESSION)); + return; + } + + final Map judiciaryByScheduleId = fetchJudiciaryByScheduleId(allCourtScheduleIds); + + for (final Hearing hearing : needsFallback) { + final JsonArray judiciary = resolveJudiciaryForHearing(hearing.getProperties(), judiciaryByScheduleId); + if (judiciary != null && !judiciary.isEmpty()) { + injectJudiciary((ObjectNode) hearing.getProperties(), judiciary); + } + setJudiciarySource(hearing.getProperties(), JUDICIARY_SOURCE_SESSION); + } + } + + private boolean isJudiciaryEmpty(final JsonNode props) { + if (isNull(props)) { + return true; + } + final JsonNode judiciary = props.get(JUDICIARY); + return isNull(judiciary) || !judiciary.isArray() || judiciary.isEmpty(); + } + + private List extractCourtScheduleIds(final JsonNode props) { + final List ids = new ArrayList<>(); + if (isNull(props)) { + return ids; + } + final JsonNode hearingDays = props.path(HEARING_DAYS); + if (hearingDays.isMissingNode() || !hearingDays.isArray()) { + return ids; + } + hearingDays.forEach(day -> { + final JsonNode csId = day.get(COURT_SCHEDULE_ID); + if (csId != null && !csId.isNull() && !csId.asText().isBlank()) { + ids.add(csId.asText()); + } + }); + return ids; + } + + Map fetchJudiciaryByScheduleId(final Set courtScheduleIds) { + final Response response = getCourtSchedulesResponse(courtScheduleIds); + if (response == null || response.getStatus() != Response.Status.OK.getStatusCode()) { + if (response != null) { + LOGGER.warn("getCourtSchedulesById returned HTTP {} for courtScheduleIds: {}", response.getStatus(), courtScheduleIds); + } + return Map.of(); + } + + final JsonArray schedulesArray = getCourtSchedulesArray(response); + if (schedulesArray == null || schedulesArray.isEmpty()) { + return Map.of(); + } + + return extractJudiciaryByScheduleId(courtScheduleIds, schedulesArray); + } + + private Map extractJudiciaryByScheduleId(final Set courtScheduleIds, final JsonArray schedulesArray) { + final Map result = new HashMap<>(); + for (int i = 0; i < schedulesArray.size(); i++) { + final JsonObject courtRoom = schedulesArray.getJsonObject(i); + final JsonArray sessions = courtRoom.getJsonArray(SESSIONS); + if (sessions == null || sessions.isEmpty()) { + continue; + } + addSessionJudiciaries(courtScheduleIds, result, sessions); + } + return result; + } + + private void addSessionJudiciaries(final Set courtScheduleIds, + final Map result, + final JsonArray sessions) { + for (int j = 0; j < sessions.size(); j++) { + final JsonObject session = sessions.getJsonObject(j); + final String csId = session.getString(COURT_SCHEDULE_ID, null); + if (csId == null || !courtScheduleIds.contains(csId)) { + continue; + } + final JsonArray judiciaries = session.getJsonArray("judiciaries"); + if (judiciaries != null && !judiciaries.isEmpty()) { + result.put(csId, judiciaries); + } + } + } + + private Response getCourtSchedulesResponse(final Set courtScheduleIds) { + final Map params = new HashMap<>(); + params.put(COURT_SCHEDULE_IDS_PARAM, String.join(",", courtScheduleIds)); + + try { + return hearingSlotsService.getCourtSchedulesById(params); + } catch (final RuntimeException e) { + LOGGER.warn("Could not call getCourtSchedulesById for judiciary fallback: {}", e.getMessage()); + return null; + } + } + + private JsonArray getCourtSchedulesArray(final Response response) { + final JsonObject responseJson = (JsonObject) response.getEntity(); + if (responseJson == null || responseJson.isEmpty()) { + return null; + } + return responseJson.getJsonArray(COURT_SCHEDULES); + } + + private JsonArray resolveJudiciaryForHearing(final JsonNode props, + final Map judiciaryByScheduleId) { + final List ids = extractCourtScheduleIds(props); + final Map byJudiciaryId = new HashMap<>(); + for (final String csId : ids) { + final JsonArray judiciary = judiciaryByScheduleId.get(csId); + if (judiciary == null) continue; + for (int i = 0; i < judiciary.size(); i++) { + final JsonObject j = judiciary.getJsonObject(i); + final String judiciaryId = j.getString(JUDICIARY_ID, null); + if (judiciaryId != null) { + byJudiciaryId.putIfAbsent(judiciaryId, j); + } + } + } + if (byJudiciaryId.isEmpty()) { + return null; + } + final javax.json.JsonArrayBuilder arrayBuilder = javax.json.Json.createArrayBuilder(); + byJudiciaryId.values().forEach(arrayBuilder::add); + return arrayBuilder.build(); + } + + private void injectJudiciary(final ObjectNode propertiesNode, final JsonArray courtSchedulerJudiciary) { + final ArrayNode judiciaryArray = mapper.createArrayNode(); + for (int i = 0; i < courtSchedulerJudiciary.size(); i++) { + final JsonObject j = courtSchedulerJudiciary.getJsonObject(i); + final ObjectNode role = mapper.createObjectNode(); + + role.put(JUDICIAL_ID, j.getString(JUDICIARY_ID, null)); + putStringIfPresent(j, role, JUDICIARY_TYPE); + putBooleanIfPresent(j, role, IS_BENCH_CHAIRMAN); + putBooleanIfPresent(j, role, IS_DEPUTY); + putIntIfPresent(j, role, SEQ_ID); + putStringIfPresent(j, role, TITLE_PREFIX); + putStringIfPresent(j, role, TITLE_JUDICIAL_PREFIX); + putStringIfPresent(j, role, TITLE_JUDICIAL_PREFIX_WELSH); + putStringIfPresent(j, role, PERSON_ID); + putStringIfPresent(j, role, REQUESTED_NAME); + putStringIfPresent(j, role, SURNAME); + putStringIfPresent(j, role, FORENAMES); + putStringIfPresent(j, role, EMAIL_ADDRESS); + if (j.containsKey(SPECIALISMS) && !j.isNull(SPECIALISMS)) { + final ArrayNode specialismsNode = mapper.createArrayNode(); + j.getJsonArray(SPECIALISMS).forEach(s -> { + final String val = s.getValueType() == javax.json.JsonValue.ValueType.STRING + ? ((javax.json.JsonString) s).getString() + : s.toString(); + specialismsNode.add(val); + }); + role.set(SPECIALISMS, specialismsNode); + } + + judiciaryArray.add(role); + } + propertiesNode.set(JUDICIARY, judiciaryArray); + } + + private void putStringIfPresent(final JsonObject source, final ObjectNode target, final String key) { + if (source.containsKey(key) && !source.isNull(key)) { + target.put(key, source.getString(key)); + } + } + + private void putBooleanIfPresent(final JsonObject source, final ObjectNode target, final String key) { + if (source.containsKey(key) && !source.isNull(key)) { + target.put(key, source.getBoolean(key)); + } + } + + private void putIntIfPresent(final JsonObject source, final ObjectNode target, final String key) { + if (source.containsKey(key) && !source.isNull(key)) { + target.put(key, source.getInt(key)); + } + } + + private void setJudiciarySource(final JsonNode props, final String source) { + if (props != null) { + ((ObjectNode) props).put(JUDICIARY_SOURCE, source); + } + } +} diff --git a/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/HearingQueryViewTest.java b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/HearingQueryViewTest.java index 8f5428ae8..d0ee12a3a 100644 --- a/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/HearingQueryViewTest.java +++ b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/HearingQueryViewTest.java @@ -82,6 +82,7 @@ import uk.gov.moj.cpp.listing.query.view.hearing.ApplicationTypeFilter; import uk.gov.moj.cpp.listing.query.view.hearing.HearingJsonListConverterFilterEjectCases; import uk.gov.moj.cpp.listing.query.view.service.NotesService; +import uk.gov.moj.cpp.listing.query.view.service.SessionJudiciaryEnrichmentService; import uk.gov.moj.cpp.listing.query.view.service.csv.HearingCsvReportService; import java.io.IOException; @@ -196,6 +197,9 @@ public class HearingQueryViewTest { @Mock private HearingCsvReportService hearingCsvReportService; + @Mock + private SessionJudiciaryEnrichmentService sessionJudiciaryEnrichmentService; + @Spy private JsonObjectToObjectConverter jsonObjectToObjectConverter = new JsonObjectConvertersFactory().jsonObjectToObjectConverter();; @@ -339,6 +343,7 @@ public void searchHearingsWithSearchDateWithAllParametersProvidedApartFromStartT eq(fromString(AUTHORITY_ID).toString()), eq(HEARING_TYPE_ID.toString()), eq(JURISDICTION_TYPE.toString()), eq(SEARCH_DATE), eq(SEARCH_DATE.atTime(LocalTime.MIN).atZone(UTC)), eq(SEARCH_DATE.atTime(END_TIME).atZone(UTC))); verify(hearingJsonListConverterFilterEjectCases).convertForSearchHearing(eq(hearingsJson), anyMap()); + verify(sessionJudiciaryEnrichmentService).enrichWithSessionJudiciary(eq(hearingsJson)); } @Test @@ -384,6 +389,7 @@ public void searchHearingsWithSearchDateWithAllParametersProvidedApartFromStartT final JsonEnvelope results = hearingsQueryView.searchHearings(query); assertThat(hearingsJson.size(), is(2)); assertThat(results.payloadAsJsonObject().getJsonArray("hearings").size(), is(1)); + verify(sessionJudiciaryEnrichmentService).enrichWithSessionJudiciary(eq(hearingsFilteredJson)); } @Test @@ -439,6 +445,7 @@ public void searchHearingsWithSearchDateWithAllParametersProvided() { eq(SEARCH_DATE.atTime(START_TIME).atZone(UTC)), eq(SEARCH_DATE.atTime(END_TIME).atZone(UTC))); verify(hearingJsonListConverterFilterEjectCases).convertForSearchHearing(eq(hearingsJson), anyMap()); verify(notesService, times(1)).findNotes(eq(ALLOCATED), eq(COURT_ROOM_ID.toString()), eq(SEARCH_DATE.toString()), any(List.class)); + verify(sessionJudiciaryEnrichmentService).enrichWithSessionJudiciary(eq(hearingsJson)); } @Test diff --git a/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java index e04837a0a..d88991add 100644 --- a/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java +++ b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/RangeSearchQueryTest.java @@ -41,6 +41,7 @@ import uk.gov.moj.cpp.listing.query.view.dto.PaginationParameterFactory; import uk.gov.moj.cpp.listing.query.view.hearing.HearingJsonListConverterFilterEjectCases; import uk.gov.moj.cpp.listing.query.view.service.NotesService; +import uk.gov.moj.cpp.listing.query.view.service.SessionJudiciaryEnrichmentService; import java.time.Instant; import java.time.LocalDate; @@ -145,6 +146,9 @@ public class RangeSearchQueryTest { @Mock private NotesService notesService; + @Mock + private SessionJudiciaryEnrichmentService sessionJudiciaryEnrichmentService; + @InjectMocks private RangeSearchQuery rangeSearchQuery; diff --git a/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/courtlist/CourtListsBuilderTest.java b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/courtlist/CourtListsBuilderTest.java index 5ee8aebd8..427d33f35 100644 --- a/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/courtlist/CourtListsBuilderTest.java +++ b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/courtlist/CourtListsBuilderTest.java @@ -1,14 +1,6 @@ package uk.gov.moj.cpp.listing.query.view.courtlist; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Optional.empty; -import static java.util.Optional.of; -import static java.util.UUID.randomUUID; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.mockito.Mockito.mock; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.Mockito.when; import uk.gov.moj.cpp.listing.common.xhibit.CommonXhibitReferenceDataService; @@ -16,339 +8,98 @@ import uk.gov.moj.cpp.listing.query.view.courtlist.pojo.FlatHearing; import java.time.LocalDate; +import java.util.List; import java.util.Optional; import java.util.UUID; -import javax.json.JsonArray; +import javax.json.Json; import javax.json.JsonObject; -import uk.gov.justice.services.messaging.JsonObjects; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) class CourtListsBuilderTest { - private static final UUID COURT_CENTRE_ID = randomUUID(); - private static final LocalDate START_DATE = LocalDate.of(2024, 1, 15); - private static final String END_DATE = ""; + private static final String CREST_CODE = "A"; - private CommonXhibitReferenceDataService referenceDataService; + @Mock + private CommonXhibitReferenceDataService commonXhibitReferenceDataService; + + private UUID courtCentreId; @BeforeEach void setUp() { - referenceDataService = mock(CommonXhibitReferenceDataService.class); - } - - @Test - void shouldCreateBuilderInstanceViaFactoryMethod() { - final CourtListsBuilder builder = CourtListsBuilder.forCourtCentre(referenceDataService); - - assertThat(builder, is(notNullValue())); - } - - @Test - void shouldReturnBuilderFromPrepareEmptyCourtSiteHearingsForFluentChaining() { - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"), courtSite("B"))); - - final CourtListsBuilder result = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID); - - assertThat(result, is(notNullValue())); - } - - @Test - void shouldReturnBuilderFromAssignHearingsForFluentChaining() { - final UUID roomId = randomUUID(); - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - when(referenceDataService.getCourtRoom(COURT_CENTRE_ID, roomId)) - .thenReturn(of(courtRoomMapping("A"))); - - final CourtListsBuilder result = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, asList(flatHearing(of(roomId)))); - - assertThat(result, is(notNullValue())); - } - - @Test - void shouldReturnBuilderFromGroupFlatHearingsForFluentChaining() { - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - when(referenceDataService.getDefaultCrestCourtSiteCode(COURT_CENTRE_ID)).thenReturn("A"); - - final CourtListsBuilder result = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, asList(flatHearing(empty()))) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE); - - assertThat(result, is(notNullValue())); - } - - @Test - void shouldUseDefaultCourtSiteWhenHearingHasNoCourtRoom() { - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - when(referenceDataService.getDefaultCrestCourtSiteCode(COURT_CENTRE_ID)).thenReturn("A"); - - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, asList(flatHearing(empty()))) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); - - assertThat(courtLists.size(), is(1)); - assertThat(courtLists.getJsonObject(0).getJsonArray("sittings").size(), is(1)); - } - - @Test - void shouldAssignHearingToMappedCourtSiteWhenCourtRoomIsPresent() { - final UUID roomId = randomUUID(); - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - when(referenceDataService.getCourtRoom(COURT_CENTRE_ID, roomId)) - .thenReturn(of(courtRoomMapping("A"))); - - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, asList(flatHearing(of(roomId)))) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); - - assertThat(courtLists.size(), is(1)); - assertThat(courtLists.getJsonObject(0).getJsonArray("sittings").size(), is(1)); - } - - @Test - void shouldFallBackToDefaultCourtSiteWhenCourtRoomMappingIsAbsent() { - final UUID roomId = randomUUID(); - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - when(referenceDataService.getCourtRoom(COURT_CENTRE_ID, roomId)).thenReturn(empty()); - when(referenceDataService.getDefaultCrestCourtSiteCode(COURT_CENTRE_ID)).thenReturn("A"); - - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, asList(flatHearing(of(roomId)))) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); - - assertThat(courtLists.size(), is(1)); - assertThat(courtLists.getJsonObject(0).getJsonArray("sittings").size(), is(1)); - } - - @Test - void shouldProduceOneCourtListEntryPerCourtSite() { - final UUID roomA = randomUUID(); - final UUID roomB = randomUUID(); - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"), courtSite("B"))); - when(referenceDataService.getCourtRoom(COURT_CENTRE_ID, roomA)) - .thenReturn(of(courtRoomMapping("A"))); - when(referenceDataService.getCourtRoom(COURT_CENTRE_ID, roomB)) - .thenReturn(of(courtRoomMapping("B"))); - - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, - asList(flatHearing(of(roomA)), flatHearing(of(roomB)))) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); - - assertThat(courtLists.size(), is(2)); - } - - @Test - void shouldIncludeCrestCourtSiteAndSittingsInEachCourtListEntry() { - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - when(referenceDataService.getDefaultCrestCourtSiteCode(COURT_CENTRE_ID)).thenReturn("A"); - - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, asList(flatHearing(empty()))) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); - - final JsonObject entry = courtLists.getJsonObject(0); - assertThat(entry.containsKey("crestCourtSite"), is(true)); - assertThat(entry.containsKey("sittings"), is(true)); - } - - @Test - void shouldPopulateCrestCourtSiteDataFromReferenceDataService() { - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - when(referenceDataService.getDefaultCrestCourtSiteCode(COURT_CENTRE_ID)).thenReturn("A"); - - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, asList(flatHearing(empty()))) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); - - final JsonObject crestCourtSite = courtLists.getJsonObject(0).getJsonObject("crestCourtSite"); - assertThat(crestCourtSite.getString("crestCourtSiteCode"), is("A")); - assertThat(crestCourtSite.getString("crestCourtSiteName"), is("Site A")); - } - - @Test - void shouldProduceEmptyCourtListsArrayWhenNoCourtSites() { - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(emptyList()); - - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, emptyList()) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); - - assertThat(courtLists.size(), is(0)); + courtCentreId = UUID.randomUUID(); + final JsonObject courtSite = Json.createObjectBuilder() + .add("crestCourtSiteCode", CREST_CODE) + .build(); + when(commonXhibitReferenceDataService.getCrestCourtSitesForCrownCourtCentre(courtCentreId)) + .thenReturn(List.of(courtSite)); } @Test - void shouldProduceCourtListEntryWithNoSittingsWhenNoHearingsAssigned() { - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, emptyList()) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); - - assertThat(courtLists.size(), is(1)); - assertThat(courtLists.getJsonObject(0).getJsonArray("sittings").size(), is(0)); - } + void shouldAssignHearingToCourtSiteWhenCourtRoomIdIsPresent() { + // Given — a court room that maps to CREST_CODE + final UUID courtRoomId = UUID.randomUUID(); + final CourtRoomMapping mapping = new CourtRoomMapping.Builder() + .withCrestCourtSiteCode(CREST_CODE) + .build(); + when(commonXhibitReferenceDataService.getCourtRoom(courtCentreId, courtRoomId)) + .thenReturn(Optional.of(mapping)); - @Test - void shouldGroupHearingsWithSameDateAndCourtRoomIntoOneSitting() { - final UUID roomId = randomUUID(); - final UUID judicialId = randomUUID(); - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - when(referenceDataService.getCourtRoom(COURT_CENTRE_ID, roomId)) - .thenReturn(of(courtRoomMapping("A"))); + // FlatHearing with a present Optional courtRoomId + // exercises: getCourtRoomId().map(UUID::toString) path of the changed log line + final FlatHearing flatHearing = new FlatHearing( + LocalDate.now(), null, Optional.of(courtRoomId), null, false); - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, - asList( - flatHearingWithJudiciary(of(roomId), judicialId), - flatHearingWithJudiciary(of(roomId), judicialId), - flatHearingWithJudiciary(of(roomId), judicialId))) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); + final CourtListsBuilder builder = CourtListsBuilder.forCourtCentre(commonXhibitReferenceDataService) + .prepareEmptyCourtSiteHearings(courtCentreId); - final JsonArray sittings = courtLists.getJsonObject(0).getJsonArray("sittings"); - assertThat(sittings.size(), is(1)); - assertThat(sittings.getJsonObject(0).getJsonArray("hearings").size(), is(3)); + // When / Then + assertDoesNotThrow(() -> + builder.assignHearingsToCourtSitesUsingCourtRoom(courtCentreId, List.of(flatHearing))); } @Test - void shouldCreateSeparateSittingsForHearingsInDifferentCourtRooms() { - final UUID roomA = randomUUID(); - final UUID roomB = randomUUID(); - final UUID judicialId = randomUUID(); - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - when(referenceDataService.getCourtRoom(COURT_CENTRE_ID, roomA)) - .thenReturn(of(courtRoomMapping("A"))); - when(referenceDataService.getCourtRoom(COURT_CENTRE_ID, roomB)) - .thenReturn(of(courtRoomMapping("A"))); - - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, - asList( - flatHearingWithJudiciary(of(roomA), judicialId), - flatHearingWithJudiciary(of(roomB), judicialId))) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); - - final JsonArray sittings = courtLists.getJsonObject(0).getJsonArray("sittings"); - assertThat(sittings.size(), is(2)); - } + void shouldAssignHearingToDefaultCourtSiteWhenCourtRoomIdIsAbsent() { + // Given — no courtRoomId, falls back to default + when(commonXhibitReferenceDataService.getDefaultCrestCourtSiteCode(courtCentreId)) + .thenReturn(CREST_CODE); - @Test - void shouldIncludeSittingDateInEachSittingEntry() { - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - when(referenceDataService.getDefaultCrestCourtSiteCode(COURT_CENTRE_ID)).thenReturn("A"); + // FlatHearing with an empty Optional courtRoomId + // exercises: getCourtRoomId().orElse("No Value") path of the changed log line + final FlatHearing flatHearing = new FlatHearing( + LocalDate.now(), null, Optional.empty(), null, false); - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, asList(flatHearing(empty()))) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); + final CourtListsBuilder builder = CourtListsBuilder.forCourtCentre(commonXhibitReferenceDataService) + .prepareEmptyCourtSiteHearings(courtCentreId); - final JsonObject sitting = courtLists.getJsonObject(0).getJsonArray("sittings").getJsonObject(0); - assertThat(sitting.getString("sittingDate"), is(START_DATE.toString())); + // When / Then + assertDoesNotThrow(() -> + builder.assignHearingsToCourtSitesUsingCourtRoom(courtCentreId, List.of(flatHearing))); } @Test - void shouldIncludeHearingTypeInEachHearing() { - when(referenceDataService.getCrestCourtSitesForCrownCourtCentre(COURT_CENTRE_ID)) - .thenReturn(asList(courtSite("A"))); - when(referenceDataService.getDefaultCrestCourtSiteCode(COURT_CENTRE_ID)).thenReturn("A"); + void shouldFallBackToDefaultCourtSiteWhenCourtRoomHasNoMapping() { + // Given — courtRoomId present but no mapping found + final UUID unmappedRoomId = UUID.randomUUID(); + when(commonXhibitReferenceDataService.getCourtRoom(courtCentreId, unmappedRoomId)) + .thenReturn(Optional.empty()); + when(commonXhibitReferenceDataService.getDefaultCrestCourtSiteCode(courtCentreId)) + .thenReturn(CREST_CODE); - final JsonArray courtLists = CourtListsBuilder.forCourtCentre(referenceDataService) - .prepareEmptyCourtSiteHearings(COURT_CENTRE_ID) - .assignHearingsToCourtSitesUsingCourtRoom(COURT_CENTRE_ID, asList(flatHearing(empty()))) - .groupFlatHearingsIntoSittings(START_DATE, END_DATE) - .buildCourtListsArray(COURT_CENTRE_ID); + final FlatHearing flatHearing = new FlatHearing( + LocalDate.now(), null, Optional.of(unmappedRoomId), null, false); - final JsonObject hearing = courtLists.getJsonObject(0) - .getJsonArray("sittings").getJsonObject(0) - .getJsonArray("hearings").getJsonObject(0); - assertThat(hearing.getJsonObject("hearingType").getString("description"), is("Trial")); - } + final CourtListsBuilder builder = CourtListsBuilder.forCourtCentre(commonXhibitReferenceDataService) + .prepareEmptyCourtSiteHearings(courtCentreId); - // ─── helpers ──────────────────────────────────────────────────────────────── - - private JsonObject courtSite(final String code) { - return JsonObjects.createObjectBuilder() - .add("crestCourtSiteCode", code) - .add("crestCourtSiteName", "Site " + code) - .build(); - } - - private CourtRoomMapping courtRoomMapping(final String crestCourtSiteCode) { - return new CourtRoomMapping.Builder().withCrestCourtSiteCode(crestCourtSiteCode).build(); - } - - private FlatHearing flatHearing(final Optional courtRoomId) { - return flatHearingWithJudiciary(courtRoomId, randomUUID()); - } - - private FlatHearing flatHearingWithJudiciary(final Optional courtRoomId, final UUID judicialId) { - final JsonObject caseHearings = buildWeekCommencingCaseHearings(judicialId); - return new FlatHearing( - START_DATE, - caseHearings.getJsonArray("judiciary"), - courtRoomId, - caseHearings, - true); - } - - private JsonObject buildWeekCommencingCaseHearings(final UUID judicialId) { - return JsonObjects.createObjectBuilder() - .add("weekCommencingStartDate", START_DATE.toString()) - .add("weekCommencingEndDate", "2024-01-19") - .add("type", JsonObjects.createObjectBuilder() - .add("description", "Trial") - .build()) - .add("judiciary", JsonObjects.createArrayBuilder() - .add(JsonObjects.createObjectBuilder() - .add("judicialId", judicialId.toString()))) - .add("listedCases", JsonObjects.createArrayBuilder() - .add(JsonObjects.createObjectBuilder() - .add("restrictFromCourtList", false) - .add("caseIdentifier", JsonObjects.createObjectBuilder() - .add("caseReference", "T12345") - .build()) - .add("defendants", JsonObjects.createArrayBuilder().build()))) - .build(); + // When / Then + assertDoesNotThrow(() -> + builder.assignHearingsToCourtSitesUsingCourtRoom(courtCentreId, List.of(flatHearing))); } } diff --git a/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/service/SessionJudiciaryEnrichmentServiceTest.java b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/service/SessionJudiciaryEnrichmentServiceTest.java new file mode 100644 index 000000000..44e86d2d6 --- /dev/null +++ b/listing-query/listing-query-view/src/test/java/uk/gov/moj/cpp/listing/query/view/service/SessionJudiciaryEnrichmentServiceTest.java @@ -0,0 +1,586 @@ +package uk.gov.moj.cpp.listing.query.view.service; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import uk.gov.moj.cpp.listing.common.service.HearingSlotsService; +import uk.gov.moj.cpp.listing.persistence.entity.Hearing; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.ws.rs.core.Response; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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; + +@ExtendWith(MockitoExtension.class) +class SessionJudiciaryEnrichmentServiceTest { + + private static final String COURT_SCHEDULE_ID_1 = UUID.randomUUID().toString(); + private static final String COURT_SCHEDULE_ID_2 = UUID.randomUUID().toString(); + private static final String JUDICIARY_ID_1 = UUID.randomUUID().toString(); + private static final String JUDICIARY_ID_2 = UUID.randomUUID().toString(); + + @Mock + private HearingSlotsService hearingSlotsService; + + @InjectMocks + private SessionJudiciaryEnrichmentService service; + + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + void shouldDoNothingForEmptyList() { + service.enrichWithSessionJudiciary(Collections.emptyList()); + verify(hearingSlotsService, never()).getCourtSchedulesById(argThat(m -> true)); + } + + @Test + void shouldDoNothingForNullList() { + service.enrichWithSessionJudiciary(null); + verify(hearingSlotsService, never()).getCourtSchedulesById(argThat(m -> true)); + } + + @Test + void shouldSetJudiciarySourceHearingAndNotCallCourtSchedulerWhenJudiciaryAlreadyPresent() throws Exception { + final Hearing hearing = hearingWithJudiciary(COURT_SCHEDULE_ID_1); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + assertThat(hearing.getProperties().get("judiciarySource").asText(), + is(SessionJudiciaryEnrichmentService.JUDICIARY_SOURCE_HEARING)); + verify(hearingSlotsService, never()).getCourtSchedulesById(argThat(m -> true)); + } + + @Test + void shouldInjectSessionJudiciaryAndMapFieldsCorrectlyWhenHearingJudiciaryIsEmpty() throws Exception { + final Hearing hearing = hearingWithoutJudiciary(COURT_SCHEDULE_ID_1); + + when(hearingSlotsService.getCourtSchedulesById(argThat( + m -> COURT_SCHEDULE_ID_1.equals(m.get("courtScheduleIds"))))) + .thenReturn(buildCourtSchedulerResponse( + COURT_SCHEDULE_ID_1, JUDICIARY_ID_1, "RECORDER", true, false)); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + final JsonNode props = hearing.getProperties(); + assertThat(props.get("judiciarySource").asText(), is(SessionJudiciaryEnrichmentService.JUDICIARY_SOURCE_SESSION)); + + final JsonNode judiciary = props.get("judiciary"); + assertThat(judiciary.isArray(), is(true)); + assertThat(judiciary.size(), is(1)); + + final JsonNode role = judiciary.get(0); + assertThat(role.get("judicialId").asText(), is(JUDICIARY_ID_1)); + assertThat(role.get("judiciaryType").asText(), is("RECORDER")); + assertThat(role.get("isBenchChairman").asBoolean(), is(true)); + assertThat(role.get("isDeputy").asBoolean(), is(false)); + } + + @Test + void shouldInjectSessionJudiciaryWhenHearingJudiciaryFieldIsMissingEntirely() throws Exception { + final Hearing hearing = hearingWithNoJudiciaryField(COURT_SCHEDULE_ID_1); + + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(buildCourtSchedulerResponse( + COURT_SCHEDULE_ID_1, JUDICIARY_ID_1, "JUDGE", false, true)); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + final JsonNode props = hearing.getProperties(); + assertThat(props.get("judiciarySource").asText(), is(SessionJudiciaryEnrichmentService.JUDICIARY_SOURCE_SESSION)); + assertThat(props.get("judiciary").size(), is(1)); + assertThat(props.get("judiciary").get(0).get("judicialId").asText(), is(JUDICIARY_ID_1)); + } + + @Test + void shouldSetSessionSourceButLeaveJudiciaryEmptyWhenNoMatchingScheduleIdInResponse() throws Exception { + final Hearing hearing = hearingWithoutJudiciary(COURT_SCHEDULE_ID_1); + + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(buildCourtSchedulerResponse( + COURT_SCHEDULE_ID_2, JUDICIARY_ID_1, "JUDGE", false, true)); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + final JsonNode props = hearing.getProperties(); + assertThat(props.get("judiciarySource").asText(), is(SessionJudiciaryEnrichmentService.JUDICIARY_SOURCE_SESSION)); + assertThat(props.get("judiciary").size(), is(0)); + } + + @Test + void shouldSetSessionSourceAndSkipCourtSchedulerCallWhenHearingDaysFieldIsAbsent() throws Exception { + final Hearing hearing = hearingFromJson(""" + { "judiciary": [] } + """); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + assertThat(hearing.getProperties().get("judiciarySource").asText(), + is(SessionJudiciaryEnrichmentService.JUDICIARY_SOURCE_SESSION)); + verify(hearingSlotsService, never()).getCourtSchedulesById(argThat(m -> true)); + } + + @Test + void shouldSetSessionSourceAndSkipCourtSchedulerCallWhenNoCourtScheduleIdsInHearingDays() throws Exception { + final Hearing hearing = hearingWithoutJudiciaryAndNoCourtScheduleId(); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + assertThat(hearing.getProperties().get("judiciarySource").asText(), + is(SessionJudiciaryEnrichmentService.JUDICIARY_SOURCE_SESSION)); + verify(hearingSlotsService, never()).getCourtSchedulesById(argThat(m -> true)); + } + + @Test + void shouldSetSessionSourceAndLeaveJudiciaryEmptyWhenCourtSchedulerReturnsNonOkStatus() throws Exception { + final Hearing hearing = hearingWithoutJudiciary(COURT_SCHEDULE_ID_1); + + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + assertThat(hearing.getProperties().get("judiciarySource").asText(), + is(SessionJudiciaryEnrichmentService.JUDICIARY_SOURCE_SESSION)); + assertThat(hearing.getProperties().get("judiciary").size(), is(0)); + } + + @Test + void shouldDeduplicateJudiciaryWhenSameCourtScheduleIdAppearsInMultipleHearingDays() throws Exception { + final Hearing hearing = hearingWithTwoHearingDays(COURT_SCHEDULE_ID_1, COURT_SCHEDULE_ID_1); + + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(buildCourtSchedulerResponse( + COURT_SCHEDULE_ID_1, JUDICIARY_ID_1, "MAGISTRATE", false, false)); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + assertThat(hearing.getProperties().get("judiciary").size(), is(1)); + } + + @Test + void shouldBatchAllCourtScheduleIdsIntoASingleCourtSchedulerCallForMultipleHearings() throws Exception { + final Hearing hearing1 = hearingWithoutJudiciary(COURT_SCHEDULE_ID_1); + final Hearing hearing2 = hearingWithoutJudiciary(COURT_SCHEDULE_ID_2); + + when(hearingSlotsService.getCourtSchedulesById(argThat( + m -> m.get("courtScheduleIds").contains(COURT_SCHEDULE_ID_1) + && m.get("courtScheduleIds").contains(COURT_SCHEDULE_ID_2)))) + .thenReturn(buildCourtSchedulerResponseWithTwoSessions( + COURT_SCHEDULE_ID_1, JUDICIARY_ID_1, "RECORDER", + COURT_SCHEDULE_ID_2, JUDICIARY_ID_2, "JUDGE")); + + service.enrichWithSessionJudiciary(List.of(hearing1, hearing2)); + + verify(hearingSlotsService, times(1)).getCourtSchedulesById(argThat(m -> true)); + + assertThat(hearing1.getProperties().get("judiciary").get(0).get("judicialId").asText(), is(JUDICIARY_ID_1)); + assertThat(hearing2.getProperties().get("judiciary").get(0).get("judicialId").asText(), is(JUDICIARY_ID_2)); + } + + @Test + void shouldHandleMixedListOfHearingsWithAndWithoutJudiciary() throws Exception { + final Hearing hearingWithJudiciary = hearingWithJudiciary(COURT_SCHEDULE_ID_1); + final Hearing hearingWithoutJudiciary = hearingWithoutJudiciary(COURT_SCHEDULE_ID_2); + + when(hearingSlotsService.getCourtSchedulesById(argThat( + m -> COURT_SCHEDULE_ID_2.equals(m.get("courtScheduleIds"))))) + .thenReturn(buildCourtSchedulerResponse( + COURT_SCHEDULE_ID_2, JUDICIARY_ID_2, "DISTRICT_JUDGE", false, true)); + + service.enrichWithSessionJudiciary(List.of(hearingWithJudiciary, hearingWithoutJudiciary)); + + assertThat(hearingWithJudiciary.getProperties().get("judiciarySource").asText(), + is(SessionJudiciaryEnrichmentService.JUDICIARY_SOURCE_HEARING)); + assertThat(hearingWithJudiciary.getProperties().get("judiciary").get(0).get("judicialId").asText(), + is(JUDICIARY_ID_1)); + + assertThat(hearingWithoutJudiciary.getProperties().get("judiciarySource").asText(), + is(SessionJudiciaryEnrichmentService.JUDICIARY_SOURCE_SESSION)); + assertThat(hearingWithoutJudiciary.getProperties().get("judiciary").get(0).get("judicialId").asText(), + is(JUDICIARY_ID_2)); + } + + @Test + void shouldNotCrashAndNotCallCourtSchedulerWhenHearingPropertiesIsNull() { + final Hearing hearingWithNullProps = new Hearing(); + + service.enrichWithSessionJudiciary(List.of(hearingWithNullProps)); + + verify(hearingSlotsService, never()).getCourtSchedulesById(argThat(m -> true)); + } + + @Test + void shouldSkipJudiciaryItemsWhoseJudiciaryIdIsNullInResponse() throws Exception { + final Hearing hearing = hearingWithoutJudiciary(COURT_SCHEDULE_ID_1); + + final JsonObject session = Json.createObjectBuilder() + .add("courtScheduleId", COURT_SCHEDULE_ID_1) + .add("sessionDate", "2026-01-15") + .add("judiciaries", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("judiciaryType", "RECORDER") + .add("isBenchChairman", false) + .add("isDeputy", false)) + .add(Json.createObjectBuilder() + .add("id", JUDICIARY_ID_1) + .add("judiciaryType", "JUDGE") + .add("isBenchChairman", false) + .add("isDeputy", false))) + .build(); + final JsonObject responseBody = Json.createObjectBuilder() + .add("courtSchedules", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("courtRoomId", UUID.randomUUID().toString()) + .add("courtRoomName", "Room 1") + .add("sessions", Json.createArrayBuilder().add(session)))) + .build(); + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(Response.ok(responseBody).build()); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + final JsonNode judiciary = hearing.getProperties().get("judiciary"); + assertThat(judiciary.size(), is(1)); + assertThat(judiciary.get(0).get("judicialId").asText(), is(JUDICIARY_ID_1)); + } + + @Test + void shouldInjectJudiciaryCorrectlyWhenOptionalFieldsAreMissingFromCourtSchedulerResponse() throws Exception { + final Hearing hearing = hearingWithoutJudiciary(COURT_SCHEDULE_ID_1); + + final JsonObject session = Json.createObjectBuilder() + .add("courtScheduleId", COURT_SCHEDULE_ID_1) + .add("sessionDate", "2026-01-15") + .add("judiciaries", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("id", JUDICIARY_ID_1))) + .build(); + final JsonObject responseBody = Json.createObjectBuilder() + .add("courtSchedules", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("courtRoomId", UUID.randomUUID().toString()) + .add("courtRoomName", "Room 1") + .add("sessions", Json.createArrayBuilder().add(session)))) + .build(); + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(Response.ok(responseBody).build()); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + final JsonNode role = hearing.getProperties().get("judiciary").get(0); + assertThat(role.get("judicialId").asText(), is(JUDICIARY_ID_1)); + assertThat(role.has("isBenchChairman"), is(false)); + assertThat(role.has("isDeputy"), is(false)); + assertThat(role.has("judiciaryType"), is(false)); + } + + @Test + void shouldReturnEmptyMapAndNotPropagateWhenCourtSchedulerCallThrows() throws Exception { + final Hearing hearing = hearingWithoutJudiciary(COURT_SCHEDULE_ID_1); + + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenThrow(new IllegalStateException("contextSystemUserId missing!!!")); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + assertThat(hearing.getProperties().get("judiciarySource").asText(), + is(SessionJudiciaryEnrichmentService.JUDICIARY_SOURCE_SESSION)); + assertThat(hearing.getProperties().get("judiciary").size(), is(0)); + } + + @Test + void shouldReturnEmptyMapWhenResponseIsNotOk() { + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(Response.status(Response.Status.NOT_FOUND).build()); + + final Map result = service.fetchJudiciaryByScheduleId(Set.of(COURT_SCHEDULE_ID_1)); + + assertThat(result.isEmpty(), is(true)); + } + + @Test + void shouldReturnEmptyMapWhenCourtSchedulesArrayIsEmpty() { + final JsonObject emptyResponse = Json.createObjectBuilder() + .add("courtSchedules", Json.createArrayBuilder()) + .build(); + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(Response.ok(emptyResponse).build()); + + final Map result = service.fetchJudiciaryByScheduleId(Set.of(COURT_SCHEDULE_ID_1)); + + assertThat(result.isEmpty(), is(true)); + } + + @Test + void shouldReturnEmptyMapWhenSessionsArrayIsNullOrMissing() { + final JsonObject response = Json.createObjectBuilder() + .add("courtSchedules", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("courtRoomId", UUID.randomUUID().toString()) + .add("courtRoomName", "Room 1"))) + .build(); + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(Response.ok(response).build()); + + final Map result = service.fetchJudiciaryByScheduleId(Set.of(COURT_SCHEDULE_ID_1)); + + assertThat(result.isEmpty(), is(true)); + } + + @Test + void shouldIgnoreSessionsWithEmptyJudiciaryArray() { + final JsonObject response = Json.createObjectBuilder() + .add("courtSchedules", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("courtRoomId", UUID.randomUUID().toString()) + .add("courtRoomName", "Room 1") + .add("sessions", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("courtScheduleId", COURT_SCHEDULE_ID_1) + .add("sessionDate", "2026-01-15") + .add("judiciaries", Json.createArrayBuilder()))))) + .build(); + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(Response.ok(response).build()); + + final Map result = service.fetchJudiciaryByScheduleId(Set.of(COURT_SCHEDULE_ID_1)); + + assertThat(result.isEmpty(), is(true)); + } + + @Test + void shouldReturnEmptyMapWhenResponseBodyIsEmptyJsonObject() { + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(Response.ok(Json.createObjectBuilder().build()).build()); + + final Map result = service.fetchJudiciaryByScheduleId(Set.of(COURT_SCHEDULE_ID_1)); + + assertThat(result.isEmpty(), is(true)); + } + + @Test + void shouldIgnoreSessionsWhoseCourtScheduleIdWasNotRequested() { + final JsonObject response = Json.createObjectBuilder() + .add("courtSchedules", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("courtRoomId", UUID.randomUUID().toString()) + .add("courtRoomName", "Room 1") + .add("sessions", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("courtScheduleId", COURT_SCHEDULE_ID_2) + .add("sessionDate", "2026-01-15") + .add("judiciaries", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("id", JUDICIARY_ID_1) + .add("judiciaryType", "JUDGE") + .add("isBenchChairman", false) + .add("isDeputy", false))))))) + .build(); + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(Response.ok(response).build()); + + final Map result = service.fetchJudiciaryByScheduleId(Set.of(COURT_SCHEDULE_ID_1)); + + assertThat(result.isEmpty(), is(true)); + } + + @Test + void shouldInjectAllRefDataFieldsFromSessionJudiciaryResponse() throws Exception { + final Hearing hearing = hearingWithoutJudiciary(COURT_SCHEDULE_ID_1); + + final JsonObject judiciary = Json.createObjectBuilder() + .add("id", JUDICIARY_ID_1) + .add("judiciaryType", "Senior Circuit Judge") + .add("isBenchChairman", false) + .add("isDeputy", false) + .add("seqId", 143117) + .add("titlePrefix", "His Honour Judge") + .add("titleJudicialPrefix", "His Honour Judge") + .add("titleJudicialPrefixWelsh", "Ei Anrhydedd y Barnwr") + .add("personId", "131172") + .add("specialisms", Json.createArrayBuilder().add("ATTEMPTED_MURDER").add("MURDER")) + .add("requestedName", "HIS HONOUR JUDGE MELBOURNE INMAN KC HONORARY RECORDER OF BIRMINGHAM") + .add("surname", "Inman") + .add("forenames", "Melbourne Donald") + .add("emailAddress", "HHJ.Melbourne.Inman@eJudiciary.net") + .build(); + + final JsonObject responseBody = Json.createObjectBuilder() + .add("courtSchedules", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("courtRoomId", UUID.randomUUID().toString()) + .add("courtRoomName", "Court Room 1") + .add("sessions", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("courtScheduleId", COURT_SCHEDULE_ID_1) + .add("sessionDate", "2026-01-15") + .add("judiciaries", Json.createArrayBuilder().add(judiciary)))))) + .build(); + + when(hearingSlotsService.getCourtSchedulesById(argThat(m -> true))) + .thenReturn(Response.ok(responseBody).build()); + + service.enrichWithSessionJudiciary(List.of(hearing)); + + final JsonNode role = hearing.getProperties().get("judiciary").get(0); + assertThat(role.get("judicialId").asText(), is(JUDICIARY_ID_1)); + assertThat(role.get("judiciaryType").asText(), is("Senior Circuit Judge")); + assertThat(role.get("isBenchChairman").asBoolean(), is(false)); + assertThat(role.get("isDeputy").asBoolean(), is(false)); + assertThat(role.get("seqId").asInt(), is(143117)); + assertThat(role.get("titlePrefix").asText(), is("His Honour Judge")); + assertThat(role.get("titleJudicialPrefix").asText(), is("His Honour Judge")); + assertThat(role.get("titleJudicialPrefixWelsh").asText(), is("Ei Anrhydedd y Barnwr")); + assertThat(role.get("personId").asText(), is("131172")); + assertThat(role.get("specialisms").get(0).asText(), is("ATTEMPTED_MURDER")); + assertThat(role.get("specialisms").get(1).asText(), is("MURDER")); + assertThat(role.get("requestedName").asText(), is("HIS HONOUR JUDGE MELBOURNE INMAN KC HONORARY RECORDER OF BIRMINGHAM")); + assertThat(role.get("surname").asText(), is("Inman")); + assertThat(role.get("forenames").asText(), is("Melbourne Donald")); + assertThat(role.get("emailAddress").asText(), is("HHJ.Melbourne.Inman@eJudiciary.net")); + } + + private Hearing hearingWithJudiciary(final String courtScheduleId) throws Exception { + final String json = """ + { + "judiciary": [{"judicialId": "%s", "judicialRoleType": {"judiciaryType": "JUDGE"}}], + "hearingDays": [{"courtScheduleId": "%s"}] + } + """.formatted(JUDICIARY_ID_1, courtScheduleId); + return hearingFromJson(json); + } + + private Hearing hearingWithoutJudiciary(final String courtScheduleId) throws Exception { + final String json = """ + { + "judiciary": [], + "hearingDays": [{"courtScheduleId": "%s"}] + } + """.formatted(courtScheduleId); + return hearingFromJson(json); + } + + private Hearing hearingWithNoJudiciaryField(final String courtScheduleId) throws Exception { + final String json = """ + { + "hearingDays": [{"courtScheduleId": "%s"}] + } + """.formatted(courtScheduleId); + return hearingFromJson(json); + } + + private Hearing hearingWithTwoHearingDays(final String csId1, final String csId2) throws Exception { + final String json = """ + { + "judiciary": [], + "hearingDays": [ + {"courtScheduleId": "%s"}, + {"courtScheduleId": "%s"} + ] + } + """.formatted(csId1, csId2); + return hearingFromJson(json); + } + + private Hearing hearingWithoutJudiciaryAndNoCourtScheduleId() throws Exception { + final String json = """ + { + "judiciary": [], + "hearingDays": [{"hearingDate": "2026-01-15"}] + } + """; + return hearingFromJson(json); + } + + private Hearing hearingFromJson(final String json) throws Exception { + final JsonNode props = mapper.readTree(json); + final Hearing hearing = new Hearing(); + hearing.setProperties(props); + return hearing; + } + + private Response buildCourtSchedulerResponse(final String courtScheduleId, + final String judiciaryId, + final String judiciaryType, + final boolean isBenchChairman, + final boolean isDeputy) { + final JsonObject session = Json.createObjectBuilder() + .add("courtScheduleId", courtScheduleId) + .add("sessionDate", "2026-01-15") + .add("judiciaries", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("id", judiciaryId) + .add("judiciaryType", judiciaryType) + .add("isBenchChairman", isBenchChairman) + .add("isDeputy", isDeputy))) + .build(); + + final JsonObject responseBody = Json.createObjectBuilder() + .add("courtSchedules", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("courtRoomId", UUID.randomUUID().toString()) + .add("courtRoomName", "Court Room 1") + .add("sessions", Json.createArrayBuilder().add(session)))) + .build(); + + return Response.ok(responseBody).build(); + } + + private Response buildCourtSchedulerResponseWithTwoSessions( + final String csId1, final String jId1, final String jType1, + final String csId2, final String jId2, final String jType2) { + + final JsonObject session1 = Json.createObjectBuilder() + .add("courtScheduleId", csId1) + .add("sessionDate", "2026-01-15") + .add("judiciaries", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("id", jId1) + .add("judiciaryType", jType1) + .add("isBenchChairman", false) + .add("isDeputy", false))) + .build(); + + final JsonObject session2 = Json.createObjectBuilder() + .add("courtScheduleId", csId2) + .add("sessionDate", "2026-01-16") + .add("judiciaries", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("id", jId2) + .add("judiciaryType", jType2) + .add("isBenchChairman", false) + .add("isDeputy", false))) + .build(); + + final JsonObject responseBody = Json.createObjectBuilder() + .add("courtSchedules", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("courtRoomId", UUID.randomUUID().toString()) + .add("courtRoomName", "Court Room 1") + .add("sessions", Json.createArrayBuilder() + .add(session1) + .add(session2)))) + .build(); + + return Response.ok(responseBody).build(); + } +} diff --git a/listing-query/pom.xml b/listing-query/pom.xml index a46e2804c..f28c5acac 100644 --- a/listing-query/pom.xml +++ b/listing-query/pom.xml @@ -3,7 +3,7 @@ uk.gov.moj.cpp.listing listing-parent - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-service/pom.xml b/listing-service/pom.xml index dbc39d93f..d2754af15 100644 --- a/listing-service/pom.xml +++ b/listing-service/pom.xml @@ -3,7 +3,7 @@ uk.gov.moj.cpp.listing listing-parent - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-viewstore/listing-viewstore-liquibase/pom.xml b/listing-viewstore/listing-viewstore-liquibase/pom.xml index ff0e365ac..f3e2f1a5a 100644 --- a/listing-viewstore/listing-viewstore-liquibase/pom.xml +++ b/listing-viewstore/listing-viewstore-liquibase/pom.xml @@ -3,7 +3,7 @@ listing-viewstore uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-viewstore/listing-viewstore-persistence/pom.xml b/listing-viewstore/listing-viewstore-persistence/pom.xml index 76f09987a..724b0833d 100644 --- a/listing-viewstore/listing-viewstore-persistence/pom.xml +++ b/listing-viewstore/listing-viewstore-persistence/pom.xml @@ -3,7 +3,7 @@ uk.gov.moj.cpp.listing listing-viewstore - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/listing-viewstore/pom.xml b/listing-viewstore/pom.xml index 650dd7c5d..da895e465 100644 --- a/listing-viewstore/pom.xml +++ b/listing-viewstore/pom.xml @@ -3,7 +3,7 @@ listing-parent uk.gov.moj.cpp.listing - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 8ff200e60..8a49f1e6a 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ uk.gov.moj.cpp.listing listing-parent - 17.103.176-SNAPSHOT + 17.103.163-JA-SNAPSHOT pom Listing Context Microservice Parent @@ -29,7 +29,7 @@ sonar-report.json git file://${project.build.directory}/site - 17.103.13 + 17.103.11 17.103.133 1.0.4 5.7 @@ -42,10 +42,11 @@ 2.2.11 1.7.10 42.1.4 + 7.0.0.jre8 1.4 - 17.0.94 - 17.104.49 + 17.0.87 + 17.104.50 **/uk/gov/justice/api/mapper/**/*, diff --git a/run-it-midnight.sh b/run-it-midnight.sh deleted file mode 100755 index 0fde32c44..000000000 --- a/run-it-midnight.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -# run-it-midnight.sh — prove the listing IT suite is midnight-safe WITHOUT waiting for real midnight. -# -# Freezes ItClock (listing-integration-test/.../it/util/ItClock.java) at a set of hazardous instants -# via -Dit.clock, which the listing-integration-test failsafe profile forwards into the forked IT JVM -# as the it.clock system property. Each anchor exercises a specific date hazard; the headline one is the -# 00:00-01:00 BST band where a Europe/London "today" and a UTC "today" land on different calendar days. -# -# Pre-Phase-2 (no ItClock) this script REPRODUCES the bug (expect FAIL at the BST band anchors) — that is -# itself a valid baseline. With ItClock + the -Duser.timezone=UTC failsafe pin in place, every anchor must -# pass. Per repo CLAUDE.md, the ONLY sanctioned way to run the ITs is ./runIntegrationTests.sh. -# -# Note: if ./runIntegrationTests.sh ignores -Dit.test on this stack, each anchor runs the FULL suite under -# the frozen clock instead of the date-sensitive subset — slower, but still a valid midnight-safety proof. -set -u - -REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" -cd "$REPO_ROOT" - -# Date-sensitive subset (verified to exist on team/ccsph2n). Comma-separated for -Dit.test. -SUBSET='PublishCourtListIT,WeekCommencingHearingIT,ListCourtWeekCommencingHearingIT,RangeSearchQueryForCourtCalendarIT,HearingCsvReportIT,ExhibitScenarioIT,HearingIT,EjectCaseCourtSlotsManagementIT,HearingDaysIT,RangeSearchQueryForMagistratesIT,ListingNoteIT' - -# Sunday 2026-06-14 -> Monday 2026-06-15 so the midnight crossing also crosses a weekend -# (multi-day skip-weekend / next-working-day logic). 2026-06-12 is a Friday. -ANCHORS=( - "2026-06-14T23:30:00+01:00[Europe/London]" # just BEFORE midnight, BST, Sun - "2026-06-15T00:00:30+01:00[Europe/London]" # just AFTER midnight, BST, Mon - "2026-06-15T00:30:00+01:00[Europe/London]" # MID-BAND, BST <-- headline case - "2026-01-15T00:30:00Z" # GMT/winter control (offset 0) - "2026-06-12T00:30:00+01:00[Europe/London]" # Fri 00:30 -> next-working-day = Mon -) - -green=0 -for a in "${ANCHORS[@]}"; do - echo "==================== anchor: $a ====================" - docker rm -f $(docker ps -aq --filter "name=cpp-" --filter "name=wildfly-to-hap") 2>/dev/null - mvn -q clean - IT_CLOCK="$a" ./runIntegrationTests.sh -Dit.clock="$a" -Dit.test="$SUBSET" - if [[ $? -eq 0 ]]; then - green=$((green + 1)); echo "PASS $a" - else - echo "FAIL $a" - fi -done - -echo "====================================================" -echo "MIDNIGHT RESULT: $green/${#ANCHORS[@]} anchors green" -[[ "$green" -eq "${#ANCHORS[@]}" ]] && exit 0 || exit 1