implement Gehaltsaufstellung dyflexis data access

This commit is contained in:
Marcel Arndt
2025-11-18 13:32:36 +01:00
parent 4abe9e44d8
commit 4cf79f3094
18 changed files with 1015 additions and 1107 deletions
@@ -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(),
)
}