feat(t-paliad-206): switch Go layer to lowercase dot-form proceeding codes
Sweeps internal/services + internal/handlers + internal/models to use the new proceeding codes landed by mig 096. Stable Code* constants live in internal/services/proceeding_mapping.go so a future rename needs to touch one file. Substantive changes: - proceeding_mapping.go gains ResolveCounterclaimRouting() — the cascade resolver that routes upc.ccr.cfi (illustrative peer) back to upc.inf.cfi with with_ccr=true as default flag (design doc S1). - deadline_search_service.go forum-bucket map updated; upc.ccr.cfi added to upc_cfi since it is a CFI peer. - project_service.go CreateCounterclaim default lookup parameterised so the SQL string carries the constant, not a literal. - proceeding_codes_shape_test.go: new file. Validates the shape regex standalone (always runs) and walks live DB rows asserting every active fristenrechner row matches the new shape + every stable Code* constant resolves to exactly one active row. Comments and test fixtures throughout the Go tree updated to the new shape. Tests pass under `go test ./internal/... -short`.
This commit is contained in:
@@ -359,7 +359,7 @@ func itoa(n int) string {
|
||||
// POST /api/projects/{id}/counterclaim
|
||||
//
|
||||
// Body: {
|
||||
// "proceeding_type_id": 9, // optional, defaults to UPC_REV
|
||||
// "proceeding_type_id": 9, // optional, defaults to upc.rev.cfi
|
||||
// "flip_our_side": false, // optional, default-flip otherwise
|
||||
// "title": "EP3456789 — Widerklage (CCR)", // optional, auto-suggested
|
||||
// "case_number": "ACT_xxx_2026" // optional CCR case number
|
||||
|
||||
@@ -174,7 +174,7 @@ type Project struct {
|
||||
// InstanceLevel is the procedural instance the project sits at:
|
||||
// 'first' (default) | 'appeal' | 'cassation'. Combined with the
|
||||
// proceeding code + jurisdiction by FristenrechnerService to pick
|
||||
// the effective proceeding (DE_INF + appeal → DE_INF_OLG, etc.).
|
||||
// the effective proceeding (de.inf.lg + appeal → de.inf.olg, etc.).
|
||||
// NULL = unset / not applicable; the calculator treats NULL as
|
||||
// 'first'. Backfill happens via the project-detail picker UI
|
||||
// (Phase 3 Slice 8); this column ships in Slice 1 ahead of the
|
||||
@@ -594,7 +594,9 @@ type DeadlineRuleAudit struct {
|
||||
}
|
||||
|
||||
// ProceedingType is one of INF/REV/CCR/APM/APP/AMD/ZPO_CIVIL (matter
|
||||
// management) or UPC_*/DE_*/EPA_*/EP_GRANT (Fristenrechner UI).
|
||||
// management) or the lowercase dot-separated fristenrechner codes
|
||||
// (upc.*.*, de.*.*, epa.*.*, dpma.*.*) — see
|
||||
// docs/design-proceeding-code-taxonomy-2026-05-18.md.
|
||||
type ProceedingType struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
Code string `db:"code" json:"code"`
|
||||
|
||||
@@ -62,16 +62,16 @@ func (s *DeadlineSearchService) SetEventCategoryService(ec *EventCategoryService
|
||||
//
|
||||
// Empty bucket slug = no narrowing.
|
||||
var ForumToProceedingCodes = map[string][]string{
|
||||
"upc_cfi": {"UPC_INF", "UPC_REV", "UPC_PI", "UPC_DAMAGES", "UPC_DISCOVERY", "UPC_APP_ORDERS"},
|
||||
"upc_coa": {"UPC_APP", "UPC_COST_APPEAL"},
|
||||
"de_lg": {"DE_INF"},
|
||||
"de_olg": {"DE_INF_OLG"},
|
||||
"de_bgh": {"DE_INF_BGH", "DE_NULL_BGH", "DPMA_BGH_RB"},
|
||||
"de_bpatg": {"DE_NULL", "DPMA_BPATG_BESCHWERDE"},
|
||||
"epa_grant": {"EP_GRANT"},
|
||||
"epa_opp": {"EPA_OPP"},
|
||||
"epa_appeal": {"EPA_APP"},
|
||||
"dpma": {"DPMA_OPP"},
|
||||
"upc_cfi": {CodeUPCInfringement, CodeUPCRevocation, CodeUPCCounterclaim, CodeUPCPreliminary, CodeUPCDamages, CodeUPCDiscovery, CodeUPCAppealOrder},
|
||||
"upc_coa": {CodeUPCAppealMerits, CodeUPCAppealCost},
|
||||
"de_lg": {CodeDEInfringementLG},
|
||||
"de_olg": {CodeDEInfringementOLG},
|
||||
"de_bgh": {CodeDEInfringementBGH, CodeDENullityBGH, CodeDPMAAppealBGH},
|
||||
"de_bpatg": {CodeDENullityBPatG, CodeDPMAAppealBPatG},
|
||||
"epa_grant": {CodeEPAGrant},
|
||||
"epa_opp": {CodeEPAOpposition},
|
||||
"epa_appeal": {CodeEPAOppositionAppeal},
|
||||
"dpma": {CodeDPMAOpposition},
|
||||
}
|
||||
|
||||
// SearchOptions carries the optional facet filters from the URL query
|
||||
|
||||
@@ -96,14 +96,15 @@ func TestDeadlineSearch(t *testing.T) {
|
||||
}
|
||||
card := findCardBySlug(t, resp, "statement-of-defence")
|
||||
// Expected at minimum: UPC R.23, ZPO §276, PatG §82, EPC R.79, PatG §59.
|
||||
// The actual data has 9 rule rows (UPC_INF, UPC_REV, UPC_PI,
|
||||
// UPC_DAMAGES, UPC_DISCOVERY, DE_INF, DE_NULL, EPA_OPP, DPMA_OPP).
|
||||
// The actual data has 9 rule rows (upc.inf.cfi, upc.rev.cfi,
|
||||
// upc.pi.cfi, upc.dmgs.cfi, upc.disc.cfi, de.inf.lg,
|
||||
// de.null.bpatg, epa.opp.opd, dpma.opp.dpma).
|
||||
mustHaveLegalSource(t, card, "UPC.RoP.23.1")
|
||||
mustHaveLegalSource(t, card, "DE.ZPO.276.1")
|
||||
mustHaveLegalSource(t, card, "DE.PatG.82.1")
|
||||
mustHaveLegalSource(t, card, "EU.EPC-R.79.1")
|
||||
mustHaveLegalSource(t, card, "DE.PatG.59.3")
|
||||
mustHaveProceedingCodes(t, card, "UPC_INF", "DE_INF", "DE_NULL", "EPA_OPP", "DPMA_OPP")
|
||||
mustHaveProceedingCodes(t, card, CodeUPCInfringement, CodeDEInfringementLG, CodeDENullityBPatG, CodeEPAOpposition, CodeDPMAOpposition)
|
||||
})
|
||||
|
||||
t.Run("RoP 23 returns the UPC R.23 hit", func(t *testing.T) {
|
||||
@@ -169,7 +170,7 @@ func TestDeadlineSearch(t *testing.T) {
|
||||
}
|
||||
// Statement-of-defence is filed by the defendant. Filtering
|
||||
// party=claimant should NOT drop the concept entirely — the
|
||||
// effective_party can vary per pill (e.g. EPA_OPP Erwiderung
|
||||
// effective_party can vary per pill (e.g. epa.opp.opd Erwiderung
|
||||
// is owed by the patentee/claimant). At least it must not
|
||||
// return any card with EVERY pill on defendant side.
|
||||
for _, c := range resp.Cards {
|
||||
@@ -254,9 +255,9 @@ func TestDeadlineSearch(t *testing.T) {
|
||||
t.Fatalf("search: %v", err)
|
||||
}
|
||||
// Every rule pill must be a UPC proceeding. The seed maps every
|
||||
// concept under this subtree to UPC_INF or UPC_APP — no DE/EPA/
|
||||
// DPMA codes should leak.
|
||||
allowedRulePrefix := []string{"UPC_"}
|
||||
// concept under this subtree to upc.inf.cfi or upc.apl.merits — no
|
||||
// DE/EPA/DPMA codes should leak.
|
||||
allowedRulePrefix := []string{"upc."}
|
||||
for _, c := range resp.Cards {
|
||||
for _, p := range c.Pills {
|
||||
if p.Kind != "rule" {
|
||||
@@ -289,21 +290,21 @@ func TestDeadlineSearch(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("search: %v", err)
|
||||
}
|
||||
// Junction maps three concepts × UPC_INF for this leaf:
|
||||
// Junction maps three concepts × upc.inf.cfi for this leaf:
|
||||
// defence-to-counterclaim-for-revocation, application-to-amend,
|
||||
// reply-to-defence. Every pill must be UPC_INF.
|
||||
// reply-to-defence. Every pill must be upc.inf.cfi.
|
||||
for _, c := range resp.Cards {
|
||||
for _, p := range c.Pills {
|
||||
if p.Kind != "rule" {
|
||||
continue
|
||||
}
|
||||
if p.Proceeding == nil || p.Proceeding.Code != "UPC_INF" {
|
||||
if p.Proceeding == nil || p.Proceeding.Code != CodeUPCInfringement {
|
||||
code := "(nil)"
|
||||
if p.Proceeding != nil {
|
||||
code = p.Proceeding.Code
|
||||
}
|
||||
t.Errorf("klageerwiderung-mit-ccr leaf leaked non-UPC_INF pill on %q: proc=%s",
|
||||
c.Concept.Slug, code)
|
||||
t.Errorf("klageerwiderung-mit-ccr leaf leaked non-%s pill on %q: proc=%s",
|
||||
CodeUPCInfringement, c.Concept.Slug, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,8 +345,8 @@ func TestDeadlineSearch(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("v4 forum filter ANDs against subtree narrowing", func(t *testing.T) {
|
||||
// Pick the UPC_INF subtree and add a forum chip that excludes
|
||||
// UPC_INF — the result must be empty (the user contradicted
|
||||
// Pick the upc.inf.cfi subtree and add a forum chip that excludes
|
||||
// upc.inf.cfi — the result must be empty (the user contradicted
|
||||
// themselves; empty is the correct UX).
|
||||
resp, err := svc.Search(ctx, "", SearchOptions{
|
||||
EventCategorySlug: "cms-eingang.gegenseite.upc-inf",
|
||||
|
||||
@@ -238,8 +238,8 @@ func (s *EventCategoryService) ConceptIDsForSlug(ctx context.Context, slug strin
|
||||
//
|
||||
// Distinct from "every concept_id ever mapped" because a concept can
|
||||
// appear at the root view in MULTIPLE proceeding contexts that the tree
|
||||
// authors intentionally surfaced — e.g. opposition under both EPA_OPP
|
||||
// and DPMA_OPP. We respect those tuples even at the root so the
|
||||
// authors intentionally surfaced — e.g. opposition under both epa.opp.opd
|
||||
// and dpma.opp.dpma. We respect those tuples even at the root so the
|
||||
// result-card pill set matches the junction's design.
|
||||
func (s *EventCategoryService) AllOutcomes(ctx context.Context) ([]ConceptOutcome, error) {
|
||||
const sqlText = `
|
||||
|
||||
@@ -98,7 +98,7 @@ var ErrUnknownProceedingType = errors.New("unknown proceeding type")
|
||||
// empty/nil for the legacy behaviour.
|
||||
//
|
||||
// - PriorityDateStr: when non-empty (YYYY-MM-DD), rules with anchor_alt =
|
||||
// 'priority_date' (e.g. EP_GRANT.ep_grant.publish per Art. 93 EPÜ) use
|
||||
// 'priority_date' (e.g. epa.grant.exa.ep_grant.publish per Art. 93 EPÜ) use
|
||||
// this date as their base instead of the parent's adjusted date / the
|
||||
// trigger date.
|
||||
// - Flags: lowercase string flags from the UI (e.g. "with_ccr",
|
||||
@@ -158,13 +158,13 @@ type CalcOptions struct {
|
||||
// Audit-driven extensions:
|
||||
//
|
||||
// - opts.Flags can flip flag-conditioned rules onto their alt_* values
|
||||
// (e.g. UPC_INF inf.reply / inf.rejoin under "with_ccr"). When a
|
||||
// (e.g. upc.inf.cfi inf.reply / inf.rejoin under "with_ccr"). When a
|
||||
// rule's condition_flag array is non-empty, the rule renders iff
|
||||
// EVERY element is in opts.Flags; rules that fail this gate are
|
||||
// suppressed entirely (used by Phase B1 cross-flow rules that should
|
||||
// only appear with their flag).
|
||||
// - opts.PriorityDateStr overrides the anchor for rules with anchor_alt
|
||||
// set (e.g. EP_GRANT publication date is 18mo from priority, not filing).
|
||||
// set (e.g. epa.grant.exa publication date is 18mo from priority, not filing).
|
||||
// - opts.AnchorOverrides per-rule (rule_code → YYYY-MM-DD) lets the
|
||||
// caller redirect a downstream rule's parent anchor to a user-set
|
||||
// date. Used for court-extended deadlines and for entering
|
||||
@@ -318,7 +318,7 @@ func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, t
|
||||
// 3. parent set, court-determined → IsCourtSet (waypoint)
|
||||
// 4. parent set, NOT court-determined → "filed-with-parent"
|
||||
// semantic: rule is filed AT THE SAME TIME as its parent
|
||||
// (e.g. UPC_REV.rev.app_to_amend, rev.cc_inf — R.49(2) says
|
||||
// (e.g. upc.rev.cfi.rev.app_to_amend, rev.cc_inf — R.49(2) says
|
||||
// Application to amend / Counterclaim for infringement are
|
||||
// INCLUDED in the Defence to revocation). Use the parent's
|
||||
// computed date.
|
||||
@@ -432,7 +432,7 @@ func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, t
|
||||
continue
|
||||
}
|
||||
|
||||
// Anchor: prefer alt-anchor (e.g. priority_date for EP_GRANT publish)
|
||||
// Anchor: prefer alt-anchor (e.g. priority_date for epa.grant.exa publish)
|
||||
// when supplied, then parent's computed date (or user override),
|
||||
// then trigger date.
|
||||
baseDate := triggerDate
|
||||
@@ -715,7 +715,7 @@ func (s *FristenrechnerService) CalculateRule(ctx context.Context, params CalcRu
|
||||
}
|
||||
|
||||
// Zero-duration non-court-determined rules are "filed at the same
|
||||
// time as parent" markers (UPC_REV.app_to_amend, UPC_REV.cc_inf):
|
||||
// time as parent" markers (upc.rev.cfi.app_to_amend, upc.rev.cfi.cc_inf):
|
||||
// effectively mean "due on the trigger date itself". The card-click
|
||||
// flow doesn't need to surface those as a calc panel — but if it
|
||||
// does, returning the trigger date is the right answer.
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestAllFlagsSet(t *testing.T) {
|
||||
{"single flag, present → true (legacy with_ccr pattern)", []string{"with_ccr"}, mkSet("with_ccr"), true},
|
||||
{"single flag, absent → false", []string{"with_ccr"}, mkSet(), false},
|
||||
{"single flag, other present → false", []string{"with_ccr"}, mkSet("with_amend"), false},
|
||||
{"two flags, both present → true (UPC_INF nested)", []string{"with_ccr", "with_amend"}, mkSet("with_ccr", "with_amend"), true},
|
||||
{"two flags, both present → true (upc.inf.cfi nested)", []string{"with_ccr", "with_amend"}, mkSet("with_ccr", "with_amend"), true},
|
||||
{"two flags, only one present → false", []string{"with_ccr", "with_amend"}, mkSet("with_ccr"), false},
|
||||
{"two flags, both present + extra → true (extra flags don't matter)", []string{"with_ccr", "with_amend"}, mkSet("with_ccr", "with_amend", "with_cci"), true},
|
||||
}
|
||||
@@ -86,10 +86,10 @@ func TestCalculateRule(t *testing.T) {
|
||||
courts := NewCourtService(pool)
|
||||
svc := NewFristenrechnerService(rules, holidays, courts)
|
||||
|
||||
t.Run("plain rule calc — UPC_INF inf.sod, R.23(1), 3 months", func(t *testing.T) {
|
||||
t.Run("plain rule calc — upc.inf.cfi inf.sod, R.23(1), 3 months", func(t *testing.T) {
|
||||
// 2026-01-15 + 3 months = 2026-04-15. No vacation overlap.
|
||||
got, err := svc.CalculateRule(ctx, CalcRuleParams{
|
||||
ProceedingCode: "UPC_INF",
|
||||
ProceedingCode: CodeUPCInfringement,
|
||||
RuleLocalCode: "inf.sod",
|
||||
TriggerDate: "2026-01-15",
|
||||
})
|
||||
@@ -105,14 +105,14 @@ func TestCalculateRule(t *testing.T) {
|
||||
if got.Rule.LegalSourceDisplay != "UPC RoP R.23(1)" {
|
||||
t.Errorf("legalSourceDisplay = %q, want UPC RoP R.23(1)", got.Rule.LegalSourceDisplay)
|
||||
}
|
||||
if got.Proceeding.Code != "UPC_INF" {
|
||||
t.Errorf("proceeding code = %q, want UPC_INF", got.Proceeding.Code)
|
||||
if got.Proceeding.Code != CodeUPCInfringement {
|
||||
t.Errorf("proceeding code = %q, want upc.inf.cfi", got.Proceeding.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("court-determined rule → IsCourtSet=true, no dueDate", func(t *testing.T) {
|
||||
got, err := svc.CalculateRule(ctx, CalcRuleParams{
|
||||
ProceedingCode: "UPC_INF",
|
||||
ProceedingCode: CodeUPCInfringement,
|
||||
RuleLocalCode: "inf.decision",
|
||||
TriggerDate: "2026-01-15",
|
||||
})
|
||||
@@ -131,7 +131,7 @@ func TestCalculateRule(t *testing.T) {
|
||||
// inf.def_to_ccr requires with_ccr. Without the flag, FlagsRequired
|
||||
// is still surfaced so the UI can render the checkbox.
|
||||
got, err := svc.CalculateRule(ctx, CalcRuleParams{
|
||||
ProceedingCode: "UPC_INF",
|
||||
ProceedingCode: CodeUPCInfringement,
|
||||
RuleLocalCode: "inf.def_to_ccr",
|
||||
TriggerDate: "2026-01-15",
|
||||
})
|
||||
@@ -148,7 +148,7 @@ func TestCalculateRule(t *testing.T) {
|
||||
|
||||
t.Run("flag-conditional rule with flag → FlagsApplied populated", func(t *testing.T) {
|
||||
got, err := svc.CalculateRule(ctx, CalcRuleParams{
|
||||
ProceedingCode: "UPC_INF",
|
||||
ProceedingCode: CodeUPCInfringement,
|
||||
RuleLocalCode: "inf.def_to_ccr",
|
||||
TriggerDate: "2026-01-15",
|
||||
Flags: []string{"with_ccr"},
|
||||
@@ -163,7 +163,7 @@ func TestCalculateRule(t *testing.T) {
|
||||
|
||||
t.Run("missing TriggerDate → error", func(t *testing.T) {
|
||||
_, err := svc.CalculateRule(ctx, CalcRuleParams{
|
||||
ProceedingCode: "UPC_INF",
|
||||
ProceedingCode: CodeUPCInfringement,
|
||||
RuleLocalCode: "inf.sod",
|
||||
TriggerDate: "",
|
||||
})
|
||||
@@ -174,7 +174,7 @@ func TestCalculateRule(t *testing.T) {
|
||||
|
||||
t.Run("unknown rule → ErrUnknownRule", func(t *testing.T) {
|
||||
_, err := svc.CalculateRule(ctx, CalcRuleParams{
|
||||
ProceedingCode: "UPC_INF",
|
||||
ProceedingCode: CodeUPCInfringement,
|
||||
RuleLocalCode: "totally.fake",
|
||||
TriggerDate: "2026-01-15",
|
||||
})
|
||||
@@ -417,12 +417,12 @@ func TestUIDeadline_WireShape_Slice8(t *testing.T) {
|
||||
courts := NewCourtService(pool)
|
||||
svc := NewFristenrechnerService(rules, holidays, courts)
|
||||
|
||||
resp, err := svc.Calculate(ctx, "UPC_INF", "2026-01-15", CalcOptions{})
|
||||
resp, err := svc.Calculate(ctx, CodeUPCInfringement, "2026-01-15", CalcOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Calculate UPC_INF: %v", err)
|
||||
t.Fatalf("Calculate upc.inf.cfi: %v", err)
|
||||
}
|
||||
if len(resp.Deadlines) == 0 {
|
||||
t.Fatal("Calculate UPC_INF returned no deadlines — seed-data missing?")
|
||||
t.Fatal("Calculate upc.inf.cfi returned no deadlines — seed-data missing?")
|
||||
}
|
||||
|
||||
allowed := map[string]bool{
|
||||
@@ -446,6 +446,6 @@ func TestUIDeadline_WireShape_Slice8(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if !sawConditionExpr {
|
||||
t.Logf("warning: no UPC_INF rule had conditionExpr populated — verify mig 084 ran")
|
||||
t.Logf("warning: no upc.inf.cfi rule had conditionExpr populated — verify mig 084 ran")
|
||||
}
|
||||
}
|
||||
|
||||
121
internal/services/proceeding_codes_shape_test.go
Normal file
121
internal/services/proceeding_codes_shape_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"mgit.msbls.de/m/paliad/internal/db"
|
||||
)
|
||||
|
||||
// shapeRegex is the lowercase dot-separated form ratified by t-paliad-204
|
||||
// and enforced at the DB layer by mig 096's paliad_proceeding_code_shape
|
||||
// CHECK constraint. Every active fristenrechner-category row must match.
|
||||
var shapeRegex = regexp.MustCompile(`^[a-z]+\.[a-z]+\.[a-z]+$`)
|
||||
|
||||
// TestProceedingCodeShape walks every active fristenrechner-category row
|
||||
// in paliad.proceeding_types and asserts the `code` matches the
|
||||
// taxonomy regex. Catches future inserts that slip past the CHECK
|
||||
// constraint (e.g. via a manual psql edit on a staging snapshot) and
|
||||
// catches drift between this Go layer's stable code constants and the
|
||||
// DB.
|
||||
//
|
||||
// Mirrors the assertions in mig 096 §8 — same regex, same shape — so a
|
||||
// failure here pinpoints which row went off-shape without making a DB
|
||||
// trip first.
|
||||
//
|
||||
// Skipped when TEST_DATABASE_URL is unset, mirroring the pattern in
|
||||
// project_service_test.go.
|
||||
func TestProceedingCodeShape(t *testing.T) {
|
||||
url := os.Getenv("TEST_DATABASE_URL")
|
||||
if url == "" {
|
||||
t.Skip("TEST_DATABASE_URL not set — skipping live DB test")
|
||||
}
|
||||
if err := db.ApplyMigrations(url); err != nil {
|
||||
t.Fatalf("apply migrations: %v", err)
|
||||
}
|
||||
pool, err := sqlx.Connect("postgres", url)
|
||||
if err != nil {
|
||||
t.Fatalf("connect: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var rows []struct {
|
||||
ID int `db:"id"`
|
||||
Code string `db:"code"`
|
||||
}
|
||||
if err := pool.SelectContext(ctx, &rows,
|
||||
`SELECT id, code FROM paliad.proceeding_types
|
||||
WHERE category = 'fristenrechner' AND is_active = true
|
||||
ORDER BY id`); err != nil {
|
||||
t.Fatalf("load active fristenrechner rows: %v", err)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
t.Fatal("no active fristenrechner rows — mig 096 likely not applied")
|
||||
}
|
||||
for _, r := range rows {
|
||||
if !shapeRegex.MatchString(r.Code) {
|
||||
t.Errorf("proceeding_types[id=%d] code=%q does not match taxonomy shape %s",
|
||||
r.ID, r.Code, shapeRegex.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Spot-check the stable code constants in proceeding_mapping.go all
|
||||
// resolve to live rows. Catches a constant being renamed without a
|
||||
// matching mig update.
|
||||
stable := []string{
|
||||
CodeUPCInfringement, CodeUPCRevocation, CodeUPCCounterclaim,
|
||||
CodeUPCPreliminary, CodeUPCDamages, CodeUPCDiscovery,
|
||||
CodeUPCAppealMerits, CodeUPCAppealOrder, CodeUPCAppealCost,
|
||||
CodeDEInfringementLG, CodeDEInfringementOLG, CodeDEInfringementBGH,
|
||||
CodeDENullityBPatG, CodeDENullityBGH,
|
||||
CodeEPAGrant, CodeEPAOpposition, CodeEPAOppositionAppeal,
|
||||
CodeDPMAOpposition, CodeDPMAAppealBPatG, CodeDPMAAppealBGH,
|
||||
}
|
||||
for _, c := range stable {
|
||||
var hit int
|
||||
if err := pool.GetContext(ctx, &hit,
|
||||
`SELECT count(*) FROM paliad.proceeding_types
|
||||
WHERE code = $1 AND is_active = true`, c); err != nil {
|
||||
t.Fatalf("count rows for %s: %v", c, err)
|
||||
}
|
||||
if hit != 1 {
|
||||
t.Errorf("stable code constant %q matches %d active rows, want 1", c, hit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestProceedingCodeShapeRegexStandalone exercises the regex without
|
||||
// hitting the DB so the shape rule is verified on every `go test ./...`
|
||||
// run (no skip when TEST_DATABASE_URL is unset).
|
||||
func TestProceedingCodeShapeRegexStandalone(t *testing.T) {
|
||||
good := []string{
|
||||
"upc.inf.cfi", "upc.rev.cfi", "upc.ccr.cfi", "upc.apl.merits",
|
||||
"upc.apl.order", "upc.apl.cost", "de.inf.lg", "de.null.bgh",
|
||||
"epa.opp.opd", "epa.grant.exa", "dpma.opp.dpma",
|
||||
}
|
||||
for _, code := range good {
|
||||
if !shapeRegex.MatchString(code) {
|
||||
t.Errorf("good code %q rejected by shape regex", code)
|
||||
}
|
||||
}
|
||||
bad := []string{
|
||||
"UPC_INF", // old uppercase
|
||||
"upc.inf", // missing third position
|
||||
"upc.inf.cfi.extra", // four positions
|
||||
"upc..cfi", // empty middle
|
||||
"upc-inf-cfi", // dashes
|
||||
"_archived_litigation",
|
||||
}
|
||||
for _, code := range bad {
|
||||
if shapeRegex.MatchString(code) {
|
||||
t.Errorf("bad code %q accepted by shape regex", code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,49 @@ package services
|
||||
// proceeding_mapping bridges the two proceeding-type vocabularies in the
|
||||
// codebase: the **litigation** conceptual category (INF / REV / APP /
|
||||
// CCR / AMD / APM / ZPO_CIVIL) used by the historical project-binding
|
||||
// + Pipeline-A rules, and the **fristenrechner** code category (UPC_INF
|
||||
// / DE_INF / EPA_OPP / …) used by the Determinator cascade + rule
|
||||
// engine. Post-Phase-3-Slice-5 (t-paliad-186) projects bind to
|
||||
// fristenrechner codes directly, but the litigation→fristenrechner
|
||||
// + Pipeline-A rules, and the **fristenrechner** code category
|
||||
// (upc.inf.cfi / de.inf.lg / epa.opp.opd / …) used by the Determinator
|
||||
// cascade + rule engine. Post-Phase-3-Slice-5 (t-paliad-186) projects
|
||||
// bind to fristenrechner codes directly, but the litigation→fristenrechner
|
||||
// mapping is still needed for the ~40 Pipeline-A rules that remain on
|
||||
// litigation proceedings and for any other surface that thinks in
|
||||
// litigation terms.
|
||||
//
|
||||
// The mapping table here is the single source of truth — see
|
||||
// docs/design-determinator-row-cascade-2026-05-13.md §4.2 for the
|
||||
// design rationale + ambiguity notes. **Never silent FK promotion**:
|
||||
// every ambiguous case returns ok=false so callers can degrade
|
||||
// gracefully ("no narrowing") instead of guessing.
|
||||
// design rationale + ambiguity notes, and
|
||||
// docs/design-proceeding-code-taxonomy-2026-05-18.md for the
|
||||
// lowercase dot-separated naming convention applied by mig 096
|
||||
// (t-paliad-206). **Never silent FK promotion**: every ambiguous case
|
||||
// returns ok=false so callers can degrade gracefully ("no narrowing")
|
||||
// instead of guessing.
|
||||
|
||||
// Stable code constants — the strings landed by mig 096. Use these
|
||||
// throughout the codebase so a future rename only needs to touch this
|
||||
// file. The id-anchored FKs (deadline_rules.proceeding_type_id,
|
||||
// projects.proceeding_type_id) are unaffected by the rename.
|
||||
const (
|
||||
CodeUPCInfringement = "upc.inf.cfi"
|
||||
CodeUPCRevocation = "upc.rev.cfi"
|
||||
CodeUPCCounterclaim = "upc.ccr.cfi"
|
||||
CodeUPCPreliminary = "upc.pi.cfi"
|
||||
CodeUPCDamages = "upc.dmgs.cfi"
|
||||
CodeUPCDiscovery = "upc.disc.cfi"
|
||||
CodeUPCAppealMerits = "upc.apl.merits"
|
||||
CodeUPCAppealOrder = "upc.apl.order"
|
||||
CodeUPCAppealCost = "upc.apl.cost"
|
||||
CodeDEInfringementLG = "de.inf.lg"
|
||||
CodeDEInfringementOLG = "de.inf.olg"
|
||||
CodeDEInfringementBGH = "de.inf.bgh"
|
||||
CodeDENullityBPatG = "de.null.bpatg"
|
||||
CodeDENullityBGH = "de.null.bgh"
|
||||
CodeEPAGrant = "epa.grant.exa"
|
||||
CodeEPAOpposition = "epa.opp.opd"
|
||||
CodeEPAOppositionAppeal = "epa.opp.boa"
|
||||
CodeDPMAOpposition = "dpma.opp.dpma"
|
||||
CodeDPMAAppealBPatG = "dpma.appeal.bpatg"
|
||||
CodeDPMAAppealBGH = "dpma.appeal.bgh"
|
||||
)
|
||||
|
||||
// MapLitigationToFristenrechner returns the fristenrechner code +
|
||||
// condition flags implied by a (litigationCode, jurisdiction) pair.
|
||||
@@ -27,61 +57,83 @@ package services
|
||||
// and leave the cascade wide-open rather than auto-pick.
|
||||
//
|
||||
// Condition flags are returned as a slice so callers can apply them
|
||||
// alongside the fristenrechner code (CCR+UPC → UPC_INF + with_ccr,
|
||||
// AMD+UPC → UPC_INF + with_amend). An empty slice means no flag
|
||||
// alongside the fristenrechner code (CCR+UPC → upc.inf.cfi + with_ccr,
|
||||
// AMD+UPC → upc.inf.cfi + with_amend). An empty slice means no flag
|
||||
// context applies.
|
||||
func MapLitigationToFristenrechner(litigationCode, jurisdiction string) (fristenrechnerCode string, conditionFlags []string, ok bool) {
|
||||
switch litigationCode {
|
||||
case "INF":
|
||||
switch jurisdiction {
|
||||
case "UPC":
|
||||
return "UPC_INF", nil, true
|
||||
return CodeUPCInfringement, nil, true
|
||||
case "DE":
|
||||
return "DE_INF", nil, true
|
||||
return CodeDEInfringementLG, nil, true
|
||||
}
|
||||
case "REV":
|
||||
switch jurisdiction {
|
||||
case "UPC":
|
||||
return "UPC_REV", nil, true
|
||||
return CodeUPCRevocation, nil, true
|
||||
case "DE":
|
||||
return "DE_NULL", nil, true
|
||||
return CodeDENullityBPatG, nil, true
|
||||
}
|
||||
case "CCR":
|
||||
// Counterclaim revocation — UPC fold-in is structural (the
|
||||
// counterclaim lives inside an UPC_INF proceeding with the
|
||||
// counterclaim lives inside an upc.inf.cfi proceeding with the
|
||||
// with_ccr flag). DE Nichtigkeit is conceptually the same
|
||||
// adversarial-validity test, no separate flag.
|
||||
switch jurisdiction {
|
||||
case "UPC":
|
||||
return "UPC_INF", []string{"with_ccr"}, true
|
||||
return CodeUPCInfringement, []string{"with_ccr"}, true
|
||||
case "DE":
|
||||
return "DE_NULL", nil, true
|
||||
return CodeDENullityBPatG, nil, true
|
||||
}
|
||||
case "AMD":
|
||||
// Amendment-application bundled into UPC_INF via with_amend.
|
||||
// Amendment-application bundled into upc.inf.cfi via with_amend.
|
||||
// No DE / EPA / DPMA analogue today.
|
||||
if jurisdiction == "UPC" {
|
||||
return "UPC_INF", []string{"with_amend"}, true
|
||||
return CodeUPCInfringement, []string{"with_amend"}, true
|
||||
}
|
||||
case "APP":
|
||||
// Appeal is ambiguous in DE (OLG vs BGH) and the project
|
||||
// model doesn't carry the instance hint we'd need to
|
||||
// disambiguate. UPC is unambiguous.
|
||||
// disambiguate. UPC is unambiguous — upc.apl.merits covers
|
||||
// the merits appeal track for inf/rev/ccr/damages.
|
||||
if jurisdiction == "UPC" {
|
||||
return "UPC_APP", nil, true
|
||||
return CodeUPCAppealMerits, nil, true
|
||||
}
|
||||
case "APM":
|
||||
// Preliminary injunction / urgency procedure — UPC-only
|
||||
// concept in the fristenrechner taxonomy.
|
||||
if jurisdiction == "UPC" {
|
||||
return "UPC_PI", nil, true
|
||||
return CodeUPCPreliminary, nil, true
|
||||
}
|
||||
case "OPP":
|
||||
// Opposition — primarily EPA. DPMA has DPMA_OPP but it
|
||||
// Opposition — primarily EPA. DPMA has dpma.opp.dpma but it
|
||||
// doesn't surface from the litigation vocabulary today.
|
||||
if jurisdiction == "EPA" {
|
||||
return "EPA_OPP", nil, true
|
||||
return CodeEPAOpposition, nil, true
|
||||
}
|
||||
}
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
// ResolveCounterclaimRouting handles the determinator's
|
||||
// upc.ccr.cfi illustrative-peer route: the code exists in the dropdown
|
||||
// for taxonomic completeness, but no rules are attached to it. When the
|
||||
// cascade resolves to upc.ccr.cfi we route the rule lookup back to
|
||||
// upc.inf.cfi with a default with_ccr=true flag — see
|
||||
// docs/design-proceeding-code-taxonomy-2026-05-18.md §0.3 sub-decision S1.
|
||||
//
|
||||
// `code` is the proceeding code the cascade resolved to. If it's
|
||||
// upc.ccr.cfi, the function returns (CodeUPCInfringement,
|
||||
// []string{"with_ccr"}, true). For any other code the function returns
|
||||
// (code, nil, false) and callers proceed with the code unchanged. The
|
||||
// boolean signals "routing was applied"; the caller can surface the hint
|
||||
// "Regeln liegen auf upc.inf.cfi (with_ccr=true); wir leiten Sie dorthin
|
||||
// weiter." in the UI.
|
||||
func ResolveCounterclaimRouting(code string) (effectiveCode string, defaultFlags []string, routed bool) {
|
||||
if code == CodeUPCCounterclaim {
|
||||
return CodeUPCInfringement, []string{"with_ccr"}, true
|
||||
}
|
||||
return code, nil, false
|
||||
}
|
||||
|
||||
@@ -14,20 +14,20 @@ func TestMapLitigationToFristenrechner(t *testing.T) {
|
||||
}
|
||||
cases := []tc{
|
||||
// Unambiguous UPC fold-ins.
|
||||
{"INF", "UPC", "UPC_INF", nil, true},
|
||||
{"REV", "UPC", "UPC_REV", nil, true},
|
||||
{"APP", "UPC", "UPC_APP", nil, true},
|
||||
{"APM", "UPC", "UPC_PI", nil, true},
|
||||
// CCR + UPC = UPC_INF with the with_ccr flag.
|
||||
{"CCR", "UPC", "UPC_INF", []string{"with_ccr"}, true},
|
||||
// AMD + UPC = UPC_INF with the with_amend flag.
|
||||
{"AMD", "UPC", "UPC_INF", []string{"with_amend"}, true},
|
||||
{"INF", "UPC", CodeUPCInfringement, nil, true},
|
||||
{"REV", "UPC", CodeUPCRevocation, nil, true},
|
||||
{"APP", "UPC", CodeUPCAppealMerits, nil, true},
|
||||
{"APM", "UPC", CodeUPCPreliminary, nil, true},
|
||||
// CCR + UPC = upc.inf.cfi with the with_ccr flag.
|
||||
{"CCR", "UPC", CodeUPCInfringement, []string{"with_ccr"}, true},
|
||||
// AMD + UPC = upc.inf.cfi with the with_amend flag.
|
||||
{"AMD", "UPC", CodeUPCInfringement, []string{"with_amend"}, true},
|
||||
// DE first-instance / Nichtigkeit mappings.
|
||||
{"INF", "DE", "DE_INF", nil, true},
|
||||
{"REV", "DE", "DE_NULL", nil, true},
|
||||
{"CCR", "DE", "DE_NULL", nil, true},
|
||||
{"INF", "DE", CodeDEInfringementLG, nil, true},
|
||||
{"REV", "DE", CodeDENullityBPatG, nil, true},
|
||||
{"CCR", "DE", CodeDENullityBPatG, nil, true},
|
||||
// EPA opposition.
|
||||
{"OPP", "EPA", "EPA_OPP", nil, true},
|
||||
{"OPP", "EPA", CodeEPAOpposition, nil, true},
|
||||
// Ambiguous: APP+DE has both OLG and BGH analogues; project
|
||||
// model can't disambiguate, so degrade.
|
||||
{"APP", "DE", "", nil, false},
|
||||
@@ -52,3 +52,32 @@ func TestMapLitigationToFristenrechner(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCounterclaimRouting(t *testing.T) {
|
||||
t.Run("upc.ccr.cfi routes to upc.inf.cfi with with_ccr", func(t *testing.T) {
|
||||
gotCode, gotFlags, routed := ResolveCounterclaimRouting(CodeUPCCounterclaim)
|
||||
if gotCode != CodeUPCInfringement {
|
||||
t.Errorf("effective code = %q, want %q", gotCode, CodeUPCInfringement)
|
||||
}
|
||||
if !reflect.DeepEqual(gotFlags, []string{"with_ccr"}) {
|
||||
t.Errorf("default flags = %v, want [with_ccr]", gotFlags)
|
||||
}
|
||||
if !routed {
|
||||
t.Errorf("routed = false, want true")
|
||||
}
|
||||
})
|
||||
t.Run("non-ccr code passes through unchanged", func(t *testing.T) {
|
||||
for _, code := range []string{CodeUPCInfringement, CodeUPCRevocation, CodeDEInfringementLG, "anything-else"} {
|
||||
gotCode, gotFlags, routed := ResolveCounterclaimRouting(code)
|
||||
if gotCode != code {
|
||||
t.Errorf("ResolveCounterclaimRouting(%q) returned %q, want pass-through", code, gotCode)
|
||||
}
|
||||
if gotFlags != nil {
|
||||
t.Errorf("ResolveCounterclaimRouting(%q) flags = %v, want nil", code, gotFlags)
|
||||
}
|
||||
if routed {
|
||||
t.Errorf("ResolveCounterclaimRouting(%q) routed = true, want false", code)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -134,8 +134,8 @@ type CreateProjectInput struct {
|
||||
// 'first' (default once the picker UI lands) | 'appeal' | 'cassation'.
|
||||
// NULL = unset. Phase 3 Slice 8 (t-paliad-189, design §7) — the
|
||||
// SmartTimeline + calculator combine this with proceeding_code +
|
||||
// jurisdiction to pick the effective rule corpus (DE_INF + appeal →
|
||||
// DE_INF_OLG, etc.). Validated against the mig 080 CHECK on the
|
||||
// jurisdiction to pick the effective rule corpus (de.inf.lg + appeal →
|
||||
// de.inf.olg, etc.). Validated against the mig 080 CHECK on the
|
||||
// column; service surfaces ErrInvalidInput on a bad value.
|
||||
InstanceLevel *string `json:"instance_level,omitempty"`
|
||||
|
||||
@@ -1178,7 +1178,7 @@ func (s *ProjectService) Delete(ctx context.Context, userID, id uuid.UUID) error
|
||||
}
|
||||
|
||||
// CounterclaimOpts narrows CreateCounterclaim. Empty zero values fall back
|
||||
// to the design defaults: proceeding_type_id = UPC_REV, our_side = inverted
|
||||
// to the design defaults: proceeding_type_id = upc.rev.cfi, our_side = inverted
|
||||
// from the parent, title = "<patent reference> — Widerklage (CCR)" when a
|
||||
// patent reference is resolvable, else "<parent title> — Widerklage".
|
||||
//
|
||||
@@ -1229,7 +1229,7 @@ func (s *ProjectService) LoadCounterclaimChildrenVisible(ctx context.Context, us
|
||||
// and "both" pass through unchanged. The opts.FlipOurSide override
|
||||
// supports the rare R.49.2.b CCI shape where flipping is wrong.
|
||||
//
|
||||
// proceeding_type_id default (§4.4): UPC_REV for the standard CCR-on-
|
||||
// proceeding_type_id default (§4.4): upc.rev.cfi for the standard CCR-on-
|
||||
// validity. UPC_CCI is the rarer R.49.2.b path; callers pass the id
|
||||
// explicitly when they want it.
|
||||
func (s *ProjectService) CreateCounterclaim(ctx context.Context, userID, parentID uuid.UUID, opts CounterclaimOpts) (*models.Project, error) {
|
||||
@@ -1248,7 +1248,7 @@ func (s *ProjectService) CreateCounterclaim(ctx context.Context, userID, parentI
|
||||
return nil, fmt.Errorf("%w: parent project is itself a counterclaim — two-level CCR chains are not allowed", ErrInvalidInput)
|
||||
}
|
||||
|
||||
// Resolve proceeding_type_id default to UPC_REV when caller didn't
|
||||
// Resolve proceeding_type_id default to upc.rev.cfi when caller didn't
|
||||
// override. The DB row is required because the projection layer
|
||||
// dereferences it (paliad.proceeding_types.code).
|
||||
procTypeID := 0
|
||||
@@ -1257,9 +1257,9 @@ func (s *ProjectService) CreateCounterclaim(ctx context.Context, userID, parentI
|
||||
} else {
|
||||
err := s.db.GetContext(ctx, &procTypeID,
|
||||
`SELECT id FROM paliad.proceeding_types
|
||||
WHERE code = 'UPC_REV' AND is_active = true`)
|
||||
WHERE code = $1 AND is_active = true`, CodeUPCRevocation)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve default UPC_REV proceeding type: %w", err)
|
||||
return nil, fmt.Errorf("resolve default %s proceeding type: %w", CodeUPCRevocation, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1920,8 +1920,8 @@ func validateOurSide(s string) error {
|
||||
|
||||
// validateInstanceLevel checks the procedural-instance enum (Phase 3
|
||||
// Slice 8, t-paliad-189, design §7). Empty string clears the column;
|
||||
// the three named values map to the rule-corpus ladder DE_INF →
|
||||
// DE_INF_OLG → DE_INF_BGH that the SmartTimeline will surface in a
|
||||
// the three named values map to the rule-corpus ladder de.inf.lg →
|
||||
// de.inf.olg → de.inf.bgh that the SmartTimeline will surface in a
|
||||
// follow-up calculator slice. The DB-level CHECK on mig 080 enforces
|
||||
// the same set; this validation gives a clearer error than letting
|
||||
// the trigger fire.
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
// service layer (defence-in-depth). A non-fristenrechner-category
|
||||
// id INSERT via plain SQL must raise EXCEPTION.
|
||||
//
|
||||
// 4. Passing a fristenrechner-category id (UPC_INF) succeeds.
|
||||
// 4. Passing a fristenrechner-category id (upc.inf.cfi) succeeds.
|
||||
//
|
||||
// Phase 3 Slice 9 follow-up B (t-paliad-200, mig 093) retired the
|
||||
// 'litigation' category from the rule corpus; the negative-case lookup
|
||||
@@ -90,8 +90,9 @@ func TestProjectService_ProceedingTypeCategoryGuard(t *testing.T) {
|
||||
var fristenrechnerID int
|
||||
if err := pool.GetContext(ctx, &fristenrechnerID,
|
||||
`SELECT id FROM paliad.proceeding_types
|
||||
WHERE category = 'fristenrechner' AND code = 'UPC_INF' AND is_active = true`); err != nil {
|
||||
t.Fatalf("look up UPC_INF id: %v", err)
|
||||
WHERE category = 'fristenrechner' AND code = $1 AND is_active = true`,
|
||||
CodeUPCInfringement); err != nil {
|
||||
t.Fatalf("look up %s id: %v", CodeUPCInfringement, err)
|
||||
}
|
||||
|
||||
users := NewUserService(pool)
|
||||
|
||||
@@ -46,18 +46,20 @@ func TestCreateCounterclaim_Live(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userID := uuid.New()
|
||||
patentID := uuid.New() // sibling parent: the patent hub
|
||||
caseID := uuid.New() // the parent case (UPC_INF)
|
||||
caseID := uuid.New() // the parent case (upc.inf.cfi)
|
||||
|
||||
// Resolve UPC_INF + UPC_REV ids once. We need real ids from the
|
||||
// proceeding_types seed because they're NOT NULL on the test row.
|
||||
// Resolve upc.inf.cfi + upc.rev.cfi ids once. We need real ids from
|
||||
// the proceeding_types seed because they're NOT NULL on the test row.
|
||||
var upcInf, upcRev int
|
||||
if err := pool.GetContext(ctx, &upcInf,
|
||||
`SELECT id FROM paliad.proceeding_types WHERE code = 'UPC_INF'`); err != nil {
|
||||
t.Fatalf("resolve UPC_INF: %v", err)
|
||||
`SELECT id FROM paliad.proceeding_types WHERE code = $1`,
|
||||
CodeUPCInfringement); err != nil {
|
||||
t.Fatalf("resolve %s: %v", CodeUPCInfringement, err)
|
||||
}
|
||||
if err := pool.GetContext(ctx, &upcRev,
|
||||
`SELECT id FROM paliad.proceeding_types WHERE code = 'UPC_REV'`); err != nil {
|
||||
t.Fatalf("resolve UPC_REV: %v", err)
|
||||
`SELECT id FROM paliad.proceeding_types WHERE code = $1`,
|
||||
CodeUPCRevocation); err != nil {
|
||||
t.Fatalf("resolve %s: %v", CodeUPCRevocation, err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
@@ -102,7 +104,7 @@ func TestCreateCounterclaim_Live(t *testing.T) {
|
||||
patentID, userID); err != nil {
|
||||
t.Fatalf("seed patent team: %v", err)
|
||||
}
|
||||
// Child case (UPC_INF) under the patent.
|
||||
// Child case (upc.inf.cfi) under the patent.
|
||||
if _, err := pool.ExecContext(ctx,
|
||||
`INSERT INTO paliad.projects
|
||||
(id, type, parent_id, path, title, status, created_by,
|
||||
@@ -151,9 +153,9 @@ func TestCreateCounterclaim_Live(t *testing.T) {
|
||||
if child.OurSide == nil || *child.OurSide != "defendant" {
|
||||
t.Errorf("child.OurSide = %v, want defendant", child.OurSide)
|
||||
}
|
||||
// 4. Default proceeding_type_id resolved to UPC_REV.
|
||||
// 4. Default proceeding_type_id resolved to upc.rev.cfi.
|
||||
if child.ProceedingTypeID == nil || *child.ProceedingTypeID != upcRev {
|
||||
t.Errorf("child.ProceedingTypeID = %v, want UPC_REV (%d)", child.ProceedingTypeID, upcRev)
|
||||
t.Errorf("child.ProceedingTypeID = %v, want upc.rev.cfi (%d)", child.ProceedingTypeID, upcRev)
|
||||
}
|
||||
// 5. Auto-suggested title carries the patent reference + suffix.
|
||||
if !strings.Contains(child.Title, "EP3456789") || !strings.Contains(child.Title, "Widerklage") {
|
||||
|
||||
@@ -599,7 +599,7 @@ func laneLabelFor(child *models.Project, policy LevelPolicy) string {
|
||||
switch policy.LaneAxis {
|
||||
case "child_case":
|
||||
// Append the proceeding type code when known so the lawyer can
|
||||
// identify which case at a glance ("UPC-CFI München (UPC_INF)").
|
||||
// identify which case at a glance ("UPC-CFI München (upc.inf.cfi)").
|
||||
if child.ProceedingTypeID != nil {
|
||||
return child.Title
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user