Three correctness bugs from the t-paliad-101 QA sweep, fixed together since
they all change displayed/saved numbers users rely on.
B1 — Kostenrechner UPC GESAMTKOSTEN double-count
ComputeUPCInstance was setting InstanceTotal = effectiveCourtFee +
recoverableCeiling. The R.152 recoverable-cost cap is the OPPOSING
side's worst-case loss-of-suit liability, not the user's own cost —
folding it into GESAMTKOSTEN inflated the UPC total under a label
that means "your outlay," and the DE LG/OLG/BGH branches don't add
any opponent estimate. Drop it from InstanceTotal; the ceiling
still surfaces as its own RecoverableCeiling line item.
Live pre-fix on paliad.de (Streitwert 100k, UPC 1. Instanz only):
instanceTotal = 52600 = 14600 court fee + 38000 R.152 ceiling
Post-fix:
instanceTotal = 14600 (court fee only); RecoverableCeiling stays 38000
B3 — Court-determined Termine emit trigger date as a real-looking date
Zwischenverfahren / Mündliche Verhandlung / Entscheidung all live in
paliad.deadline_rules with duration_value=0 and parent_id=NULL, so
Calculate() classified them as IsRootEvent and emitted the trigger
date as their own DueDate. Worse, RoP.151 "Antrag auf Kostenentscheidung"
parents off inf.decision and chained 1 month off the placeholder ->
bogus deadline that the UI rendered as real.
Fix: classify a zero-duration rule as IsCourtSet (not IsRootEvent)
when primary_party = 'court' or event_type ∈ {hearing, decision,
order}. Track court-set rule IDs and propagate IsCourtSet downstream
to any rule whose parent is court-set, so RoP.151 also surfaces as
court-set rather than a fabricated date. Save-modal already greys
out IsCourtSet rows so the "Gerichtsbestimmte Termine ohne Datum
werden übersprungen" footnote becomes truthful again.
Live pre-fix on paliad.de (UPC_INF, trigger 2026-04-29):
Zwischenverfahren / Oral / Entscheidung -> dueDate 2026-04-29
Antrag auf Kostenentscheidung -> 2026-05-29 (bogus, +1mo from trigger)
B6 — Fristenrechner save flow stored rule code in TITLE
Frontend was concatenating "RoP.023 — Klageerwiderung" into the
title because deadlines had nowhere else to put the citation, and
the /deadlines REGEL column ended up showing "—". Add migration 032
with a paliad.deadlines.rule_code text column, plumb it through
CreateDeadlineInput / insertTx, drop the now-redundant r.code AS
rule_code JOIN alias on the list query (the deadline owns its
citation), and render f.rule_code on the project-detail deadlines
table + /deadlines events list + deadline-detail page.
Build, vet, and tests all clean. New unit test
TestIsCourtDeterminedRule pins the B3 discriminator across the
event_type / primary_party combinations seen in migrations 012 + 031.
Repro creds: tester@hlc.de
268 lines
6.8 KiB
Go
268 lines
6.8 KiB
Go
package calc
|
||
|
||
import (
|
||
"math"
|
||
"testing"
|
||
)
|
||
|
||
func TestComputeBaseFee_GKG_2025(t *testing.T) {
|
||
tests := []struct {
|
||
streitwert float64
|
||
isRVG bool
|
||
want float64
|
||
}{
|
||
// Minimum fee: first bracket, one increment
|
||
{100, false, 40},
|
||
{300, false, 40},
|
||
// First bracket boundary: includes additional step for 300-500 range
|
||
{500, false, 80},
|
||
{501, false, 101}, // enters second bracket: 80 + 21
|
||
{1000, false, 101}, // still one step in second bracket
|
||
{2000, false, 143}, // 80 + 3*21 = 143
|
||
// RVG base for 1M EUR
|
||
{1000000, true, 5553.5},
|
||
// GKG base for 1M EUR
|
||
{1000000, false, 6278},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
got, err := ComputeBaseFee(tt.streitwert, tt.isRVG, "2025")
|
||
if err != nil {
|
||
t.Fatalf("ComputeBaseFee(%v, %v, 2025): %v", tt.streitwert, tt.isRVG, err)
|
||
}
|
||
if math.Abs(got-tt.want) > 0.01 {
|
||
t.Errorf("ComputeBaseFee(%v, isRVG=%v, 2025) = %v, want %v", tt.streitwert, tt.isRVG, got, tt.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestComputeBaseFee_Aktuell_Alias(t *testing.T) {
|
||
v2025, err := ComputeBaseFee(1000000, false, "2025")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
vAktuell, err := ComputeBaseFee(1000000, false, "Aktuell")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if v2025 != vAktuell {
|
||
t.Errorf("Aktuell alias: got %v, want %v (same as 2025)", vAktuell, v2025)
|
||
}
|
||
}
|
||
|
||
func TestComputeBaseFee_UnknownVersion(t *testing.T) {
|
||
_, err := ComputeBaseFee(1000, false, "1999")
|
||
if err == nil {
|
||
t.Error("expected error for unknown version, got nil")
|
||
}
|
||
}
|
||
|
||
func TestComputeDEInstance_LG(t *testing.T) {
|
||
input := InstanceInput{
|
||
Enabled: true,
|
||
FeeVersion: "2025",
|
||
NumAttorneys: 1,
|
||
NumPatentAttorneys: 1,
|
||
NumClients: 1,
|
||
OralHearing: true,
|
||
}
|
||
meta := DEInfringementInstances[0] // LG
|
||
result, err := ComputeDEInstance(1000000, input, meta, 0.19)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
if !result.Enabled {
|
||
t.Fatal("expected enabled=true")
|
||
}
|
||
if result.CourtFeeBasis != "GKG" {
|
||
t.Errorf("expected GKG, got %s", result.CourtFeeBasis)
|
||
}
|
||
// Court fee: 3.0 × 6278 = 18834
|
||
if math.Abs(result.CourtFee-18834) > 0.01 {
|
||
t.Errorf("CourtFee = %v, want 18834", result.CourtFee)
|
||
}
|
||
if result.Attorney == nil {
|
||
t.Fatal("expected attorney breakdown")
|
||
}
|
||
if result.PatentAttorney == nil {
|
||
t.Fatal("expected patent attorney breakdown")
|
||
}
|
||
if result.InstanceTotal <= 0 {
|
||
t.Error("expected positive instance total")
|
||
}
|
||
}
|
||
|
||
func TestComputeDEInstance_Disabled(t *testing.T) {
|
||
input := InstanceInput{Enabled: false}
|
||
meta := DEInfringementInstances[0]
|
||
result, err := ComputeDEInstance(1000000, input, meta, 0.19)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if result.Enabled {
|
||
t.Error("expected enabled=false")
|
||
}
|
||
if result.InstanceTotal != 0 {
|
||
t.Errorf("expected 0 total for disabled, got %v", result.InstanceTotal)
|
||
}
|
||
}
|
||
|
||
func TestComputeUPCInstance_Pre2026(t *testing.T) {
|
||
input := InstanceInput{
|
||
Enabled: true,
|
||
FeeVersion: "pre2026",
|
||
IsSME: false,
|
||
}
|
||
result, err := ComputeUPCInstance(1000000, input, "UPC_FIRST")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// Fixed: 11000, value-based for 1M: 4000
|
||
if result.FixedFee != 11000 {
|
||
t.Errorf("FixedFee = %v, want 11000", result.FixedFee)
|
||
}
|
||
if result.ValueBasedFee != 4000 {
|
||
t.Errorf("ValueBasedFee = %v, want 4000", result.ValueBasedFee)
|
||
}
|
||
if result.CourtFeesTotal != 15000 {
|
||
t.Errorf("CourtFeesTotal = %v, want 15000", result.CourtFeesTotal)
|
||
}
|
||
// Recoverable for 1M: 112000
|
||
if result.RecoverableCeiling != 112000 {
|
||
t.Errorf("RecoverableCeiling = %v, want 112000", result.RecoverableCeiling)
|
||
}
|
||
}
|
||
|
||
func TestComputeUPCInstance_SME(t *testing.T) {
|
||
input := InstanceInput{
|
||
Enabled: true,
|
||
FeeVersion: "pre2026",
|
||
IsSME: true,
|
||
}
|
||
result, err := ComputeUPCInstance(1000000, input, "UPC_FIRST")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// SME: 15000 × (1 - 0.4) = 9000
|
||
if result.CourtFeesSME != 9000 {
|
||
t.Errorf("CourtFeesSME = %v, want 9000", result.CourtFeesSME)
|
||
}
|
||
// InstanceTotal is the user's own outlay — court fee only, never the
|
||
// opposing side's R.152 recoverable cap (which stays on RecoverableCeiling).
|
||
expectedTotal := 9000.0
|
||
if math.Abs(result.InstanceTotal-expectedTotal) > 0.01 {
|
||
t.Errorf("InstanceTotal = %v, want %v", result.InstanceTotal, expectedTotal)
|
||
}
|
||
if result.RecoverableCeiling != 112000 {
|
||
t.Errorf("RecoverableCeiling = %v, want 112000 (separate line item)", result.RecoverableCeiling)
|
||
}
|
||
}
|
||
|
||
func TestComputeEPAInstance(t *testing.T) {
|
||
input := InstanceInput{Enabled: true}
|
||
result, err := ComputeEPAInstance(input, "EPA_OPPOSITION")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if result.Fee != 880 {
|
||
t.Errorf("EPA Opposition Fee = %v, want 880", result.Fee)
|
||
}
|
||
}
|
||
|
||
func TestComputeEPAInstance_SME(t *testing.T) {
|
||
input := InstanceInput{Enabled: true, IsSME: true}
|
||
result, err := ComputeEPAInstance(input, "EPA_APPEAL")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if result.Fee != 1880 {
|
||
t.Errorf("EPA Appeal SME Fee = %v, want 1880", result.Fee)
|
||
}
|
||
}
|
||
|
||
func TestCalculate_FullRequest(t *testing.T) {
|
||
req := CostRequest{
|
||
Streitwert: 1000000,
|
||
VATRate: 0.19,
|
||
Instances: map[string]InstanceInput{
|
||
"LG": {
|
||
Enabled: true,
|
||
FeeVersion: "Aktuell",
|
||
NumAttorneys: 1,
|
||
NumPatentAttorneys: 1,
|
||
NumClients: 1,
|
||
OralHearing: true,
|
||
},
|
||
"UPC_FIRST": {
|
||
Enabled: true,
|
||
FeeVersion: "2026",
|
||
},
|
||
"EPA_OPPOSITION": {
|
||
Enabled: true,
|
||
},
|
||
},
|
||
}
|
||
|
||
resp, err := Calculate(req)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// Should have 6 DE results, 2 UPC results, 2 EPA results
|
||
if len(resp.DEResults) != 6 {
|
||
t.Errorf("expected 6 DE results, got %d", len(resp.DEResults))
|
||
}
|
||
if len(resp.UPCResults) != 2 {
|
||
t.Errorf("expected 2 UPC results, got %d", len(resp.UPCResults))
|
||
}
|
||
if len(resp.EPAResults) != 2 {
|
||
t.Errorf("expected 2 EPA results, got %d", len(resp.EPAResults))
|
||
}
|
||
|
||
// LG should be enabled
|
||
if !resp.DEResults[0].Enabled {
|
||
t.Error("LG should be enabled")
|
||
}
|
||
// OLG should be disabled
|
||
if resp.DEResults[1].Enabled {
|
||
t.Error("OLG should be disabled")
|
||
}
|
||
|
||
// Grand total should be positive
|
||
if resp.Totals.GrandTotal <= 0 {
|
||
t.Error("expected positive grand total")
|
||
}
|
||
|
||
// EPA fees should include opposition
|
||
if resp.Totals.EPAFees != 880 {
|
||
t.Errorf("EPA fees = %v, want 880", resp.Totals.EPAFees)
|
||
}
|
||
}
|
||
|
||
func TestAttorneyFees_MultipleClients(t *testing.T) {
|
||
input := InstanceInput{
|
||
Enabled: true,
|
||
FeeVersion: "2025",
|
||
NumAttorneys: 1,
|
||
NumClients: 3,
|
||
OralHearing: true,
|
||
}
|
||
meta := DEInfringementInstances[0] // LG
|
||
|
||
result, err := ComputeDEInstance(1000000, input, meta, 0.19)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// Erhöhung: min((3-1)*0.3, 2.0) × baseFee = 0.6 × 5553.5 = 3332.1
|
||
if result.Attorney == nil {
|
||
t.Fatal("expected attorney breakdown")
|
||
}
|
||
if math.Abs(result.Attorney.Erhoehungsgebuehr-3332.1) > 0.01 {
|
||
t.Errorf("Erhoehungsgebuehr = %v, want 3332.1", result.Attorney.Erhoehungsgebuehr)
|
||
}
|
||
}
|