Go calculation engine (internal/calc/): - GKG/RVG step-based fee computation with 4 schedule versions (2005-2025) - DE court instances: LG, OLG, BGH NZB/Rev, BPatG, BGH Nullity - UPC fees: fixed + value-based with SME reduction (pre-2026 and 2026) - EPA proceedings: Opposition and Appeal fixed fees - Attorney + patent attorney fee breakdown with Erhöhung, MwSt - 11 unit tests covering all calculation paths Frontend (Bun/TSX): - SSR page shell with two-column layout (inputs + sticky results) - Streitwert slider + presets, VAT selector, instance cards with details - Client JS: form state management, API calls, result rendering - Print-friendly layout API: POST /api/tools/kostenrechner (protected, JSON) Route: GET /tools/kostenrechner (protected page) Navigation: Added tool links to header
264 lines
6.5 KiB
Go
264 lines
6.5 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)
|
||
}
|
||
// Total = SME court fee + recoverable
|
||
expectedTotal := 9000.0 + 112000.0
|
||
if math.Abs(result.InstanceTotal-expectedTotal) > 0.01 {
|
||
t.Errorf("InstanceTotal = %v, want %v", result.InstanceTotal, expectedTotal)
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|