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.
426 lines
13 KiB
Go
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"})
|
|
}
|