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)
334 lines
9.2 KiB
Go
334 lines
9.2 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_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)
|
||
}
|
||
}
|