avicenna/service/data-hub/business-engine/internal/payroll/parser.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
}