Skip to content

Commit ff6f0dc

Browse files
Rylan-cgiclaude
andcommitted
test(schedule10): Story 11.2 Task 9 — verify, fix a shared-sequence collision, clear checkstyle
Verification gate for Story 11.2 (Schedule 10 write path + Check Status): 1148 unit + 865 integration tests, 0 failures; 0 checkstyle violations in schedule10 (1234 pre-existing elsewhere left untouched). Repair a latent CROSS-SCHEDULE collision, not a Schedule 10 bug. V33 seeds ILCR_COST_REPORT_DETAIL ids 9500-9506, above the SHARED ILCR_COST_REPORT_DETAIL_SEQ start of 9000. Nothing had consumed 500 values in one suite before, so it stayed latent; Schedule 10 writes twelve cost rows per road detail (Schedule 6 writes 1, 7A/7B about 2) and was the first to push past 9500, failing seven Schedule 6/7A ITs with DuplicateKeyException. V20260818 now restarts the shared sequence at 10000, following the V21 precedent, after confirming no test pins a generated cost-detail id in 9000-9599. Add Schedule10RequestValidationTest (24 tests) pinning the constraint-to-bundle-key pairing for both write requests: the schedule10TsaOrTflRequiredErrorMsg vs Schedule 6 decoy key, the sub-grade-100 / stabilizing-999.999 length asymmetry from both sides, the two distinct cost bands, and the deliberate absence of @digits. Written green as characterization, not red-phase. Re-wrap javadoc and comments across 19 schedule10 sources to clear 119 checkstyle violations. Comment-only: all 33 package sources were compared against HEAD with comments stripped and whitespace removed and are token-identical, so the integration run stands as evidence for the production code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 79c8d50 commit ff6f0dc

21 files changed

Lines changed: 754 additions & 250 deletions

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
/**
44
* Raised when a road-detail write names a BEC classification the cross-reference does not offer, or
5-
* when that classification plus the RSMR class resolves to no moisture-code pair at all. Maps to 400
6-
* with legacy's own {@code invalidBiogeoCode} text.
5+
* when that classification plus the RSMR class resolves to no moisture-code pair at all. Maps to
6+
* 400 with legacy's own {@code invalidBiogeoCode} text.
77
*
8-
* <p>Two distinct causes, one message, because legacy uses this same string for both: its Check Status
9-
* emits it when a stored BEC id is outside the allowable filtered list, and its autocomplete refuses a
10-
* typed value that resolves to nothing.
8+
* <p>Two distinct causes, one message, because legacy uses this same string for both: its Check
9+
* Status emits it when a stored BEC id is outside the allowable filtered list, and its autocomplete
10+
* refuses a typed value that resolves to nothing.
1111
*
1212
* <p>The zero-candidate case matters for the write path specifically: the two moisture codes are
1313
* derived from this classification and the RSMR class, and both target columns are {@code NOT NULL}

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55

66
/**
77
* Raised when a TFL-located page carries a TFL number the reference table does not hold, after the
8-
* missing-leading-zero aliases have been applied. Maps to 400 with legacy's verbatim validator text.
8+
* missing-leading-zero aliases have been applied. Maps to 400 with legacy's verbatim validator
9+
* text.
910
*
10-
* <p>Because the accept set and the Road-Group-derivable set are the same 22 keys, a TFL that passes
11-
* this check always derives a Road Group — so the "unmapped TFL saves with a blank Road Group" state
12-
* is unreachable through a write, in this application and in legacy alike. It exists only in stored
13-
* data that predates or bypassed the screen.
11+
* <p>Because the accept set and the Road-Group-derivable set are the same 22 keys, a TFL that
12+
* passes this check always derives a Road Group — so the "unmapped TFL saves with a blank Road
13+
* Group" state is unreachable through a write, in this application and in legacy alike. It exists
14+
* only in stored data that predates or bypassed the screen.
1415
*/
1516
public class InvalidTflNumberException extends BusinessException {
1617

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
* Raised when an UPDATE reaches the service without its optimistic-lock token. Maps to 400.
88
*
99
* <p>Bean Validation already enforces this through the {@code OnUpdate} group, so this is the
10-
* belt-and-braces guard for any path that reaches the service without that group applied. Without it
11-
* the token would be unboxed from null and NPE into a 500.
10+
* belt-and-braces guard for any path that reaches the service without that group applied. Without
11+
* it the token would be unboxed from null and NPE into a 500.
1212
*
13-
* <p>Deliberately a 400 rather than a coerced 409: substituting a sentinel like {@code -1} matches no
14-
* row, so the optimistic-lock UPDATE misses and the user is told another user changed the row when the
15-
* real fix is to send the token. Several older schedules coerce; this one does not.
13+
* <p>Deliberately a 400 rather than a coerced 409: substituting a sentinel like {@code -1} matches
14+
* no row, so the optimistic-lock UPDATE misses and the user is told another user changed the row
15+
* when the real fix is to send the token. Several older schedules coerce; this one does not.
1616
*/
1717
public class RevisionCountRequiredException extends BusinessException {
1818

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,19 @@ static String rmgFor(String tsaNumber, String tsbNumberCode, String tflNumberCod
8787
/**
8888
* The storable form of an entered TFL number, or {@code null} when it is not a valid TFL.
8989
*
90-
* <p>Legacy's validator ({@code ILCRTflNumberValidator:33-45}) accepts a TFL if the lookup resolves
91-
* it directly, or if it resolves after applying the missing-leading-zero aliases
92-
* ({@code RoadGroupUtil.translateNoLeadingZeroButNumberMatch} :202-215). It validates the alias but
93-
* then stores the raw entry, which leaves an accepted value in a form the reference table does not
90+
* <p>Legacy's validator ({@code ILCRTflNumberValidator:33-45}) accepts a TFL if the lookup
91+
* resolves it directly, or if it resolves after applying the missing-leading-zero aliases ({@code
92+
* RoadGroupUtil.translateNoLeadingZeroButNumberMatch} :202-215). It validates the alias but then
93+
* stores the raw entry, which leaves an accepted value in a form the reference table does not
9494
* hold; this returns the canonical form so the stored value is the one that resolves.
9595
*
9696
* <p><strong>On which table validates.</strong> The legacy validator calls Schedule <em>6</em>'s
97-
* lookup even for this screen. That reads like a defect, but the two tables accept an identical set
98-
* of 22 keys — verified by diffing the {@code case} labels of both methods — so validating here
99-
* against Schedule 10's own table accepts and rejects exactly what legacy does. Only the returned
100-
* Road Group values differ between the tables, which is what this class exists to keep separate.
101-
* The cross-wiring is still worth reporting upstream: the tables are maintained independently, so a
102-
* future edit to either would silently split validation from derivation.
97+
* lookup even for this screen. That reads like a defect, but the two tables accept an identical
98+
* set of 22 keys — verified by diffing the {@code case} labels of both methods — so validating
99+
* here against Schedule 10's own table accepts and rejects exactly what legacy does. Only the
100+
* returned Road Group values differ between the tables, which is what this class exists to keep
101+
* separate. The cross-wiring is still worth reporting upstream: the tables are maintained
102+
* independently, so a future edit to either would silently split validation from derivation.
103103
*
104104
* @param tflNumberCode the entered TFL number, possibly missing a leading zero
105105
* @return the canonical TFL to store, or {@code null} when the value is not a valid TFL

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

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,39 +21,40 @@
2121
* (:32-339).
2222
*
2323
* <p><strong>It evaluates the assembled document, not the database.</strong> Legacy validates its
24-
* loaded domain object, so this takes the same input — which means Check Status sees exactly what the
25-
* GET serves, needs no queries of its own, and can be unit-tested from a hand-built document. It also
26-
* puts the derived totals within reach: several rules check {@code Total Costs}, {@code Total} and
27-
* {@code $/km}, which exist only as derived values.
24+
* loaded domain object, so this takes the same input — which means Check Status sees exactly what
25+
* the GET serves, needs no queries of its own, and can be unit-tested from a hand-built document.
26+
* It also puts the derived totals within reach: several rules check {@code Total Costs}, {@code
27+
* Total} and {@code $/km}, which exist only as derived values.
2828
*
2929
* <p><strong>Emission order is contractual.</strong> Rules run page-by-page, and within a page the
3030
* three page-level rules come before the per-road ones, in the order legacy declares them. The
3131
* ordinals that appear in the composed text are positional, so a reordering would silently renumber
3232
* the user's error list.
3333
*
3434
* <p>This class resolves no text. It emits bundle keys plus pre-formatted arguments and the label
35-
* prefix; the controller performs the single concatenation {@code label + ": " + resolvedText}, so the
36-
* verbatim byte composition lives in exactly one place.
35+
* prefix; the controller performs the single concatenation {@code label + ": " + resolvedText}, so
36+
* the verbatim byte composition lives in exactly one place.
3737
*
3838
* <p><strong>Legacy quirks preserved deliberately</strong> — each is reproduced, not corrected:
3939
* <ul>
40-
* <li>{@code Road Name} and {@code Sub Zone} are titled with the PAGE label only, so on a page with
41-
* several roads the user cannot tell which road is at fault.</li>
42-
* <li>{@code Sub Zone} has no control anywhere on the screen; it is populated only on read from the
43-
* catalogue row.</li>
44-
* <li>{@code Material Type Total (%)} is reported whenever the five percentages do not total 100
45-
* including when all five are blank, because the legacy total coerces nulls to zero and is
40+
* <li>{@code Road Name} and {@code Sub Zone} are titled with the PAGE label only, so on a page
41+
* with several roads the user cannot tell which road is at fault.</li>
42+
* <li>{@code Sub Zone} has no control anywhere on the screen; it is populated only on read from
43+
* the catalogue row.</li>
44+
* <li>{@code Material Type Total (%)} is reported whenever the five percentages do not total 100
45+
* including when all five are blank, because the legacy total coerces nulls to zero and is
4646
* therefore never absent.</li>
47-
* <li>Additional-stabilizing transfers are checked against a floor of ZERO here while the entry form
48-
* accepts negatives, so a value the form allowed can be reported as out of range.</li>
49-
* <li>{@code Region}, {@code Road Type} and {@code Ballast Method Code} are required on the form but
50-
* checked NOWHERE here.</li>
51-
* <li>End Haul and Overland figures are checked nowhere — those rules are commented out in legacy.</li>
52-
* <li>Both range bounds are formatted with the LOWER bound's pattern; legacy accepts an upper pattern
53-
* and then ignores it.</li>
54-
* <li>Label drift against the screen is kept as legacy writes it: {@code Ripple Rock} for the screen's
55-
* "Rippable Rock", {@code Less Landing} singular, {@code Less Other Eng}, and a lower-case
56-
* {@code total} in one stabilizing label.</li>
47+
* <li>Additional-stabilizing transfers are checked against a floor of ZERO here while the entry
48+
* form accepts negatives, so a value the form allowed can be reported as out of range.</li>
49+
* <li>{@code Region}, {@code Road Type} and {@code Ballast Method Code} are required on the form
50+
* but checked NOWHERE here.</li>
51+
* <li>End Haul and Overland figures are checked nowhere — those rules are commented out in
52+
* legacy.</li>
53+
* <li>Both range bounds are formatted with the LOWER bound's pattern; legacy accepts an upper
54+
* pattern and then ignores it.</li>
55+
* <li>Label drift against the screen is kept as legacy writes it: {@code Ripple Rock} for the
56+
* screen's "Rippable Rock", {@code Less Landing} singular, {@code Less Other Eng}, and a
57+
* lower-case {@code total} in one stabilizing label.</li>
5758
* </ul>
5859
*
5960
* <p>Two deliberate departures: the Boulder Area rule is dropped, because that field is removed by
@@ -78,8 +79,8 @@ final class Schedule10CheckStatus {
7879
/** Ballast method requiring the additional-stabilizing figures and a material type. */
7980
private static final String BALLAST_CRUSHED = "C";
8081

81-
// Legacy number patterns, transcribed per rule. Formatting is applied mechanically so the rendered
82-
// bounds are whatever the pattern produces, exactly as legacy renders them.
82+
// Legacy number patterns, transcribed per rule. Formatting is applied mechanically so the
83+
// rendered bounds are whatever the pattern produces, exactly as legacy renders them.
8384
private static final String FMT_INT = "###";
8485
private static final String FMT_3DP = "###.###";
8586
private static final String FMT_1DP = "###.#";
@@ -97,7 +98,9 @@ final class Schedule10CheckStatus {
9798
private Schedule10CheckStatus() {
9899
}
99100

100-
/** One outstanding requirement: the machine field, its label prefix, and an unresolved message. */
101+
/**
102+
* One outstanding requirement: the machine field, its label prefix, and an unresolved message.
103+
*/
101104
record Issue(String field, String label, String messageKey, List<String> args) {
102105
}
103106

@@ -182,8 +185,8 @@ static DetailOutcome evaluateRoadDetail(
182185
requirePresent(
183186
issues, "subzone", pagePrefix + " Sub Zone", bec == null ? null : bec.subzone());
184187

185-
// Legacy dereferences the classification and its id without a guard, so one road detail holding a
186-
// null foreign key aborts the entire check before any message is emitted. Reported instead.
188+
// Legacy dereferences the classification and its id without a guard, so one road detail holding
189+
// a null foreign key aborts the entire check before any message is emitted. Reported instead.
187190
if (bec == null || !allowableBec.contains(bec.biogeoclimaticCatalogueId())) {
188191
issues.add(new Issue("becClassification", prefix + " BEC Zone", MSG_BEC, List.of()));
189192
}
@@ -217,12 +220,14 @@ static DetailOutcome evaluateRoadDetail(
217220
requireRange(issues, "subGradeActualCost", prefix + " Sub-Grade: Actual Cost ($)",
218221
field(subGrade, SubGrade::actualCost), ZERO, FMT_MONEY, SEVEN_DIGITS, false);
219222
requireRange(issues, "subGradeTtTransfer", prefix + " Sub-Grade: TtT Transfer ($)",
220-
field(subGrade, SubGrade::ttTransfer), SEVEN_DIGITS.negate(), FMT_MONEY, SEVEN_DIGITS, false);
223+
field(subGrade, SubGrade::ttTransfer), SEVEN_DIGITS.negate(), FMT_MONEY, SEVEN_DIGITS,
224+
false);
221225
requireRange(issues, "subGradeOtherTransfer", prefix + " Sub-Grade: Other Transfer ($)",
222226
field(subGrade, SubGrade::otherTransfer), SEVEN_DIGITS.negate(), FMT_MONEY, SEVEN_DIGITS,
223227
false);
224228
requireRange(issues, "subGradeTotalCosts", prefix + " Sub-Grade: Total Costs ($)",
225-
field(subGrade, SubGrade::totalCosts), EIGHT_DIGITS.negate(), FMT_MONEY, EIGHT_DIGITS, false);
229+
field(subGrade, SubGrade::totalCosts), EIGHT_DIGITS.negate(), FMT_MONEY, EIGHT_DIGITS,
230+
false);
226231
requireRange(issues, "lessBridges", prefix + " Sub-Grade: Less Bridges ($)",
227232
field(subGrade, SubGrade::lessBridges), ZERO, FMT_MONEY, SEVEN_DIGITS, false);
228233
requireRange(issues, "lessCulverts", prefix + " Sub-Grade: Less Culverts ($)",
@@ -236,13 +241,15 @@ static DetailOutcome evaluateRoadDetail(
236241
requireRange(issues, "lessOtherEng", prefix + " Sub-Grade: Less Other Eng ($)",
237242
field(subGrade, SubGrade::lessOtherEng), ZERO, FMT_MONEY, SEVEN_DIGITS, false);
238243
requireRange(issues, "subGradeTotal", prefix + " Sub-Grade: Total ($)",
239-
field(subGrade, SubGrade::total), EIGHT_DIGITS.negate(), FMT_MONEY_2DP, EIGHT_DIGITS, false);
240-
requireRange(issues, "subGradeCostPerLength", prefix + " Sub-Grade: $/km",
241-
field(subGrade, SubGrade::costPerLength), EIGHT_DIGITS.negate(), FMT_MONEY_2DP, EIGHT_DIGITS,
244+
field(subGrade, SubGrade::total), EIGHT_DIGITS.negate(), FMT_MONEY_2DP, EIGHT_DIGITS,
242245
false);
246+
requireRange(issues, "subGradeCostPerLength", prefix + " Sub-Grade: $/km",
247+
field(subGrade, SubGrade::costPerLength), EIGHT_DIGITS.negate(), FMT_MONEY_2DP,
248+
EIGHT_DIGITS, false);
243249

244250
Stabilizing stabilizing = detail.stabilizing();
245-
boolean crushed = stabilizing != null && BALLAST_CRUSHED.equals(stabilizing.ballastMethodCode());
251+
boolean crushed =
252+
stabilizing != null && BALLAST_CRUSHED.equals(stabilizing.ballastMethodCode());
246253

247254
requireRange(issues, "stabilizingLength", prefix + " Additional Stabilizing: Length (km)",
248255
field(stabilizing, Stabilizing::length), ZERO, FMT_3DP, new BigDecimal("999.999"), crushed);
@@ -251,7 +258,8 @@ static DetailOutcome evaluateRoadDetail(
251258
field(stabilizing, Stabilizing::surfaceWidth), ZERO, FMT_1DP, new BigDecimal("999.9"),
252259
crushed);
253260
requireRange(issues, "stabilizingDepth", prefix + " Additional Stabilizing: Depth (m)",
254-
field(stabilizing, Stabilizing::depth), ZERO, FMT_2DP_SMALL, new BigDecimal("99.9"), crushed);
261+
field(stabilizing, Stabilizing::depth), ZERO, FMT_2DP_SMALL, new BigDecimal("99.9"),
262+
crushed);
255263
requireRange(issues, "stabilizingDistanceToSource",
256264
prefix + " Additional Stabilizing: Distance to Source (km)",
257265
field(stabilizing, Stabilizing::distanceToSource), ZERO, FMT_1DP, new BigDecimal("999.9"),
@@ -267,8 +275,8 @@ static DetailOutcome evaluateRoadDetail(
267275
requireRange(issues, "stabilizingActualCost",
268276
prefix + " Additional Stabilizing: Actual Cost ($)",
269277
stabilizing.actualCost(), ZERO, FMT_MONEY, SEVEN_DIGITS, true);
270-
// Floor of ZERO, while the entry form accepts down to -9,999,999 for both transfers. A value the
271-
// form allowed is therefore reported here. Legacy carries the same disagreement.
278+
// Floor of ZERO, while the entry form accepts down to -9,999,999 for both transfers. A value
279+
// the form allowed is therefore reported here. Legacy carries the same disagreement.
272280
requireRange(issues, "stabilizingTtTransfer",
273281
prefix + " Additional Stabilizing: TtT Transfer ($)",
274282
stabilizing.ttTransfer(), ZERO, FMT_MONEY, SEVEN_DIGITS, true);
@@ -292,13 +300,13 @@ private static void requirePresent(
292300
}
293301

294302
/**
295-
* The legacy numeric rule: an absent optional value passes, an absent required value is reported as
296-
* missing, and otherwise the value must sit inside the inclusive range.
303+
* The legacy numeric rule: an absent optional value passes, an absent required value is reported
304+
* as missing, and otherwise the value must sit inside the inclusive range.
297305
*
298306
* <p>When both bounds are identical the must-equal message is used with a single argument. Legacy
299307
* reaches that branch by comparing the two bounds by REFERENCE, which happens to work for the one
300-
* rule that uses it because small boxed integers are cached; numeric equality is used here instead,
301-
* which agrees for every rule in this schedule and does not depend on that accident.
308+
* rule that uses it because small boxed integers are cached; numeric equality is used here
309+
* instead, which agrees for every rule in this schedule and does not depend on that accident.
302310
*/
303311
private static void requireRange(
304312
List<Issue> issues, String field, String label, BigDecimal value,
@@ -324,8 +332,8 @@ private static void requireRange(
324332
* Renders a bound with its pattern.
325333
*
326334
* <p>Both bounds use the LOWER bound's pattern: legacy accepts an upper pattern and then never
327-
* applies it. Symbols are pinned to a fixed locale so the rendered separators cannot drift with the
328-
* server's default.
335+
* applies it. Symbols are pinned to a fixed locale so the rendered separators cannot drift with
336+
* the server's default.
329337
*/
330338
private static String format(BigDecimal bound, String pattern) {
331339
DecimalFormat format =

0 commit comments

Comments
 (0)