Files
paliad/internal/calc/fees_test.go
m bd621664cf feat: implement Prozesskostenrechner (patent litigation cost calculator)
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
2026-04-14 17:25:38 +02:00

264 lines
6.5 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_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)
}
}