implement Gehaltsaufstellung dyflexis data access
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
package payroll
|
||||
|
||||
// SalaryComponent is a single position
|
||||
type SalaryComponent struct {
|
||||
Name string
|
||||
Type string
|
||||
Amount int64
|
||||
Value int64
|
||||
Details string
|
||||
}
|
||||
|
||||
type Rule interface {
|
||||
Execute(data PayrollInput) []SalaryComponent
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package payroll
|
||||
|
||||
import (
|
||||
excelread "business-engine/internal/excel_read"
|
||||
"business-engine/internal/domain"
|
||||
"business-engine/internal/gencsv"
|
||||
"business-engine/internal/genpdf"
|
||||
"encoding/json"
|
||||
@@ -24,38 +24,40 @@ func NewEmployeeHandler(service Service, logger *slog.Logger) *EmployeeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *EmployeeHandler) ListEmployees(w http.ResponseWriter, r *http.Request) {
|
||||
month := time.Date(2025, time.September, 1, 0, 0, 0, 0, time.UTC)
|
||||
employees, err := h.service.GenerateMonthlyRun(r.Context(), month)
|
||||
func (h *EmployeeHandler) RunPayroll(w http.ResponseWriter, r *http.Request) {
|
||||
month := time.Date(2025, time.November, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
employees, err := h.service.FetchActiveEmployees(r.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get employees", "error", err)
|
||||
h.logger.Error("failed to fetch active employees", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.logger.Info(fmt.Sprintf("found %d active employees", len(employees)))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(employees); err != nil {
|
||||
h.logger.Error("failed to encode response", "error", err)
|
||||
allZuschlaege, err := h.service.zuschlagParser.ParseFile("daten.csv")
|
||||
if err != nil {
|
||||
h.logger.Error("failed to load data from csv export", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *EmployeeHandler) RunPayroll(w http.ResponseWriter, r *http.Request) {
|
||||
month := time.Date(2025, time.September, 1, 0, 0, 0, 0, time.UTC)
|
||||
employeeData := excelread.ReadEmployeeData()
|
||||
|
||||
var payrollStatements []excelread.PayrollStatement
|
||||
for _, employeeRow := range employeeData {
|
||||
input, err := h.service.FetchPayrollInput(r.Context(), month, employeeRow.Vorname, employeeRow.Nachname)
|
||||
var data []domain.PayrollInput
|
||||
var payrollStatements []domain.PayrollStatement
|
||||
for _, employee := range employees {
|
||||
h.logger.Info("loading payroll input for employee", "name", fmt.Sprintf("%s %s", employee.Firstname, employee.Surname))
|
||||
input, err := h.service.LoadPayrollInput(r.Context(), month.AddDate(0, -1, 0), employee, allZuschlaege)
|
||||
if err != nil {
|
||||
h.logger.Error("could not fetch payroll input", "error", err)
|
||||
// http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
h.logger.Error("failed to fetch payroll input", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
statement := excelread.GeneratePayrollStatement(employeeRow, input)
|
||||
|
||||
statement := h.service.GeneratePayrollStatement(input)
|
||||
payrollStatements = append(payrollStatements, statement)
|
||||
data = append(data, input)
|
||||
}
|
||||
|
||||
slices.SortFunc(payrollStatements, func(a, b excelread.PayrollStatement) int {
|
||||
slices.SortFunc(payrollStatements, func(a, b domain.PayrollStatement) int {
|
||||
if a.Tourengeld < b.Tourengeld {
|
||||
return 1
|
||||
}
|
||||
@@ -70,7 +72,7 @@ func (h *EmployeeHandler) RunPayroll(w http.ResponseWriter, r *http.Request) {
|
||||
payrollStatements[2].TourengeldBonus = 300
|
||||
|
||||
h.logger.Info("generating csv salary statements")
|
||||
err := gencsv.GeneratePayrollStatementCSV(payrollStatements)
|
||||
err = gencsv.GeneratePayrollStatementCSV(payrollStatements)
|
||||
if err != nil {
|
||||
h.logger.Error("could not generate payroll statement csv", "error", err)
|
||||
}
|
||||
@@ -83,8 +85,57 @@ func (h *EmployeeHandler) RunPayroll(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(payrollStatements); err != nil {
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
h.logger.Error("failed to encode response", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// func (h *EmployeeHandler) ReadEmployeeData(w http.ResponseWriter, r *http.Request) {
|
||||
// month := time.Date(2025, time.September, 1, 0, 0, 0, 0, time.UTC)
|
||||
// employeeData := excelread.ReadEmployeeData()
|
||||
|
||||
// var payrollStatements []excelread.PayrollStatement
|
||||
// for _, employeeRow := range employeeData {
|
||||
// input, err := h.service.FetchPayrollInput(r.Context(), month, employeeRow.Vorname, employeeRow.Nachname)
|
||||
// if err != nil {
|
||||
// h.logger.Error("could not fetch payroll input", "error", err)
|
||||
// // http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
// }
|
||||
// statement := excelread.GeneratePayrollStatement(employeeRow, input)
|
||||
// payrollStatements = append(payrollStatements, statement)
|
||||
// }
|
||||
|
||||
// slices.SortFunc(payrollStatements, func(a, b excelread.PayrollStatement) int {
|
||||
// if a.Tourengeld < b.Tourengeld {
|
||||
// return 1
|
||||
// }
|
||||
// if a.Tourengeld > b.Tourengeld {
|
||||
// return -1
|
||||
// }
|
||||
// return 0
|
||||
// })
|
||||
|
||||
// payrollStatements[0].TourengeldBonus = 500
|
||||
// payrollStatements[1].TourengeldBonus = 400
|
||||
// payrollStatements[2].TourengeldBonus = 300
|
||||
|
||||
// h.logger.Info("generating csv salary statements")
|
||||
// err := gencsv.GeneratePayrollStatementCSV(payrollStatements)
|
||||
// if err != nil {
|
||||
// h.logger.Error("could not generate payroll statement csv", "error", err)
|
||||
// }
|
||||
// h.logger.Info("generating pdf salary statements")
|
||||
// for _, stmt := range payrollStatements {
|
||||
// err := genpdf.GenerateSalaryStatement(stmt, fmt.Sprintf("%s_%s.pdf", month.Format("2006-01"), fmt.Sprintf("%s %s", stmt.Vorname, stmt.Nachname)), month)
|
||||
// if err != nil {
|
||||
// h.logger.Error("could not generate salary statement pdf", "error", err)
|
||||
// }
|
||||
// }
|
||||
|
||||
// w.Header().Set("Content-Type", "application/json")
|
||||
// if err := json.NewEncoder(w).Encode(payrollStatements); err != nil {
|
||||
// h.logger.Error("failed to encode response", "error", err)
|
||||
// http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package payroll
|
||||
|
||||
import (
|
||||
"business-engine/internal/database"
|
||||
)
|
||||
|
||||
type PayrollInput struct {
|
||||
database.ListActiveEmployeesRow
|
||||
|
||||
WorkSummary struct {
|
||||
Days int64
|
||||
Hours float64
|
||||
Minutes int64
|
||||
}
|
||||
|
||||
SicknessSummary struct {
|
||||
Days int64
|
||||
Hours float64
|
||||
Minutes int64
|
||||
}
|
||||
|
||||
WorkTime []database.WorkTime
|
||||
}
|
||||
|
||||
func NewPayrollInput(employee database.ListActiveEmployeesRow) PayrollInput {
|
||||
return PayrollInput{
|
||||
ListActiveEmployeesRow: employee,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
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
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package payroll
|
||||
|
||||
type BaseSalaryRule struct{}
|
||||
|
||||
func NewBaseSalaryRule() *BaseSalaryRule {
|
||||
return &BaseSalaryRule{}
|
||||
}
|
||||
|
||||
func (r *BaseSalaryRule) Execute(data PayrollInput) []SalaryComponent {
|
||||
stundenlohnCent := data.ContractSalaryHour * 100
|
||||
|
||||
monatsGehaltCent := stundenlohnCent * data.ContractHoursWeek * 4.33
|
||||
|
||||
return []SalaryComponent{
|
||||
{
|
||||
Name: "Grundgehalt",
|
||||
Type: "Grundgehalt",
|
||||
Amount: int64(monatsGehaltCent),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package payroll
|
||||
|
||||
import (
|
||||
"business-engine/internal/database"
|
||||
excelread "business-engine/internal/excel_read"
|
||||
"business-engine/internal/domain"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -13,165 +13,280 @@ import (
|
||||
|
||||
type Repository interface {
|
||||
ListActiveEmployees(ctx context.Context) ([]database.ListActiveEmployeesRow, error)
|
||||
GetWorkSummaryForEmployeeInPeriod(ctx context.Context, args database.GetWorkSummaryForEmployeeInPeriodParams) (database.GetWorkSummaryForEmployeeInPeriodRow, error)
|
||||
GetSicknessSummaryForEmployeeInPeriod(ctx context.Context, args database.GetSicknessSummaryForEmployeeInPeriodParams) (database.GetSicknessSummaryForEmployeeInPeriodRow, error)
|
||||
ListWorkTimesForEmployeeInPeriod(ctx context.Context, args database.ListWorkTimesForEmployeeInPeriodParams) ([]database.WorkTime, error)
|
||||
ListPlannedTimesForEmployeeInPeriod(ctx context.Context, args database.ListPlannedTimesForEmployeeInPeriodParams) ([]database.ListPlannedTimesForEmployeeInPeriodRow, error)
|
||||
FetchDrivenToursInInterval(ctx context.Context, args database.FetchDrivenToursInIntervalParams) ([]database.Tour, error)
|
||||
CountTourengeldAmountInPeriod(ctx context.Context, args database.CountTourengeldAmountInPeriodParams) (float64, error)
|
||||
GetEmployeeByName(ctx context.Context, args database.GetEmployeeByNameParams) (database.GetEmployeeByNameRow, error)
|
||||
}
|
||||
|
||||
type Engine interface {
|
||||
Calculate() // payroll input -> payroll statement
|
||||
GetWeeklyTimeSummaryForEmployee(ctx context.Context, args database.GetWeeklyTimeSummaryForEmployeeParams) ([]database.GetWeeklyTimeSummaryForEmployeeRow, error)
|
||||
EvaluatePunctualityForEmployee(ctx context.Context, args database.EvaluatePunctualityForEmployeeParams) (int64, error)
|
||||
IsEmployeesDepartmentKTW(ctx context.Context, args database.IsEmployeesDepartmentKTWParams) (bool, error)
|
||||
CountVerpflegungspauschale(ctx context.Context, args database.CountVerpflegungspauschaleParams) (int64, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
repo Repository
|
||||
engine Engine
|
||||
logger *slog.Logger
|
||||
repo Repository
|
||||
logger *slog.Logger
|
||||
zuschlagParser *ZuschlagParser
|
||||
}
|
||||
|
||||
func NewService(repo Repository, logger *slog.Logger) *Service {
|
||||
func NewService(repo Repository, logger *slog.Logger, zParser *ZuschlagParser) *Service {
|
||||
return &Service{
|
||||
repo: repo,
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
logger: logger,
|
||||
zuschlagParser: zParser,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) FetchPayrollInput(ctx context.Context, month time.Time, employeeFirstname, employeeLastname string) (excelread.PayrollInputData, error) {
|
||||
s.logger.Info("fetching payroll input", "month", month)
|
||||
func (s *Service) FetchActiveEmployees(ctx context.Context) ([]database.ListActiveEmployeesRow, error) {
|
||||
s.logger.Info("fetching active employees")
|
||||
|
||||
employeeIDs, err := s.repo.GetEmployeeByName(ctx, database.GetEmployeeByNameParams{
|
||||
Firstname: employeeFirstname,
|
||||
Lastname: employeeLastname,
|
||||
employees, err := s.repo.ListActiveEmployees(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("could not fetch list of active employees", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
return employees, nil
|
||||
}
|
||||
|
||||
func (s *Service) GeneratePayrollStatement(input domain.PayrollInput) domain.PayrollStatement {
|
||||
statement := domain.PayrollStatement{
|
||||
Nachname: input.Surname,
|
||||
Vorname: input.Firstname,
|
||||
VertragStundenlohn: input.HourlyRate,
|
||||
Vertragsstunden: float64(input.ContractHours) * 4.333,
|
||||
GearbeiteteStunden: input.WorkedHours,
|
||||
GearbeiteteTage: float64(input.WorkedDays),
|
||||
Krankheitstage: float64(input.SickDays),
|
||||
Ueberstunden: input.OvertimeHours,
|
||||
|
||||
Verpflegungspauschale: float64(input.Verpflegungspauschale),
|
||||
|
||||
Wochenendzuschlag15: input.ZuschlagSamstag,
|
||||
Sonntagszuschlag50: input.ZuschlagSonntag,
|
||||
Nachtzuschlag25: input.ZuschlagNacht25,
|
||||
Nachtzuschlag40: input.ZuschlagNacht40,
|
||||
Feiertagszuschlag125: input.ZuschlagFeiertag125,
|
||||
Feiertagszuschlag150: input.ZuschlagFeiertag150,
|
||||
}
|
||||
|
||||
if input.SickDays > 0 || !input.DepartmentIsKtw {
|
||||
statement.GesundheitsbonusAbzug = "ja"
|
||||
}
|
||||
|
||||
if input.DepartmentIsKtw {
|
||||
statement.Urlaubsgeld = float64(input.HolidayDays)
|
||||
statement.PScheinZulage = float64(input.ZulagePassengerTransport)
|
||||
statement.PuenktlichkeitspraemieAnzahl = float64(input.ZulagePuenktlichkeit)
|
||||
statement.Tourengeld = input.ZulageTourengeld
|
||||
}
|
||||
return statement
|
||||
}
|
||||
|
||||
func (s *Service) LoadPayrollInput(ctx context.Context, month time.Time, employee database.ListActiveEmployeesRow, zuschlaege map[string]ZuschlagData) (domain.PayrollInput, error) {
|
||||
|
||||
weeklyTimeData, err := s.getWeeklyTimeData(ctx, month, int64(employee.ID))
|
||||
if err != nil {
|
||||
s.logger.Error("failed to sum worked hours for employee", "employeeId", employee.ID, "error", err)
|
||||
return domain.PayrollInput{}, fmt.Errorf("failed to get time data for employee %d: %w", employee.ID, err)
|
||||
}
|
||||
|
||||
metrics := s.calculatePayrollMetrics(weeklyTimeData, employee)
|
||||
|
||||
tourengeld, err := s.fetchTourengeld(ctx, month, employee.DlId)
|
||||
if err != nil {
|
||||
s.logger.Error("could not fetch Tourengeld for employee", "employeeId", employee.ID, "error", err)
|
||||
return domain.PayrollInput{}, err
|
||||
}
|
||||
|
||||
punctuality, err := s.getPunctualityData(ctx, month, int64(employee.ID))
|
||||
if err != nil {
|
||||
s.logger.Error("could not fetch punctuality for employee", "employeeId", employee.ID, "error", err)
|
||||
return domain.PayrollInput{}, err
|
||||
}
|
||||
|
||||
verpflegungsPauschale, err := s.countVerpflegungspauschale(ctx, month, int64(employee.ID))
|
||||
if err != nil {
|
||||
s.logger.Error("could not count verpflegungspauschale for employee", "employeeId", employee.ID, "error", err)
|
||||
return domain.PayrollInput{}, err
|
||||
}
|
||||
|
||||
isKtw, err := s.isEmployeesDepartmentKTW(ctx, month, int64(employee.ID))
|
||||
if err != nil {
|
||||
s.logger.Error("could not load employee department", "error", err)
|
||||
return domain.PayrollInput{}, err
|
||||
}
|
||||
s.logger.Debug("fetched department", "isKtw", isKtw)
|
||||
|
||||
input := domain.PayrollInput{
|
||||
Firstname: employee.Firstname,
|
||||
Surname: employee.Surname,
|
||||
HourlyRate: employee.ContractSalaryHour,
|
||||
ContractHours: int64(employee.ContractHoursWeek),
|
||||
DepartmentIsKtw: isKtw,
|
||||
|
||||
WorkedHours: metrics.WorkedHours,
|
||||
WorkedDays: int64(metrics.WorkedDays),
|
||||
SickDays: int64(metrics.SickHours / 8),
|
||||
HolidayDays: int64(metrics.HolidayDays),
|
||||
OvertimeHours: metrics.OvertimeHours,
|
||||
TotalHours: metrics.TotalHours,
|
||||
|
||||
ZulageTourengeld: tourengeld,
|
||||
ZulagePuenktlichkeit: punctuality,
|
||||
Verpflegungspauschale: verpflegungsPauschale,
|
||||
}
|
||||
|
||||
if isKtw && employee.PersonTransportCertificate != nil {
|
||||
if employee.PersonTransportCertificate.After(month) {
|
||||
input.ZulagePassengerTransport = int64(metrics.WorkedDays)
|
||||
}
|
||||
}
|
||||
|
||||
compoundKey := fmt.Sprintf("%s,%s", employee.Surname, employee.Firstname)
|
||||
if zuschlagData, ok := zuschlaege[compoundKey]; ok {
|
||||
input.ZuschlagNacht25 = zuschlagData.Nacht25
|
||||
input.ZuschlagNacht40 = zuschlagData.Nacht40
|
||||
input.ZuschlagSamstag = zuschlagData.Samstag15
|
||||
input.ZuschlagSonntag = zuschlagData.Sonntag50
|
||||
input.ZuschlagFeiertag125 = zuschlagData.Feiertag125
|
||||
input.ZuschlagFeiertag150 = zuschlagData.Feiertag150
|
||||
input.ZuschlagVerpflegung = zuschlagData.Verpflegung14
|
||||
} else {
|
||||
s.logger.Warn("Keine Zuschlag-Daten für Mitarbeiter gefunden", "key", compoundKey)
|
||||
// (Kein Fehler, der Mitarbeiter hatte vielleicht keine Zuschläge)
|
||||
}
|
||||
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func (s *Service) countVerpflegungspauschale(ctx context.Context, month time.Time, employeeId int64) (int64, error) {
|
||||
startOfMonth := beginningOfMonth(month)
|
||||
endOfMonth := endOfMonth(month)
|
||||
s.logger.Info("counting VerpflegungsPauschale for employee", "employeeId", employeeId, "startDate", startOfMonth, "endDate", endOfMonth)
|
||||
|
||||
verpflegungsPauschale, err := s.repo.CountVerpflegungspauschale(ctx, database.CountVerpflegungspauschaleParams{
|
||||
StartDate: &startOfMonth,
|
||||
EndDate: &endOfMonth,
|
||||
EmployeeID: employeeId,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("could not find employee with that name", "firstname", employeeFirstname, "lastname", employeeLastname, "error", err)
|
||||
return excelread.PayrollInputData{}, err
|
||||
return 0, fmt.Errorf("failed to count VerpflegungsPauschale: %w", err)
|
||||
}
|
||||
s.logger.Debug("got employee data", "employee", employeeIDs)
|
||||
return verpflegungsPauschale, nil
|
||||
}
|
||||
|
||||
startOfMonth := BeginningOfMonth(month.AddDate(0, -1, 0))
|
||||
endOfMonth := EndOfMonth(month.AddDate(0, -1, 0))
|
||||
s.logger.Info("fetching data for interval", "startDate", startOfMonth, "endDate", endOfMonth)
|
||||
func (s *Service) isEmployeesDepartmentKTW(ctx context.Context, month time.Time, employeeId int64) (bool, error) {
|
||||
startOfMonth := beginningOfMonth(month)
|
||||
endOfMonth := endOfMonth(month)
|
||||
s.logger.Info("fetching department for employee", "employeeId", employeeId, "startDate", startOfMonth, "endDate", endOfMonth)
|
||||
|
||||
isKtw, err := s.repo.IsEmployeesDepartmentKTW(ctx, database.IsEmployeesDepartmentKTWParams{
|
||||
StartDate: &startOfMonth,
|
||||
EndDate: &endOfMonth,
|
||||
EmployeeID: employeeId,
|
||||
})
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to fetch department for employee %d: %w", employeeId, err)
|
||||
}
|
||||
return isKtw, nil
|
||||
}
|
||||
|
||||
func (s *Service) getPunctualityData(ctx context.Context, month time.Time, employeeId int64) (int64, error) {
|
||||
startOfMonth := beginningOfMonth(month)
|
||||
endOfMonth := endOfMonth(month)
|
||||
s.logger.Info("fetching punctuality for employee", "employeeId", employeeId, "startDate", startOfMonth, "endDate", endOfMonth)
|
||||
|
||||
punctuality, err := s.repo.EvaluatePunctualityForEmployee(ctx, database.EvaluatePunctualityForEmployeeParams{
|
||||
StartDate: &startOfMonth,
|
||||
EndDate: &endOfMonth,
|
||||
EmployeeID: employeeId,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to fetch punctuality evaluation for employee %d: %w", employeeId, err)
|
||||
}
|
||||
|
||||
return punctuality, nil
|
||||
}
|
||||
|
||||
type payrollMetrics struct {
|
||||
WorkedHours float64
|
||||
WorkedDays float64
|
||||
SickHours float64
|
||||
HolidayDays float64
|
||||
OvertimeHours float64
|
||||
TotalHours float64
|
||||
}
|
||||
|
||||
func (s *Service) calculatePayrollMetrics(weeklyTimeData []domain.WeeklyTimeData, employee database.ListActiveEmployeesRow) payrollMetrics {
|
||||
var m payrollMetrics
|
||||
|
||||
employeeDailyContractHours := employee.ContractHoursWeek / 5
|
||||
|
||||
for _, week := range weeklyTimeData {
|
||||
m.WorkedHours += week.WorkedHours
|
||||
m.WorkedDays += week.WorkedDays
|
||||
m.SickHours += week.SickHours
|
||||
m.HolidayDays += week.HolidayDays
|
||||
}
|
||||
m.TotalHours = m.WorkedHours + m.SickHours + (m.HolidayDays * employeeDailyContractHours)
|
||||
m.OvertimeHours = m.TotalHours - (employee.ContractHoursWeek * 4.3333)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *Service) getWeeklyTimeData(ctx context.Context, month time.Time, employeeId int64) ([]domain.WeeklyTimeData, error) {
|
||||
startOfMonth := beginningOfMonth(month)
|
||||
endOfMonth := endOfMonth(month)
|
||||
s.logger.Info("fetching weekly summary for employee", "employeeId", employeeId, "startDate", startOfMonth, "endDate", endOfMonth)
|
||||
timeSummary, err := s.repo.GetWeeklyTimeSummaryForEmployee(ctx, database.GetWeeklyTimeSummaryForEmployeeParams{
|
||||
StartDate: &startOfMonth,
|
||||
EndDate: &endOfMonth,
|
||||
EmployeeID: fmt.Sprintf("%d", employeeId),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch weekly time summary for employee %d. %w", employeeId, err)
|
||||
}
|
||||
|
||||
var weeklyTimeData []domain.WeeklyTimeData
|
||||
for _, week := range timeSummary {
|
||||
wtd := domain.WeeklyTimeData{
|
||||
WeekStart: week.WeekStart,
|
||||
WorkedHours: week.TotalWorkedHours,
|
||||
WorkedDays: week.TotalWorkedDays,
|
||||
SickHours: week.TotalSickHours,
|
||||
HolidayDays: week.TotalHolidayDays,
|
||||
}
|
||||
weeklyTimeData = append(weeklyTimeData, wtd)
|
||||
}
|
||||
return weeklyTimeData, nil
|
||||
}
|
||||
|
||||
func (s *Service) fetchTourengeld(ctx context.Context, month time.Time, dlEmployeeId string) (float64, error) {
|
||||
startOfMonth := beginningOfMonth(month)
|
||||
endOfMonth := endOfMonth(month)
|
||||
|
||||
tourengeld, err := s.repo.CountTourengeldAmountInPeriod(ctx, database.CountTourengeldAmountInPeriodParams{
|
||||
StartDate: startOfMonth,
|
||||
EndDate: endOfMonth,
|
||||
EmployeeID: pgtype.Text{
|
||||
String: employeeIDs.DispoliveID,
|
||||
String: dlEmployeeId,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("could not count tourengeld for employee", "employeeId", employeeIDs.DyflexisID, "error", err)
|
||||
return 0.0, fmt.Errorf("failed to count tourengeld %w", err)
|
||||
}
|
||||
|
||||
plannedtimes, err := s.repo.ListPlannedTimesForEmployeeInPeriod(ctx, database.ListPlannedTimesForEmployeeInPeriodParams{
|
||||
EmployeeID: fmt.Sprintf("%d", employeeIDs.DyflexisID),
|
||||
StartDate: &startOfMonth,
|
||||
EndDate: &endOfMonth,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("could not fetch plannedtimes for employee", "employeeId", employeeIDs.DyflexisID, "error", err)
|
||||
}
|
||||
|
||||
worktimes, err := s.repo.ListWorkTimesForEmployeeInPeriod(ctx, database.ListWorkTimesForEmployeeInPeriodParams{
|
||||
EmployeeID: fmt.Sprintf("%d", employeeIDs.DyflexisID),
|
||||
StartDate: &startOfMonth,
|
||||
EndDate: &endOfMonth,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("could not fetch worktimes for employee", "employeeId", employeeIDs.DyflexisID, "error", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Fetched payroll input", "employee", fmt.Sprintf("%s %s", employeeFirstname, employeeLastname), "tourengeld", tourengeld, "workdays", len(worktimes))
|
||||
|
||||
return excelread.NewPayrollInputData(employeeIDs.ContractSalaryHour, tourengeld, worktimes, plannedtimes), nil
|
||||
return tourengeld, nil
|
||||
}
|
||||
|
||||
func (s *Service) GenerateMonthlyRun(ctx context.Context, month time.Time) ([]PayrollInput, error) {
|
||||
s.logger.Info("starting monthly payroll run", "month", month)
|
||||
|
||||
employees, err := s.repo.ListActiveEmployees(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("could not get active employees", "error", err)
|
||||
return []PayrollInput{}, err
|
||||
}
|
||||
s.logger.Info("found active employees for payroll run", "count", len(employees))
|
||||
|
||||
var inputs []PayrollInput
|
||||
|
||||
for _, employee := range employees {
|
||||
log := s.logger.With("employee_id", employee.ID)
|
||||
log.Info("processing employee")
|
||||
|
||||
payrollInput := NewPayrollInput(employee)
|
||||
startOfMonth := BeginningOfMonth(month.AddDate(0, -1, 0))
|
||||
endOfMonth := EndOfMonth(month.AddDate(0, -1, 0))
|
||||
|
||||
workSummary, err := s.repo.GetWorkSummaryForEmployeeInPeriod(ctx, database.GetWorkSummaryForEmployeeInPeriodParams{
|
||||
EmployeeID: fmt.Sprintf("%d", employee.ID),
|
||||
StartDate: &startOfMonth,
|
||||
EndDate: &endOfMonth,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("could not get work summary for employee", "error", err)
|
||||
return []PayrollInput{}, err
|
||||
}
|
||||
|
||||
payrollInput.WorkSummary = struct {
|
||||
Days int64
|
||||
Hours float64
|
||||
Minutes int64
|
||||
}(workSummary)
|
||||
|
||||
sicknessSummary, err := s.repo.GetSicknessSummaryForEmployeeInPeriod(ctx, database.GetSicknessSummaryForEmployeeInPeriodParams{
|
||||
EmployeeID: fmt.Sprintf("%d", employee.ID),
|
||||
StartDate: &startOfMonth,
|
||||
EndDate: &endOfMonth,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("could not get work summary for employee", "error", err)
|
||||
return []PayrollInput{}, err
|
||||
}
|
||||
|
||||
payrollInput.SicknessSummary = struct {
|
||||
Days int64
|
||||
Hours float64
|
||||
Minutes int64
|
||||
}(sicknessSummary)
|
||||
|
||||
worktimes, err := s.repo.ListWorkTimesForEmployeeInPeriod(ctx, database.ListWorkTimesForEmployeeInPeriodParams{
|
||||
EmployeeID: fmt.Sprintf("%d", employee.ID),
|
||||
StartDate: &startOfMonth,
|
||||
EndDate: &endOfMonth,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("could not get worktimes for employee", "error", err)
|
||||
return []PayrollInput{}, nil
|
||||
}
|
||||
|
||||
payrollInput.WorkTime = worktimes
|
||||
|
||||
inputs = append(inputs, payrollInput)
|
||||
}
|
||||
|
||||
return inputs, nil
|
||||
}
|
||||
|
||||
func BeginningOfMonth(date time.Time) time.Time {
|
||||
func beginningOfMonth(date time.Time) time.Time {
|
||||
return date.AddDate(0, 0, -date.Day()+1)
|
||||
}
|
||||
|
||||
func EndOfMonth(date time.Time) time.Time {
|
||||
func endOfMonth(date time.Time) time.Time {
|
||||
eom := date.AddDate(0, 1, -date.Day())
|
||||
return time.Date(
|
||||
eom.Year(),
|
||||
eom.Month(),
|
||||
eom.Day(),
|
||||
23, 59, 59, 999,
|
||||
23, 59, 59, 999999999,
|
||||
eom.Location(),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user