Files
paliad/internal/handlers/gebuehrentabellen.go
m 460736ad1e refactor(t-paliad-092): rename Go module path patholo → paliad
F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed
m/patholo → mAi/paliad → m/paliad, but go.mod still declared
`mgit.msbls.de/m/patholo` and every internal import echoed the
pre-rebrand name.

Sweep:
- go.mod: module path → mgit.msbls.de/m/paliad
- All *.go files: imports rewritten via sed
- README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad
- Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in
  i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx,
  global.css

Verified: go build/vet/test ./... clean, bun run build clean,
no remaining mgit.msbls.de/m/patholo or mAi/paliad references
outside docs that intentionally describe the rename history.
2026-04-30 16:46:31 +02:00

426 lines
13 KiB
Go

package handlers
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"strconv"
"strings"
"time"
"mgit.msbls.de/m/paliad/internal/auth"
"mgit.msbls.de/m/paliad/internal/calc"
)
// Standard Streitwerte for precomputed table rows.
var standardStreitwerte = []float64{
500, 1000, 2000, 3000, 5000,
10000, 15000, 20000, 25000,
30000, 40000, 50000,
75000, 100000, 150000, 200000,
250000, 300000, 400000, 500000,
750000, 1000000, 1500000, 2000000,
3000000, 4000000, 5000000,
7500000, 10000000, 15000000, 20000000,
25000000, 30000000, 50000000,
}
// feeVersionOrder defines the display order for GKG/RVG versions (newest first).
var feeVersionOrder = []string{"2025", "2021", "2013", "2005"}
// --- Response types ---
type FeeTableResponse struct {
GKG []FeeVersionTable `json:"gkg"`
RVG []FeeVersionTable `json:"rvg"`
UPC []UPCScheduleInfo `json:"upc"`
EPA []EPAFeeInfo `json:"epa"`
Multipliers []MultiplierInfo `json:"multipliers"`
PatKostG PatKostGInfo `json:"patkostg"`
}
type FeeVersionTable struct {
Version string `json:"version"`
Label string `json:"label"`
ValidFrom string `json:"validFrom"`
IsCurrent bool `json:"isCurrent"`
Rows []FeeTableRow `json:"rows"`
}
type FeeTableRow struct {
Streitwert float64 `json:"streitwert"`
Fee float64 `json:"fee"`
}
type UPCScheduleInfo struct {
Version string `json:"version"`
Label string `json:"label"`
FixedInfringement float64 `json:"fixedInfringement"`
FixedRevocation float64 `json:"fixedRevocation"`
SMEReduction float64 `json:"smeReduction"`
ValueBased []UPCValueRow `json:"valueBased"`
RecoverableCosts []UPCRecoverRow `json:"recoverableCosts"`
}
type UPCValueRow struct {
MaxValue *float64 `json:"maxValue"`
Fee float64 `json:"fee"`
}
type UPCRecoverRow struct {
MaxValue *float64 `json:"maxValue"`
Ceiling float64 `json:"ceiling"`
}
type EPAFeeInfo struct {
Key string `json:"key"`
Label string `json:"label"`
LabelEN string `json:"labelEN"`
Fee float64 `json:"fee"`
SMEFee float64 `json:"smeFee"`
}
type MultiplierInfo struct {
Key string `json:"key"`
Label string `json:"label"`
LabelEN string `json:"labelEN"`
CourtFeeFactor float64 `json:"courtFeeFactor"`
FeeBasis string `json:"feeBasis"`
RAVGFactor float64 `json:"raVGFactor"`
RATGFactor float64 `json:"raTGFactor"`
PAVGFactor float64 `json:"paVGFactor"`
PATGFactor float64 `json:"paTGFactor"`
}
type PatKostGInfo struct {
CourtFees []PatKostGCourtFee `json:"courtFees"`
DPMAFees []DPMAFee `json:"dpmaFees"`
AnnualFees []DPMAAnnualFee `json:"annualFees"`
}
type PatKostGCourtFee struct {
Key string `json:"key"`
Label string `json:"label"`
LabelEN string `json:"labelEN"`
Factor float64 `json:"factor"`
Note string `json:"note"`
NoteEN string `json:"noteEN"`
}
type DPMAFee struct {
Label string `json:"label"`
LabelEN string `json:"labelEN"`
Fee float64 `json:"fee"`
}
type DPMAAnnualFee struct {
Year int `json:"year"`
Fee float64 `json:"fee"`
}
// --- Lookup response ---
type LookupResponse struct {
Streitwert float64 `json:"streitwert"`
GKG map[string]float64 `json:"gkg"`
RVG map[string]float64 `json:"rvg"`
UPC map[string]UPCLookupResult `json:"upc"`
}
type UPCLookupResult struct {
FixedInfringement float64 `json:"fixedInfringement"`
ValueBasedFee float64 `json:"valueBasedFee"`
Total float64 `json:"total"`
SMETotal float64 `json:"smeTotal"`
RecoverableCeiling float64 `json:"recoverableCeiling"`
}
// --- Feedback ---
type GebuehrenFeedback struct {
FeedbackType string `json:"feedback_type"`
Message string `json:"message"`
Schedule string `json:"schedule"`
}
func handleGebuehrentabellenPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/gebuehrentabellen.html")
}
func handleGebuehrentabellenAPI(w http.ResponseWriter, r *http.Request) {
resp := buildFeeTableResponse()
writeJSON(w, http.StatusOK, resp)
}
func handleGebuehrentabellenLookup(w http.ResponseWriter, r *http.Request) {
sw := r.URL.Query().Get("streitwert")
if sw == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "streitwert parameter required"})
return
}
streitwert, err := strconv.ParseFloat(sw, 64)
if err != nil || streitwert <= 0 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid streitwert"})
return
}
result := LookupResponse{
Streitwert: streitwert,
GKG: make(map[string]float64),
RVG: make(map[string]float64),
UPC: make(map[string]UPCLookupResult),
}
for _, v := range feeVersionOrder {
gkgFee, _ := calc.ComputeBaseFee(streitwert, false, v)
rvgFee, _ := calc.ComputeBaseFee(streitwert, true, v)
result.GKG[v] = math.Round(gkgFee*100) / 100
result.RVG[v] = math.Round(rvgFee*100) / 100
}
for key, schedule := range calc.UPCFeeSchedules {
valueFee := upcValueFee(streitwert, schedule.ValueBased)
total := schedule.FixedInfringement + valueFee
smeTotal := math.Round(total * (1 - schedule.SMEReduction))
recov := upcRecoverableCeiling(streitwert, schedule.RecoverableCosts)
result.UPC[key] = UPCLookupResult{
FixedInfringement: schedule.FixedInfringement,
ValueBasedFee: valueFee,
Total: total,
SMETotal: smeTotal,
RecoverableCeiling: recov,
}
}
writeJSON(w, http.StatusOK, result)
}
func upcValueFee(streitwert float64, brackets []calc.UPCFeeBracket) float64 {
for _, b := range brackets {
if b.MaxValue == nil || streitwert <= *b.MaxValue {
return b.Fee
}
}
return brackets[len(brackets)-1].Fee
}
func upcRecoverableCeiling(streitwert float64, table []calc.UPCRecoverableCost) float64 {
for _, e := range table {
if e.MaxValue == nil || streitwert <= *e.MaxValue {
return e.Ceiling
}
}
return table[len(table)-1].Ceiling
}
func buildFeeTableResponse() FeeTableResponse {
var resp FeeTableResponse
// GKG tables
for _, v := range feeVersionOrder {
schedule := calc.FeeSchedules[v]
table := FeeVersionTable{
Version: v,
Label: schedule.Label,
ValidFrom: schedule.ValidFrom,
IsCurrent: v == "2025",
}
for _, sw := range standardStreitwerte {
fee, _ := calc.ComputeBaseFee(sw, false, v)
table.Rows = append(table.Rows, FeeTableRow{
Streitwert: sw,
Fee: math.Round(fee*100) / 100,
})
}
resp.GKG = append(resp.GKG, table)
}
// RVG tables
for _, v := range feeVersionOrder {
schedule := calc.FeeSchedules[v]
table := FeeVersionTable{
Version: v,
Label: schedule.Label,
ValidFrom: schedule.ValidFrom,
IsCurrent: v == "2025",
}
for _, sw := range standardStreitwerte {
fee, _ := calc.ComputeBaseFee(sw, true, v)
table.Rows = append(table.Rows, FeeTableRow{
Streitwert: sw,
Fee: math.Round(fee*100) / 100,
})
}
resp.RVG = append(resp.RVG, table)
}
// UPC schedules (2026 first, then pre2026)
for _, key := range []string{"2026", "pre2026"} {
schedule := calc.UPCFeeSchedules[key]
info := UPCScheduleInfo{
Version: key,
Label: schedule.Label,
FixedInfringement: schedule.FixedInfringement,
FixedRevocation: schedule.FixedRevocation,
SMEReduction: schedule.SMEReduction,
}
for _, b := range schedule.ValueBased {
info.ValueBased = append(info.ValueBased, UPCValueRow{
MaxValue: b.MaxValue,
Fee: b.Fee,
})
}
for _, r := range schedule.RecoverableCosts {
info.RecoverableCosts = append(info.RecoverableCosts, UPCRecoverRow{
MaxValue: r.MaxValue,
Ceiling: r.Ceiling,
})
}
resp.UPC = append(resp.UPC, info)
}
// EPA fees
for _, key := range []string{"EPA_OPPOSITION", "EPA_APPEAL"} {
epa := calc.EPAFees[key]
resp.EPA = append(resp.EPA, EPAFeeInfo{
Key: epa.Key,
Label: epa.Label,
LabelEN: epa.LabelEN,
Fee: epa.Fee,
SMEFee: epa.SMEFee,
})
}
// Additional EPA fees not in calc (grant-related)
resp.EPA = append(resp.EPA,
EPAFeeInfo{Key: "EPA_GRANT", Label: "Erteilungs- und Druckgebühr", LabelEN: "Grant and printing fee", Fee: 1040, SMEFee: 1040},
EPAFeeInfo{Key: "EPA_EXAMINATION", Label: "Prüfungsgebühr", LabelEN: "Examination fee", Fee: 1970, SMEFee: 1970},
EPAFeeInfo{Key: "EPA_SEARCH", Label: "Recherchengebühr", LabelEN: "Search fee", Fee: 1520, SMEFee: 1520},
EPAFeeInfo{Key: "EPA_FILING", Label: "Anmeldegebühr", LabelEN: "Filing fee", Fee: 140, SMEFee: 140},
EPAFeeInfo{Key: "EPA_DESIGNATION", Label: "Benennungsgebühr (pauschal)", LabelEN: "Designation fee (flat)", Fee: 660, SMEFee: 660},
)
// Multipliers
for _, inst := range calc.AllDEInstances() {
resp.Multipliers = append(resp.Multipliers, MultiplierInfo{
Key: inst.Key,
Label: inst.Label,
LabelEN: inst.LabelEN,
CourtFeeFactor: inst.CourtFeeFactor,
FeeBasis: inst.FeeBasis,
RAVGFactor: inst.RAVGFactor,
RATGFactor: inst.RATGFactor,
PAVGFactor: inst.PAVGFactor,
PATGFactor: inst.PATGFactor,
})
}
// PatKostG
resp.PatKostG = PatKostGInfo{
CourtFees: []PatKostGCourtFee{
{
Key: "BPatG", Label: "BPatG (Nichtigkeitsverfahren)", LabelEN: "Federal Patent Court (Nullity)",
Factor: 4.5, Note: "4,5-fache GKG-Gebühr", NoteEN: "4.5x GKG fee",
},
{
Key: "BGH_NULLITY", Label: "BGH (Nichtigkeitsberufung)", LabelEN: "Federal Court of Justice (Nullity Appeal)",
Factor: 6.0, Note: "6,0-fache GKG-Gebühr", NoteEN: "6.0x GKG fee",
},
},
DPMAFees: []DPMAFee{
{Label: "Patentanmeldung (elektronisch)", LabelEN: "Patent application (electronic)", Fee: 40},
{Label: "Patentanmeldung (Papier)", LabelEN: "Patent application (paper)", Fee: 60},
{Label: "Prüfungsantrag", LabelEN: "Examination request", Fee: 350},
{Label: "Erteilungsgebühr (bis 10 Ansprüche)", LabelEN: "Grant fee (up to 10 claims)", Fee: 125},
{Label: "je weiterer Anspruch ab Nr. 11", LabelEN: "per additional claim from 11th", Fee: 30},
{Label: "Gebrauchsmusteranmeldung (elektronisch)", LabelEN: "Utility model application (electronic)", Fee: 30},
{Label: "Gebrauchsmusteranmeldung (Papier)", LabelEN: "Utility model application (paper)", Fee: 40},
},
AnnualFees: []DPMAAnnualFee{
{Year: 3, Fee: 70}, {Year: 4, Fee: 70}, {Year: 5, Fee: 90},
{Year: 6, Fee: 130}, {Year: 7, Fee: 180}, {Year: 8, Fee: 240},
{Year: 9, Fee: 290}, {Year: 10, Fee: 350}, {Year: 11, Fee: 470},
{Year: 12, Fee: 620}, {Year: 13, Fee: 760}, {Year: 14, Fee: 910},
{Year: 15, Fee: 1060}, {Year: 16, Fee: 1230}, {Year: 17, Fee: 1410},
{Year: 18, Fee: 1590}, {Year: 19, Fee: 1790}, {Year: 20, Fee: 2030},
},
}
return resp
}
func handleGebuehrentabellenFeedback(w http.ResponseWriter, r *http.Request) {
var feedback GebuehrenFeedback
if err := json.NewDecoder(r.Body).Decode(&feedback); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage."})
return
}
feedback.FeedbackType = strings.TrimSpace(feedback.FeedbackType)
feedback.Message = strings.TrimSpace(feedback.Message)
feedback.Schedule = strings.TrimSpace(feedback.Schedule)
if feedback.Message == "" || feedback.FeedbackType == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Nachricht und Art sind erforderlich."})
return
}
accessToken := ""
email := ""
if cookie, err := r.Cookie(auth.SessionCookieName); err == nil {
accessToken = cookie.Value
email = extractEmailFromJWT(cookie.Value)
}
payload := map[string]string{
"feedback_type": feedback.FeedbackType,
"message": feedback.Message,
"schedule": feedback.Schedule,
"submitted_by": email,
}
jsonBody, err := json.Marshal(payload)
if err != nil {
log.Printf("gebuehren feedback marshal error: %v", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Interner Fehler."})
return
}
endpoint := fmt.Sprintf("%s/rest/v1/gebuehrentabellen_feedback", authClient.URL)
req2, err := http.NewRequest("POST", endpoint, bytes.NewReader(jsonBody))
if err != nil {
log.Printf("gebuehren feedback request error: %v", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Interner Fehler."})
return
}
req2.Header.Set("Content-Type", "application/json")
req2.Header.Set("apikey", authClient.AnonKey)
if accessToken != "" {
req2.Header.Set("Authorization", "Bearer "+accessToken)
} else {
req2.Header.Set("Authorization", "Bearer "+authClient.AnonKey)
}
req2.Header.Set("Prefer", "return=minimal")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req2)
if err != nil {
log.Printf("gebuehren feedback supabase error: %v", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Fehler beim Speichern."})
return
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
log.Printf("gebuehren feedback supabase status %d: %s", resp.StatusCode, string(body))
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Fehler beim Speichern."})
return
}
writeJSON(w, http.StatusCreated, map[string]string{"ok": "true"})
}