Skip to content

Commit ae9d386

Browse files
authored
Merge branch 'main' into feat/tech-hardening-29-2-stream-report-export
2 parents 388a834 + 5b6e5f4 commit ae9d386

24 files changed

Lines changed: 5342 additions & 5 deletions

backend/src/main/java/ca/bc/gov/nrs/ilcr/schedule10/Schedule10DocumentAssembler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ private Schedule10CodeLists codeLists(
382382
long millId, int year, Map<Integer, BecClassification> offerableBec) {
383383
return new Schedule10CodeLists(
384384
toCodes(repository.findForestRegions(millId, year)),
385+
toCodes(repository.findTsaNumbers(millId, year)),
386+
toCodes(repository.findSupplyBlocks(millId, year)),
385387
toCodes(repository.findRoadLifetimes(millId, year)),
386388
toCodes(repository.findBallastMethods(millId, year)),
387389
toCodes(repository.findBallastMaterials(millId, year)),

backend/src/main/java/ca/bc/gov/nrs/ilcr/schedule10/Schedule10Repository.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,63 @@ OR REL_SOIL_MOIST_RGM_CLS_CODE IN (
317317
""")
318318
List<CodeRow> findRsmrClasses(@Param("millId") long millId, @Param("year") int year);
319319

320+
/**
321+
* TSA numbers effective for the reporting year, PLUS any TSA a stored page already references.
322+
*
323+
* <p>Legacy sourced this control from {@code LookUpCaches.getTsaNumberCodeCache()}
324+
* ({@code RoadConstructionReportType.java:378}). Serving it here keeps the entry constrained to
325+
* real TSA numbers: the write path validates only the WIDTH of this leg, so without a bounded list
326+
* an arbitrary value would persist and then serve a blank Road Group.
327+
*
328+
* @param millId the mill, used to find referenced codes
329+
* @param year the reporting year
330+
* @return code/description pairs, ordered by code
331+
*/
332+
@Query("""
333+
SELECT TSA_NUMBER AS code, DESCRIPTION AS description
334+
FROM THE.TSA_NUMBER_CODE
335+
WHERE ((EFFECTIVE_DATE IS NULL
336+
OR EFFECTIVE_DATE <= TO_DATE(:year || '-01-01', 'YYYY-MM-DD'))
337+
AND (EXPIRY_DATE IS NULL
338+
OR EXPIRY_DATE >= TO_DATE(:year || '-01-01', 'YYYY-MM-DD')))
339+
OR TSA_NUMBER IN (
340+
SELECT r.TSA_NUMBER
341+
FROM THE.ROAD_CONSTRUCTION_REPRT r
342+
WHERE r.ILCR_MILL_ID = :millId
343+
AND r.REPORT_YEAR = :year
344+
AND r.ILCR_CATEGORY_ID = '10')
345+
ORDER BY TSA_NUMBER
346+
""")
347+
List<CodeRow> findTsaNumbers(@Param("millId") long millId, @Param("year") int year);
348+
349+
/**
350+
* Supply block codes effective for the reporting year, PLUS any block a stored page references.
351+
*
352+
* <p>Legacy narrowed this list to blocks whose code starts with the chosen TSA
353+
* ({@code RoadConstructionReportType.java:428-434}); the full list is served and that narrowing is
354+
* left to the control, which is where the chosen TSA lives.
355+
*
356+
* @param millId the mill, used to find referenced codes
357+
* @param year the reporting year
358+
* @return code/description pairs, ordered by code
359+
*/
360+
@Query("""
361+
SELECT TSB_NUMBER_CODE AS code, DESCRIPTION AS description
362+
FROM THE.TSB_NUMBER_CODE
363+
WHERE ((EFFECTIVE_DATE IS NULL
364+
OR EFFECTIVE_DATE <= TO_DATE(:year || '-01-01', 'YYYY-MM-DD'))
365+
AND (EXPIRY_DATE IS NULL
366+
OR EXPIRY_DATE >= TO_DATE(:year || '-01-01', 'YYYY-MM-DD')))
367+
OR TSB_NUMBER_CODE IN (
368+
SELECT r.TSB_NUMBER_CODE
369+
FROM THE.ROAD_CONSTRUCTION_REPRT r
370+
WHERE r.ILCR_MILL_ID = :millId
371+
AND r.REPORT_YEAR = :year
372+
AND r.ILCR_CATEGORY_ID = '10')
373+
ORDER BY TSB_NUMBER_CODE
374+
""")
375+
List<CodeRow> findSupplyBlocks(@Param("millId") long millId, @Param("year") int year);
376+
320377
/**
321378
* The BEC classifications a Schedule 10 road detail may reference.
322379
*

backend/src/main/java/ca/bc/gov/nrs/ilcr/schedule10/dto/Schedule10CodeLists.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
* fields, which also eliminates BR-06's runtime filtering of those two lists.
2121
*
2222
* @param forestRegions the forest region codes
23+
* @param tsaNumbers the TSA numbers offered by the location control
24+
* @param supplyBlocks the supply block codes; the control narrows these to the chosen TSA
2325
* @param roadLifetimes the Road Type codes
2426
* @param ballastMethods the ballast method codes
2527
* @param ballastMaterials the ballast material codes
@@ -29,6 +31,8 @@
2931
@JsonInclude(JsonInclude.Include.NON_NULL)
3032
public record Schedule10CodeLists(
3133
List<CodeDescriptionDto> forestRegions,
34+
List<CodeDescriptionDto> tsaNumbers,
35+
List<CodeDescriptionDto> supplyBlocks,
3236
List<CodeDescriptionDto> roadLifetimes,
3337
List<CodeDescriptionDto> ballastMethods,
3438
List<CodeDescriptionDto> ballastMaterials,

backend/src/test/java/ca/bc/gov/nrs/ilcr/schedule10/Schedule10DocumentIT.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ca.bc.gov.nrs.ilcr.schedule10;
22

3+
import static org.hamcrest.Matchers.contains;
34
import static org.hamcrest.Matchers.hasItem;
45
import static org.hamcrest.Matchers.hasSize;
56
import static org.hamcrest.Matchers.is;
@@ -329,6 +330,76 @@ void codeLists_areYearFilteredAndOmitRemovedFields() throws Exception {
329330
.andExpect(jsonPath("$.codeLists.soilMoistureCodes").doesNotExist());
330331
}
331332

333+
@Test
334+
@DisplayName("the TSA and supply-block lists are served, year-filtered, and not transposed")
335+
void tsaAndSupplyBlockLists_areServedAndYearFiltered() throws Exception {
336+
// Both lists shipped with NO coverage at all: deleting either @Query body, or swapping the two
337+
// toCodes(...) arguments in the assembler -- they are both List<CodeDescriptionDto>, so it
338+
// compiles -- left the suite green. Each assertion below fails under one of those mutations.
339+
mockMvc.perform(get(ENDPOINT).param("millId", "710").param("year", "2021")
340+
.accept(MediaType.APPLICATION_JSON))
341+
.andExpect(status().isOk())
342+
// Present and populated, so deleting a @Query body reds this.
343+
.andExpect(jsonPath("$.codeLists.tsaNumbers").isArray())
344+
.andExpect(jsonPath("$.codeLists.supplyBlocks").isArray())
345+
.andExpect(jsonPath("$.codeLists.tsaNumbers[?(@.code == '01')]", hasSize(1)))
346+
.andExpect(jsonPath("$.codeLists.tsaNumbers[?(@.code == '16')]", hasSize(1)))
347+
.andExpect(jsonPath("$.codeLists.supplyBlocks[?(@.code == '01A')]", hasSize(1)))
348+
.andExpect(jsonPath("$.codeLists.supplyBlocks[?(@.code == '16G')]", hasSize(1)))
349+
// The descriptions are what the control DISPLAYS, so they are part of the contract.
350+
.andExpect(jsonPath("$.codeLists.tsaNumbers[?(@.code == '01')].description",
351+
contains("Arrow TSA")))
352+
.andExpect(jsonPath("$.codeLists.supplyBlocks[?(@.code == '01A')].description",
353+
contains("Arrow TSA Block A")))
354+
// TRANSPOSITION GUARD: a TSA code must never appear in the block list, or the reverse. A
355+
// swap of the two assembler arguments compiles and is caught only here.
356+
.andExpect(jsonPath("$.codeLists.tsaNumbers[?(@.code == '01A')]", hasSize(0)))
357+
.andExpect(jsonPath("$.codeLists.tsaNumbers[?(@.code == '16G')]", hasSize(0)))
358+
.andExpect(jsonPath("$.codeLists.supplyBlocks[?(@.code == '01')]", hasSize(0)))
359+
.andExpect(jsonPath("$.codeLists.supplyBlocks[?(@.code == '16')]", hasSize(0)))
360+
// V20260821 seeds '90','Retired TSA' expiring 2010-12-31 stating it exists "to pin that the
361+
// year filter drops a code". Nothing pinned it until now: no stored 2021 page references
362+
// '90', so neither leg of the predicate can rescue it.
363+
.andExpect(jsonPath("$.codeLists.tsaNumbers[?(@.code == '90')]", hasSize(0)));
364+
}
365+
366+
@Test
367+
@DisplayName("an expired block a stored page references is rescued by the referenced-union leg")
368+
void supplyBlocks_referencedUnionRescuesAnExpiredCode() throws Exception {
369+
// The union leg on both queries had NO coverage: it can only rescue a code that HAS a row and
370+
// fell outside the date window, and no fixture created that shape until V20260821 was corrected
371+
// to seed '16Z' expired (see that file's CORRECTED note). Page 8904 references it, on mill 712.
372+
mockMvc.perform(get(ENDPOINT).param("millId", "712").param("year", "2021")
373+
.accept(MediaType.APPLICATION_JSON))
374+
.andExpect(status().isOk())
375+
// Drop the `OR TSB_NUMBER_CODE IN (...)` leg and this goes to 0.
376+
.andExpect(jsonPath("$.codeLists.supplyBlocks[?(@.code == '16Z')]", hasSize(1)));
377+
378+
// Scoped to the MILL and the YEAR: mill 710 references no such block, so the expired code stays
379+
// dropped there. Remove the date predicate and this goes to 1.
380+
mockMvc.perform(get(ENDPOINT).param("millId", "710").param("year", "2021")
381+
.accept(MediaType.APPLICATION_JSON))
382+
.andExpect(status().isOk())
383+
.andExpect(jsonPath("$.codeLists.supplyBlocks[?(@.code == '16Z')]", hasSize(0)));
384+
}
385+
386+
@Test
387+
@DisplayName("a code absent from its table entirely is not served, however many pages reference it")
388+
void codeLists_cannotServeACodeWithNoRow() throws Exception {
389+
// Page 8903 (mill 712) stores TSA '99' / TSB '99A', neither of which has a row in its code table.
390+
// The union leg selects FROM the code table, so it cannot invent one. This is the contract
391+
// boundary that makes the FRONTEND synthesise a stored code as its own option (review H2) — pinned
392+
// here so nobody "fixes" the client by pointing at a backend guarantee that does not exist.
393+
mockMvc.perform(get(ENDPOINT).param("millId", "712").param("year", "2021")
394+
.accept(MediaType.APPLICATION_JSON))
395+
.andExpect(status().isOk())
396+
.andExpect(jsonPath("$.codeLists.tsaNumbers[?(@.code == '99')]", hasSize(0)))
397+
.andExpect(jsonPath("$.codeLists.supplyBlocks[?(@.code == '99A')]", hasSize(0)))
398+
// The page itself still lists, carrying its stored location verbatim.
399+
.andExpect(jsonPath("$.pages[?(@.pageId == 8903)].tsaNumber", contains("99")))
400+
.andExpect(jsonPath("$.pages[?(@.pageId == 8903)].tsbNumberCode", contains("99A")));
401+
}
402+
332403
@Test
333404
@DisplayName("BEC is served structurally, gated by the surviving BR-06 xref (deviation (e))")
334405
void becIsStructuralAndXrefGated() throws Exception {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
-- TSA / TSB code tables: bring the test snapshot up to the delivery shape and seed the Schedule 10
2+
-- location options.
3+
--
4+
-- THE.TSA_NUMBER_CODE and THE.TSB_NUMBER_CODE were created by the Schedule 8 snapshot (V22:92-93) as
5+
-- code + DESCRIPTION only. Delivery carries the standard code-table quartet -- DESCRIPTION plus
6+
-- EFFECTIVE_DATE / EXPIRY_DATE / UPDATE_TIMESTAMP -- because both entities extend AbstractILCRCode
7+
-- and legacy year-filters every code cache on EFFECTIVE_DATE <= 1-Jan-{year} <= EXPIRY_DATE
8+
-- (LookupCache.java:77-98). Serving these two as Schedule 10 dropdown lists is the first consumer
9+
-- that reads the dates, so the understatement surfaces here.
10+
--
11+
-- Columns are added with DEFAULTs so the rows V22 already seeded are backfilled in place; NOT NULL
12+
-- then matches delivery, and an insert that skips a date fails here exactly as it would there.
13+
14+
ALTER TABLE THE.TSA_NUMBER_CODE ADD (
15+
EFFECTIVE_DATE DATE DEFAULT DATE '1900-01-01' NOT NULL,
16+
EXPIRY_DATE DATE DEFAULT DATE '9999-12-31' NOT NULL,
17+
UPDATE_TIMESTAMP DATE DEFAULT SYSDATE NOT NULL
18+
);
19+
20+
ALTER TABLE THE.TSB_NUMBER_CODE ADD (
21+
EFFECTIVE_DATE DATE DEFAULT DATE '1900-01-01' NOT NULL,
22+
EXPIRY_DATE DATE DEFAULT DATE '9999-12-31' NOT NULL,
23+
UPDATE_TIMESTAMP DATE DEFAULT SYSDATE NOT NULL
24+
);
25+
26+
-- The TSA numbers and supply blocks the Schedule 10 read fixtures store on their pages, so each
27+
-- stored location resolves to an option in its own dropdown.
28+
--
29+
-- '99'/'99A' are deliberately NOT seeded: they are the unmapped-TSA fixtures, and a code with no row
30+
-- at all is a real delivery state the dropdown has to survive.
31+
--
32+
-- CORRECTED 2026-08-20 (code review H5): this block previously left '16Z' unseeded too, on the stated
33+
-- grounds that doing so kept it "representative of the legacy rows that motivated the referenced-union
34+
-- leg". That is backwards. The union leg reads
35+
-- SELECT ... FROM THE.TSB_NUMBER_CODE WHERE <date window> OR TSB_NUMBER_CODE IN (<referenced>)
36+
-- so it can only rescue a code that HAS a row and fell outside the date window. A code absent from the
37+
-- table can never be selected by it, however many pages reference it — so the unseeded fixtures
38+
-- exercised nothing, and the leg had no coverage on either list. '16Z' is now seeded EXPIRED and is
39+
-- referenced by page 8904 (mill 712), which is the one shape that does exercise it.
40+
--
41+
-- A code that is absent ENTIRELY still cannot be served, which is why the FRONTEND synthesises the
42+
-- stored code as its own option (review H2) rather than relying on the backend for it.
43+
INSERT INTO THE.TSA_NUMBER_CODE (TSA_NUMBER, DESCRIPTION, EFFECTIVE_DATE, EXPIRY_DATE, UPDATE_TIMESTAMP)
44+
VALUES ('01', 'Arrow TSA', DATE '1900-01-01', DATE '9999-12-31', SYSDATE);
45+
INSERT INTO THE.TSA_NUMBER_CODE (TSA_NUMBER, DESCRIPTION, EFFECTIVE_DATE, EXPIRY_DATE, UPDATE_TIMESTAMP)
46+
VALUES ('16', 'Lakes TSA', DATE '1900-01-01', DATE '9999-12-31', SYSDATE);
47+
48+
-- An expired TSA, to pin that the year filter drops a code no stored page references.
49+
INSERT INTO THE.TSA_NUMBER_CODE (TSA_NUMBER, DESCRIPTION, EFFECTIVE_DATE, EXPIRY_DATE, UPDATE_TIMESTAMP)
50+
VALUES ('90', 'Retired TSA', DATE '1900-01-01', DATE '2010-12-31', SYSDATE);
51+
52+
INSERT INTO THE.TSB_NUMBER_CODE (TSB_NUMBER_CODE, DESCRIPTION, EFFECTIVE_DATE, EXPIRY_DATE, UPDATE_TIMESTAMP)
53+
VALUES ('01A', 'Arrow TSA Block A', DATE '1900-01-01', DATE '9999-12-31', SYSDATE);
54+
INSERT INTO THE.TSB_NUMBER_CODE (TSB_NUMBER_CODE, DESCRIPTION, EFFECTIVE_DATE, EXPIRY_DATE, UPDATE_TIMESTAMP)
55+
VALUES ('16G', 'Lakes TSA Block G', DATE '1900-01-01', DATE '9999-12-31', SYSDATE);
56+
57+
-- An EXPIRED block that page 8904 (mill 712) still references: the only fixture that makes the
58+
-- referenced-union leg falsifiable. Out of the date window, so the date predicate alone drops it; in
59+
-- the referenced set for mill 712, so the union leg brings it back for that mill and no other.
60+
INSERT INTO THE.TSB_NUMBER_CODE (TSB_NUMBER_CODE, DESCRIPTION, EFFECTIVE_DATE, EXPIRY_DATE, UPDATE_TIMESTAMP)
61+
VALUES ('16Z', 'Lakes TSA Block Z (retired)', DATE '1900-01-01', DATE '2010-12-31', SYSDATE);

frontend/src/components/core/CodeComboBox.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ export interface ComboOption {
77
readonly description: string
88
}
99

10+
/**
11+
* How typed text is matched against an option's description.
12+
*
13+
* `substring` is the historical behaviour every caller had before this prop existed and stays the
14+
* default, so Schedules 2, 4 and 8 filter exactly as they always have. `prefix` is opt-in for the
15+
* controls whose AC specifies it — Schedule 10's BEC Zone (AC5), where typing `SBS` must offer the
16+
* `SBS*` zones and not every zone containing those letters.
17+
*/
18+
export type ComboMatchMode = 'substring' | 'prefix'
19+
1020
interface CodeComboBoxProps {
1121
id: string
1222
titleText: string
@@ -16,6 +26,8 @@ interface CodeComboBoxProps {
1626
selectedCode: string
1727
/** Called with the chosen option's code ('' when cleared). */
1828
onSelect: (code: string) => void
29+
/** Defaults to `substring`, the behaviour every pre-existing caller relies on. */
30+
matchMode?: ComboMatchMode
1931
disabled?: boolean
2032
invalid?: boolean
2133
invalidText?: string
@@ -34,12 +46,18 @@ const CodeComboBox: FC<CodeComboBoxProps> = ({
3446
items,
3547
selectedCode,
3648
onSelect,
49+
matchMode = 'substring',
3750
disabled,
3851
invalid,
3952
invalidText,
4053
className,
4154
}) => {
4255
const selectedItem = items.find((option) => option.code === selectedCode) ?? null
56+
const matches = (description: string, typed: string): boolean => {
57+
const haystack = description.toLowerCase()
58+
const needle = typed.toLowerCase()
59+
return matchMode === 'prefix' ? haystack.startsWith(needle) : haystack.includes(needle)
60+
}
4361
return (
4462
<ComboBox<ComboOption>
4563
id={id}
@@ -49,14 +67,15 @@ const CodeComboBox: FC<CodeComboBoxProps> = ({
4967
items={items}
5068
itemToString={(item) => item?.description ?? ''}
5169
selectedItem={selectedItem}
52-
// Autocomplete: typing filters the list by a case-insensitive match on the description. Show the
53-
// whole list when nothing is typed OR when the input still equals the current selection (menu
54-
// just opened) — otherwise a selected value would filter the list down to itself and hide the
55-
// other options. Explicit so filtering doesn't depend on the Carbon default.
70+
// Autocomplete: typing filters the list by a case-insensitive match on the description, of the
71+
// kind `matchMode` names. Show the whole list when nothing is typed OR when the input still
72+
// equals the current selection (menu just opened) — otherwise a selected value would filter the
73+
// list down to itself and hide the other options. Explicit so filtering doesn't depend on the
74+
// Carbon default.
5675
shouldFilterItem={({ item, inputValue }) =>
5776
!inputValue ||
5877
inputValue === selectedItem?.description ||
59-
(item?.description ?? '').toLowerCase().includes(inputValue.toLowerCase())
78+
matches(item?.description ?? '', inputValue)
6079
}
6180
disabled={disabled}
6281
invalid={invalid}

0 commit comments

Comments
 (0)