Files
paliad/internal/calc/fees_test.go
m 0e1d4869fb fix(t-paliad-130): cap GKG/RVG Streitwert at €30M (§34 GKG / §22(2) RVG)
ComputeBaseFee walked the bracket table indefinitely, so a Streitwert of
e.g. €100M produced fees far above what German law actually permits. §34
GKG / §22(2) RVG cap the table at €30M — above that the fee stays at the
30M-row value.

Surgical fix: clamp streitwert to GermanFeeStreitwertCap (30M) at the top
of ComputeBaseFee. Applies to all GKG/RVG fee versions (2005, 2013, 2021,
2025); UPC value-based fees use a separate code path (lookupUPCValueFee
against UPCFeeSchedule.ValueBased) and stay uncapped — UPC has its own
statutory tier structure with explicit 50M and unlimited brackets.

Tests: cap holds across all four versions for both GKG and RVG; values
below 30M continue to scale as before; UPC remains uncapped.

Spot check (GKG / RVG base, 2025 schedule):
  1M EUR   →   6278.00 / 5553.50
  5M EUR   →  23078.00 / 19553.50
  30M EUR  → 128078.00 / 107053.50
  50M EUR  → 128078.00 / 107053.50  (capped)
  100M EUR → 128078.00 / 107053.50  (capped)
  1B EUR   → 128078.00 / 107053.50  (capped)
2026-05-04 20:58:08 +02:00

334 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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_CapsAt30M(t *testing.T) {
// §34 GKG / §22(2) RVG: above 30M Streitwert the fee is fixed at the
// 30M-row value. Verify cap holds for both GKG and RVG, across all
// known fee versions.
versions := []string{"2025", "2021", "2013", "2005"}
for _, v := range versions {
for _, isRVG := range []bool{false, true} {
fee30M, err := ComputeBaseFee(30_000_000, isRVG, v)
if err != nil {
t.Fatalf("ComputeBaseFee(30M, %v, %s): %v", isRVG, v, err)
}
fee100M, err := ComputeBaseFee(100_000_000, isRVG, v)
if err != nil {
t.Fatalf("ComputeBaseFee(100M, %v, %s): %v", isRVG, v, err)
}
fee1B, err := ComputeBaseFee(1_000_000_000, isRVG, v)
if err != nil {
t.Fatalf("ComputeBaseFee(1B, %v, %s): %v", isRVG, v, err)
}
if fee100M != fee30M {
t.Errorf("version=%s isRVG=%v: fee at 100M (%.2f) must equal fee at 30M (%.2f) — cap not applied", v, isRVG, fee100M, fee30M)
}
if fee1B != fee30M {
t.Errorf("version=%s isRVG=%v: fee at 1B (%.2f) must equal fee at 30M (%.2f) — cap not applied", v, isRVG, fee1B, fee30M)
}
}
}
}
func TestComputeBaseFee_BelowCapUnaffected(t *testing.T) {
// Streitwert < 30M must continue to scale exactly as before.
// Values are chosen comfortably below 30M — note the 500K+ bracket has
// a 50,000-EUR step, so values within the same step ceil identically.
values := []float64{100_000, 1_000_000, 5_000_000, 25_000_000}
for _, sw := range values {
feeBelow, err := ComputeBaseFee(sw, false, "2025")
if err != nil {
t.Fatalf("ComputeBaseFee(%v): %v", sw, err)
}
fee30M, err := ComputeBaseFee(30_000_000, false, "2025")
if err != nil {
t.Fatal(err)
}
if feeBelow >= fee30M {
t.Errorf("fee at %v (%.2f) should be < fee at 30M (%.2f) — cap leaking below threshold", sw, feeBelow, fee30M)
}
}
}
func TestComputeUPCInstance_NotCappedByGermanLimit(t *testing.T) {
// UPC has explicit value-based tiers up to 50M and an unlimited bracket.
// The §34 GKG cap must not bleed into UPC fee computation.
input := InstanceInput{Enabled: true, FeeVersion: "2026"}
r30M, err := ComputeUPCInstance(30_000_000, input, "UPC_FIRST")
if err != nil {
t.Fatal(err)
}
r50M, err := ComputeUPCInstance(50_000_000, input, "UPC_FIRST")
if err != nil {
t.Fatal(err)
}
if r50M.ValueBasedFee <= r30M.ValueBasedFee {
t.Errorf("UPC fee must keep scaling above 30M: 30M=%v 50M=%v", r30M.ValueBasedFee, r50M.ValueBasedFee)
}
}
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 != 2015 {
t.Errorf("EPA Appeal SME Fee = %v, want 2015", 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)
}
}