package services import ( "testing" "github.com/google/uuid" ) // TestProjectCodeSegment pins the per-type segment derivation rules // from t-paliad-222 design §3.2: // // client → reference if set, else sanitized title (cap 8 chars) // litigation → opponent_code verbatim (empty → skipped) // patent → last 3 digits of patent_number // case → uppercase tail of proceeding_types.code // project → "" func TestProjectCodeSegment(t *testing.T) { str := func(s string) *string { return &s } intp := func(i int) *int { return &i } cases := []struct { name string row projectChainRow want string }{ // Client rows. { "client with reference", projectChainRow{Type: "client", Title: "Example Co.", Reference: str("EXMPL")}, "EXMPL", }, { "client without reference falls back to slug(title)", projectChainRow{Type: "client", Title: "Example Co.", Reference: nil}, "EXAMPLEC", }, { "client without reference, diacritics stripped", projectChainRow{Type: "client", Title: "Müller GmbH"}, "MULLERGM", }, { "client with empty reference falls back to title", projectChainRow{Type: "client", Title: "ACME", Reference: str(" ")}, "ACME", }, { "client with empty title and no reference → empty", projectChainRow{Type: "client", Title: ""}, "", }, // Litigation rows. { "litigation with opponent_code", projectChainRow{Type: "litigation", Title: "X v Y", OpponentCode: str("OPNT")}, "OPNT", }, { "litigation without opponent_code → empty", projectChainRow{Type: "litigation", Title: "X v Y", OpponentCode: nil}, "", }, // Patent rows. { "patent EP1234567 → 567", projectChainRow{Type: "patent", PatentNumber: str("EP1234567")}, "567", }, { "patent with spaces EP 3 456 789 → 789", projectChainRow{Type: "patent", PatentNumber: str("EP 3 456 789")}, "789", }, { "patent with kind code EP3456789A1 → 789", projectChainRow{Type: "patent", PatentNumber: str("EP3456789A1")}, "789", }, { "patent WO2020/123456 → 456", projectChainRow{Type: "patent", PatentNumber: str("WO2020/123456")}, "456", }, { "patent shorter than 3 digits → full", projectChainRow{Type: "patent", PatentNumber: str("DE12")}, "12", }, { "patent nil → empty", projectChainRow{Type: "patent", PatentNumber: nil}, "", }, { "patent empty digit-stream → empty", projectChainRow{Type: "patent", PatentNumber: str("EP")}, "", }, // Case rows. { "case upc.inf.cfi → INF.CFI", projectChainRow{Type: "case", ProceedingTypeID: intp(8), ProceedingCode: str("upc.inf.cfi")}, "INF.CFI", }, { "case upc.apl.merits → APL.MERITS", projectChainRow{Type: "case", ProceedingTypeID: intp(11), ProceedingCode: str("upc.apl.merits")}, "APL.MERITS", }, { "case de.inf.lg → INF.LG", projectChainRow{Type: "case", ProceedingTypeID: intp(12), ProceedingCode: str("de.inf.lg")}, "INF.LG", }, { "case without proceeding_code → empty", projectChainRow{Type: "case", ProceedingTypeID: nil, ProceedingCode: nil}, "", }, { "case with single-segment code → empty (no tail)", projectChainRow{Type: "case", ProceedingCode: str("single")}, "", }, // Generic project rows contribute nothing. { "generic project → empty", projectChainRow{Type: "project", Title: "Whatever"}, "", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := projectCodeSegment(c.row) if got != c.want { t.Errorf("projectCodeSegment() = %q, want %q", got, c.want) } }) } } // TestAssembleProjectCode covers the chain assembler, including the // custom-override fast-path on the target row's `reference`. func TestAssembleProjectCode(t *testing.T) { str := func(s string) *string { return &s } intp := func(i int) *int { return &i } // The reference tree from the issue body: EXMPL → OPNT → EP3456789 → upc.inf.cfi. fullChain := []projectChainRow{ {ID: uuid.New(), Type: "client", Title: "Example Co.", Reference: str("EXMPL")}, {ID: uuid.New(), Type: "litigation", Title: "Ex v Op", OpponentCode: str("OPNT")}, {ID: uuid.New(), Type: "patent", PatentNumber: str("EP3456789")}, {ID: uuid.New(), Type: "case", ProceedingTypeID: intp(8), ProceedingCode: str("upc.inf.cfi")}, } cases := []struct { name string chain []projectChainRow want string }{ { "reference tree → EXMPL.OPNT.789.INF.CFI", fullChain, "EXMPL.OPNT.789.INF.CFI", }, { "empty chain → empty", nil, "", }, { "override on target wins outright", append(append([]projectChainRow{}, fullChain[:3]...), projectChainRow{ ID: uuid.New(), Type: "case", Reference: str("CUSTOM-CODE"), ProceedingTypeID: intp(8), ProceedingCode: str("upc.inf.cfi"), }), "CUSTOM-CODE", }, { "override with surrounding whitespace is trimmed", append(append([]projectChainRow{}, fullChain[:3]...), projectChainRow{ ID: uuid.New(), Type: "case", Reference: str(" TRIMMED "), }), "TRIMMED", }, { "override empty string falls through to derivation", append(append([]projectChainRow{}, fullChain[:3]...), projectChainRow{ ID: uuid.New(), Type: "case", Reference: str(""), ProceedingTypeID: intp(8), ProceedingCode: str("upc.inf.cfi"), }), "EXMPL.OPNT.789.INF.CFI", }, { "missing ancestors are skipped silently — case directly under client", []projectChainRow{ {Type: "client", Reference: str("EXMPL")}, {Type: "case", ProceedingCode: str("upc.inf.cfi")}, }, "EXMPL.INF.CFI", }, { "missing patent contributes nothing; client+litigation+case", []projectChainRow{ {Type: "client", Reference: str("EXMPL")}, {Type: "litigation", OpponentCode: str("OPNT")}, {Type: "case", ProceedingCode: str("upc.inf.cfi")}, }, "EXMPL.OPNT.INF.CFI", }, { "target itself is a litigation row (no case below) → up to opponent code", []projectChainRow{ {Type: "client", Reference: str("EXMPL")}, {Type: "litigation", OpponentCode: str("OPNT")}, }, "EXMPL.OPNT", }, { "litigation without opponent_code is skipped silently", []projectChainRow{ {Type: "client", Reference: str("EXMPL")}, {Type: "litigation", OpponentCode: nil}, {Type: "patent", PatentNumber: str("EP3456789")}, {Type: "case", ProceedingCode: str("upc.inf.cfi")}, }, "EXMPL.789.INF.CFI", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := assembleProjectCode(c.chain) if got != c.want { t.Errorf("assembleProjectCode() = %q, want %q", got, c.want) } }) } } // TestPatentLast3 pins the digit-extraction rule across the common // patent-number formats users type. func TestPatentLast3(t *testing.T) { cases := []struct { in, want string }{ {"EP1234567", "567"}, {"EP 1 234 567", "567"}, {"EP3456789A1", "789"}, {"WO2020/123456A1", "456"}, {"DE12", "12"}, {"EP", ""}, {"", ""}, {"NoDigitsAtAll", ""}, {"1", "1"}, {"12", "12"}, {"123", "123"}, {"1234", "234"}, } for _, c := range cases { t.Run(c.in, func(t *testing.T) { if got := patentLast3(c.in); got != c.want { t.Errorf("patentLast3(%q) = %q, want %q", c.in, got, c.want) } }) } } // TestSanitizeClientShort pins the client-short slug rule (uppercase, // strip diacritics, drop non-alnum, cap 8). func TestSanitizeClientShort(t *testing.T) { cases := []struct { in, want string }{ {"EXMPL", "EXMPL"}, {"Example Co.", "EXAMPLEC"}, {"Müller GmbH", "MULLERGM"}, {" ACME ", "ACME"}, {"", ""}, {" ", ""}, {"Hogan Lovells International LLP", "HOGANLOV"}, {"A&B (Patents) Ltd.", "ABPATENT"}, {"Société Générale", "SOCIETEG"}, } for _, c := range cases { t.Run(c.in, func(t *testing.T) { if got := sanitizeClientShort(c.in); got != c.want { t.Errorf("sanitizeClientShort(%q) = %q, want %q", c.in, got, c.want) } }) } } // TestProceedingTail pins the jurisdiction-strip rule. func TestProceedingTail(t *testing.T) { cases := []struct { in, want string }{ {"upc.inf.cfi", "INF.CFI"}, {"upc.rev.cfi", "REV.CFI"}, {"upc.pi.cfi", "PI.CFI"}, {"upc.apl.merits", "APL.MERITS"}, {"de.inf.lg", "INF.LG"}, {"de.inf.olg", "INF.OLG"}, {"single", ""}, {"", ""}, {"a.b", "B"}, {" upc.inf.cfi ", "INF.CFI"}, } for _, c := range cases { t.Run(c.in, func(t *testing.T) { if got := proceedingTail(c.in); got != c.want { t.Errorf("proceedingTail(%q) = %q, want %q", c.in, got, c.want) } }) } } // TestValidateOpponentCode pins the slug-validation rule + the // type='litigation' pairing. Empty string is the explicit clear // sentinel and always passes. func TestValidateOpponentCode(t *testing.T) { cases := []struct { name string code string ptype string wantE bool }{ {"empty clears, any type", "", "case", false}, {"empty clears, litigation", "", "litigation", false}, {"valid slug on litigation", "OPNT", "litigation", false}, {"valid slug with digits on litigation", "OPNT-2026", "litigation", false}, {"valid slug projectType empty (Update path)", "OPNT", "", false}, {"lowercase rejected", "opnt", "litigation", true}, {"underscore rejected", "OPNT_1", "litigation", true}, {"too long rejected", "OPNT-AND-A-VERY-LONG-NAME", "litigation", true}, {"non-litigation type rejected", "OPNT", "case", true}, {"non-litigation type rejected (patent)", "OPNT", "patent", true}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { err := validateOpponentCode(c.code, c.ptype) if (err != nil) != c.wantE { t.Errorf("validateOpponentCode(%q, %q) error = %v, wantErr=%v", c.code, c.ptype, err, c.wantE) } }) } } // TestValidateOurSideSubRoles pins the widened allowlist (mig 112). func TestValidateOurSideSubRoles(t *testing.T) { valid := []string{ "", "claimant", "defendant", "applicant", "appellant", "respondent", "third_party", "other", } invalid := []string{"court", "both", "unknown", "CLAIMANT", "Defendant"} for _, v := range valid { t.Run("valid_"+v, func(t *testing.T) { if err := validateOurSide(v); err != nil { t.Errorf("validateOurSide(%q) unexpected error: %v", v, err) } }) } for _, v := range invalid { t.Run("invalid_"+v, func(t *testing.T) { if err := validateOurSide(v); err == nil { t.Errorf("validateOurSide(%q) expected error, got nil", v) } }) } }