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).
225 lines
7.5 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|