Files
paliad/internal/services/submission_autoname_test.go
mAi f292338919 feat(submissions): auto-name new drafts <date> <client>./.<forum>./.<opponent> (m/paliad#155)
New project-bound submission drafts now default to a sortable, legal-
convention title instead of the bare "Entwurf N" counter:

    <YYYY-MM-DD> <ClientName> ./. <ForumShort> ./. <OpponentName>

- Date leads (ISO, Europe/Berlin) so drafts list chronologically; " ./. "
  is the German legal "gegen" separator.
- Client = root 'client' ancestor of the project tree.
- Forum = proceeding-type jurisdiction (UPC/EPA/DPMA); German proceedings
  resolve to the deciding court (LG/OLG/BGH/BPatG) from the code tail.
- Opponent = primary opposing party, picked by our_side posture
  (active → defendant bucket, reactive → claimant bucket).
- Any segment that resolves empty is omitted with its leading separator;
  a project-less draft keeps the legacy "Entwurf N" scheme entirely.
- Create-time only: existing drafts are never renamed, and a lawyer's
  later manual rename via Update is untouched. Same-slot collisions
  de-duplicate with a " (N)" suffix.

Customization scope (per-user / firm / template, issue #155 Q4) is v1.1 —
the template is hardcoded in submission_autoname.go for now; the override
string is documented as the single extension point on AutoSubmissionTitle.

Example output:
  full:        2026-05-31 Bayer AG ./. UPC ./. Novartis Pharma
  no opponent: 2026-05-31 Bayer AG ./. BPatG
  no forum:    2026-05-31 Bayer AG ./. Novartis Pharma
  date only:   2026-05-31

AutoSubmissionTitle + segment resolvers are pure and table-tested
(submission_autoname_test.go); the Create flow is covered end-to-end
against real Postgres in submission_draft_autoname_live_test.go (gated
on TEST_DATABASE_URL).
2026-05-31 15:28:54 +02:00

225 lines
7.5 KiB
Go

package services
import (
"testing"
"time"
"mgit.msbls.de/m/paliad/internal/models"
)
func party(name, role string) models.Party {
return models.Party{Name: name, Role: strPtr(role)}
}
func proceeding(jurisdiction, code string) *models.ProceedingType {
return &models.ProceedingType{Jurisdiction: strPtr(jurisdiction), Code: code}
}
func projectSide(side string) *models.Project {
if side == "" {
return &models.Project{}
}
return &models.Project{OurSide: strPtr(side)}
}
// noon UTC on 2026-05-31 → 14:00 Europe/Berlin (CEST), same calendar day.
var fixedNow = time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC)
func TestAutoSubmissionTitle(t *testing.T) {
cases := []struct {
name string
clientName string
project *models.Project
parties []models.Party
pt *models.ProceedingType
want string
}{
{
name: "full data — UPC, we are claimant",
clientName: "Bayer AG",
project: projectSide("claimant"),
parties: []models.Party{party("Novartis Pharma", "Beklagte")},
pt: proceeding("UPC", "upc.inf.cfi"),
want: "2026-05-31 Bayer AG ./. UPC ./. Novartis Pharma",
},
{
name: "full data — German court, we are respondent",
clientName: "Bayer AG",
project: projectSide("respondent"),
parties: []models.Party{party("Acme Generics", "Klägerin")},
pt: proceeding("DE", "de.null.bpatg"),
want: "2026-05-31 Bayer AG ./. BPatG ./. Acme Generics",
},
{
name: "no opponent — opposing bucket empty",
clientName: "Bayer AG",
project: projectSide("claimant"),
parties: []models.Party{party("Bayer AG", "Klägerin")}, // only our own side
pt: proceeding("UPC", "upc.inf.cfi"),
want: "2026-05-31 Bayer AG ./. UPC",
},
{
name: "no forum — proceeding type missing",
clientName: "Bayer AG",
project: projectSide("respondent"),
parties: []models.Party{party("Acme Generics", "Klägerin")},
pt: nil,
want: "2026-05-31 Bayer AG ./. Acme Generics",
},
{
name: "no client — client segment omitted",
clientName: "",
project: projectSide("claimant"),
parties: []models.Party{party("Novartis Pharma", "Beklagte")},
pt: proceeding("UPC", "upc.inf.cfi"),
want: "2026-05-31 UPC ./. Novartis Pharma",
},
{
name: "all identity segments missing — date only",
clientName: "",
project: projectSide(""), // no our_side → no opponent posture
parties: nil,
pt: nil,
want: "2026-05-31",
},
{
name: "unknown side — opponent omitted even with parties",
clientName: "Bayer AG",
project: projectSide("third_party"),
parties: []models.Party{party("Acme Generics", "Klägerin")},
pt: proceeding("EPA", "epa.opp.opd"),
want: "2026-05-31 Bayer AG ./. EPA",
},
{
name: "nil project — opponent omitted, client + forum stand",
clientName: "Bayer AG",
project: nil,
parties: []models.Party{party("Acme Generics", "Klägerin")},
pt: proceeding("DPMA", "dpma.opp.dpma"),
want: "2026-05-31 Bayer AG ./. DPMA",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := AutoSubmissionTitle(fixedNow, c.clientName, c.project, c.parties, c.pt)
if got != c.want {
t.Errorf("AutoSubmissionTitle = %q, want %q", got, c.want)
}
})
}
}
// TestAutoSubmissionTitleBerlinDate locks the Europe/Berlin localisation:
// 22:30 UTC on 2026-05-31 is already 00:30 on 2026-06-01 in CEST, so the
// date segment must roll over.
func TestAutoSubmissionTitleBerlinDate(t *testing.T) {
lateUTC := time.Date(2026, 5, 31, 22, 30, 0, 0, time.UTC)
got := AutoSubmissionTitle(lateUTC, "Bayer AG", projectSide("claimant"),
[]models.Party{party("Novartis", "Beklagte")}, proceeding("UPC", "upc.inf.cfi"))
want := "2026-06-01 Bayer AG ./. UPC ./. Novartis"
if got != want {
t.Errorf("AutoSubmissionTitle (late UTC) = %q, want %q", got, want)
}
}
func TestSubmissionForumShort(t *testing.T) {
cases := []struct {
pt *models.ProceedingType
want string
}{
{nil, ""},
{proceeding("UPC", "upc.inf.cfi"), "UPC"},
{proceeding("EPA", "epa.opp.opd"), "EPA"},
{proceeding("DPMA", "dpma.opp.dpma"), "DPMA"},
{proceeding("DE", "de.inf.lg"), "LG"},
{proceeding("DE", "de.inf.olg"), "OLG"},
{proceeding("DE", "de.inf.bgh"), "BGH"},
{proceeding("DE", "de.null.bpatg"), "BPatG"},
{proceeding("DE", "de.null.bgh"), "BGH"},
{proceeding("DE", "de.foo.amtsgericht"), "AMTSGERICHT"}, // unknown court → uppercased tail
{proceeding("de", "de.inf.lg"), "LG"}, // lowercase jurisdiction folds
{proceeding("", ""), ""}, // no jurisdiction
}
for _, c := range cases {
if got := submissionForumShort(c.pt); got != c.want {
t.Errorf("submissionForumShort(%+v) = %q, want %q", c.pt, got, c.want)
}
}
}
func TestSubmissionOpponentName(t *testing.T) {
claimantA := party("Acme", "Klägerin")
defendantB := party("Novartis", "Beklagte")
other := party("Streithelfer X", "Streithelfer")
cases := []struct {
name string
parties []models.Party
ourSide string
want string
}{
{"active → first defendant", []models.Party{claimantA, defendantB}, "claimant", "Novartis"},
{"reactive → first claimant", []models.Party{claimantA, defendantB}, "respondent", "Acme"},
{"applicant (active) → defendant", []models.Party{defendantB}, "applicant", "Novartis"},
{"appellant (active) → defendant", []models.Party{defendantB}, "appellant", "Novartis"},
{"defendant (reactive) → claimant", []models.Party{claimantA}, "defendant", "Acme"},
{"unknown side → none", []models.Party{claimantA, defendantB}, "third_party", ""},
{"empty side → none", []models.Party{claimantA, defendantB}, "", ""},
{"no opposing party → none", []models.Party{claimantA, other}, "claimant", ""},
{"opposing bucket only 'other' → none", []models.Party{other}, "respondent", ""},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := submissionOpponentName(c.parties, c.ourSide); got != c.want {
t.Errorf("submissionOpponentName = %q, want %q", got, c.want)
}
})
}
}
func TestUniqueDraftName(t *testing.T) {
cases := []struct {
name string
base string
existing []string
want string
}{
{"free", "2026-05-31 Bayer AG ./. UPC", nil, "2026-05-31 Bayer AG ./. UPC"},
{"first clash → (2)", "2026-05-31 Bayer AG ./. UPC",
[]string{"2026-05-31 Bayer AG ./. UPC"}, "2026-05-31 Bayer AG ./. UPC (2)"},
{"two clash → (3)", "2026-05-31 Bayer AG ./. UPC",
[]string{"2026-05-31 Bayer AG ./. UPC", "2026-05-31 Bayer AG ./. UPC (2)"},
"2026-05-31 Bayer AG ./. UPC (3)"},
{"gap reused → (2)", "X",
[]string{"X", "X (3)"}, "X (2)"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := uniqueDraftName(c.base, c.existing); got != c.want {
t.Errorf("uniqueDraftName = %q, want %q", got, c.want)
}
})
}
}
func TestNextDraftName(t *testing.T) {
cases := []struct {
name string
existing []string
lang string
want string
}{
{"empty de", nil, "de", "Entwurf 1"},
{"empty en", nil, "en", "Draft 1"},
{"highest+1", []string{"Entwurf 1", "Entwurf 3"}, "de", "Entwurf 4"},
{"ignores foreign names", []string{"2026-05-31 Bayer AG"}, "de", "Entwurf 1"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := nextDraftName(c.existing, c.lang); got != c.want {
t.Errorf("nextDraftName = %q, want %q", got, c.want)
}
})
}
}