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 }