186 lines
4.7 KiB
Go
186 lines
4.7 KiB
Go
package payroll
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type ZuschlagData struct {
|
|
Nacht25 float64
|
|
Nacht40 float64
|
|
Sonntag50 float64
|
|
Samstag15 float64
|
|
Feiertag125 float64
|
|
Feiertag150 float64
|
|
Verpflegung14 float64
|
|
}
|
|
|
|
type ZuschlagParser struct {
|
|
logger *slog.Logger
|
|
knownIdentifiers map[string]string
|
|
knownColumns map[string]string
|
|
}
|
|
|
|
func NewZuschlagParser(logger *slog.Logger) *ZuschlagParser {
|
|
return &ZuschlagParser{
|
|
logger: logger,
|
|
knownIdentifiers: map[string]string{
|
|
"Nachname": "Lastname",
|
|
"Vorname": "Firstname",
|
|
},
|
|
knownColumns: map[string]string{
|
|
"25%": "Nacht25",
|
|
"40%": "Nacht40",
|
|
"50%": "Sonntag50",
|
|
"14%": "Verpflegung14",
|
|
"15%": "Samstag15",
|
|
"125%": "Feiertag125",
|
|
"150%": "Feiertag150",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (p *ZuschlagParser) ParseFile(filepath string) (map[string]ZuschlagData, error) {
|
|
file, err := os.Open(filepath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("zuschlag parser: failed to open file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
reader := csv.NewReader(file)
|
|
reader.Comma = ','
|
|
reader.LazyQuotes = true
|
|
|
|
if _, err := reader.Read(); err != nil {
|
|
return nil, fmt.Errorf("zuschlag parser: an error occured while reading header 1: %w", err)
|
|
}
|
|
headerRow, err := reader.Read()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("zuschlag parser: fehler beim lesen von header 2 (zuschlag row): %w", err)
|
|
}
|
|
identifierHeaderRow, err := reader.Read()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("zuschlag parser: fehler beim lesen von header 3 (identifier row): %w", err)
|
|
}
|
|
|
|
identifierIndex := p.buildIndex(identifierHeaderRow, p.knownIdentifiers)
|
|
zuschlagIndex := p.buildIndex(headerRow, p.knownColumns)
|
|
runtimeIndex := make(map[string]int)
|
|
for k, v := range identifierIndex {
|
|
runtimeIndex[k] = v
|
|
}
|
|
for k, v := range zuschlagIndex {
|
|
runtimeIndex[k] = v
|
|
}
|
|
|
|
results := make(map[string]ZuschlagData)
|
|
|
|
for {
|
|
row, err := reader.Read()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
p.logger.Warn("zuschlag parser: zeile übersprungen (lesefehler)", "error", err)
|
|
continue
|
|
}
|
|
|
|
compoundKey, data, err := p.extractRowData(row, runtimeIndex)
|
|
if err != nil {
|
|
p.logger.Warn("zuschlag parser: zeile übersprungen (extraktionsfehler)", "error", err)
|
|
continue
|
|
}
|
|
results[compoundKey] = data
|
|
}
|
|
|
|
p.logger.Info("Zuschlag-Datei erfolgreich geparst", "records", len(results))
|
|
return results, nil
|
|
}
|
|
|
|
// buildAndValidateIndex erstellt die Index-Map und validiert Pflichtfelder.
|
|
// (map[internerName]csvSpaltenIndex)
|
|
func (p *ZuschlagParser) buildIndex(headerRow []string, knownMap map[string]string) map[string]int {
|
|
zuschlagIndex := make(map[string]int)
|
|
|
|
for i, colNameFromFile := range headerRow {
|
|
trimmedName := strings.TrimSpace(colNameFromFile)
|
|
|
|
if internalName, ok := knownMap[trimmedName]; ok {
|
|
zuschlagIndex[internalName] = i
|
|
p.logger.Debug("zuschlag parser: index gemappt", "header_name", trimmedName, "internal_name", internalName, "index", i)
|
|
}
|
|
}
|
|
return zuschlagIndex
|
|
}
|
|
|
|
// extractRowData liest eine einzelne Datenzeile basierend auf dem Index.
|
|
func (p *ZuschlagParser) extractRowData(row []string, index map[string]int) (string, ZuschlagData, error) {
|
|
var data ZuschlagData
|
|
|
|
lastname := row[index["Lastname"]]
|
|
firstname := row[index["Firstname"]]
|
|
|
|
if lastname == "" || firstname == "" {
|
|
return "", data, fmt.Errorf("leerer name in zeile (lastname: '%s', firstname: '%s')", lastname, firstname)
|
|
}
|
|
compoundKey := fmt.Sprintf("%s,%s", lastname, firstname)
|
|
|
|
for internalName, colIdx := range index {
|
|
if internalName == "Lastname" || internalName == "Firstname" {
|
|
continue
|
|
}
|
|
|
|
if colIdx >= len(row) {
|
|
p.logger.Warn("zuschlag parser: zeile kürzer als header-index", "key", compoundKey, "missing_index", colIdx)
|
|
continue
|
|
}
|
|
|
|
valueStr := row[colIdx]
|
|
valueFloat := p.parseGermanFloat(valueStr)
|
|
|
|
switch internalName {
|
|
case "Nacht25":
|
|
data.Nacht25 = valueFloat
|
|
case "Nacht40":
|
|
data.Nacht40 = valueFloat
|
|
case "Sonntag50":
|
|
data.Sonntag50 = valueFloat
|
|
case "Verpflegung14":
|
|
data.Verpflegung14 = valueFloat
|
|
case "Samstag15":
|
|
data.Samstag15 = valueFloat
|
|
case "Feiertag125":
|
|
data.Feiertag125 = valueFloat
|
|
case "Feiertag150":
|
|
data.Feiertag150 = valueFloat
|
|
|
|
}
|
|
}
|
|
|
|
return compoundKey, data, nil
|
|
}
|
|
|
|
// parseGermanFloat bereinigt und parst deutsche Währungs-/Zahlenformate.
|
|
func (p *ZuschlagParser) parseGermanFloat(valueStr string) float64 {
|
|
valueStr = strings.ReplaceAll(valueStr, ".", ",")
|
|
valueStr = strings.TrimSpace(valueStr)
|
|
|
|
valueStr = strings.ReplaceAll(valueStr, ",", ".")
|
|
|
|
if valueStr == "" {
|
|
return 0.0
|
|
}
|
|
|
|
f, err := strconv.ParseFloat(valueStr, 64)
|
|
if err != nil {
|
|
p.logger.Warn("zuschlag parser: float-parse-fehler", "input", valueStr, "error", err)
|
|
return 0.0
|
|
}
|
|
return f
|
|
}
|