implement latest changes to employee payroll process

- Add ContractDaysWeek
- Add Sickness Days to employee export sheet
- Add Gesundheitsbonus value per employee
- Additional fixes
This commit is contained in:
Marcel Arndt
2025-12-12 11:52:03 +01:00
parent 156733f65a
commit 0951282f11
17 changed files with 408 additions and 205 deletions
@@ -25,7 +25,7 @@ func NewEmployeeHandler(service Service, logger *slog.Logger) *EmployeeHandler {
}
func (h *EmployeeHandler) RunPayroll(w http.ResponseWriter, r *http.Request) {
month := time.Date(2025, time.November, 1, 0, 0, 0, 0, time.UTC)
month := time.Date(2025, time.December, 1, 0, 0, 0, 0, time.UTC)
employees, err := h.service.FetchActiveEmployees(r.Context())
if err != nil {
@@ -85,7 +85,7 @@ func (h *EmployeeHandler) RunPayroll(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(data); err != nil {
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)
}
@@ -16,8 +16,10 @@ type Repository interface {
CountTourengeldAmountInPeriod(ctx context.Context, args database.CountTourengeldAmountInPeriodParams) (float64, error)
GetWeeklyTimeSummaryForEmployee(ctx context.Context, args database.GetWeeklyTimeSummaryForEmployeeParams) ([]database.GetWeeklyTimeSummaryForEmployeeRow, error)
EvaluatePunctualityForEmployee(ctx context.Context, args database.EvaluatePunctualityForEmployeeParams) (int64, error)
IsEmployeesDepartmentNotLeitstelle(ctx context.Context, args database.IsEmployeesDepartmentNotLeitstelleParams) (bool, error)
IsEmployeesDepartmentKTW(ctx context.Context, args database.IsEmployeesDepartmentKTWParams) (bool, error)
CountVerpflegungspauschale(ctx context.Context, args database.CountVerpflegungspauschaleParams) (int64, error)
ListSickdaysForEmployee(ctx context.Context, employeeId int32) ([]database.ListSickdaysForEmployeeRow, error)
}
type Service struct {
@@ -55,8 +57,7 @@ func (s *Service) GeneratePayrollStatement(input domain.PayrollInput) domain.Pay
GearbeiteteTage: float64(input.WorkedDays),
Krankheitstage: float64(input.SickDays),
Ueberstunden: input.OvertimeHours,
Verpflegungspauschale: float64(input.Verpflegungspauschale),
SickdaysYear: input.SickdaysYear,
Wochenendzuschlag15: input.ZuschlagSamstag,
Sonntagszuschlag50: input.ZuschlagSonntag,
@@ -66,10 +67,27 @@ func (s *Service) GeneratePayrollStatement(input domain.PayrollInput) domain.Pay
Feiertagszuschlag150: input.ZuschlagFeiertag150,
}
if input.DepartmentNotLeitstelle {
statement.Verpflegungspauschale = float64(input.Verpflegungspauschale)
}
if input.SickDays > 0 || !input.DepartmentIsKtw {
statement.GesundheitsbonusAbzug = "ja"
}
if input.SickDays <= 0 && input.DepartmentIsKtw {
switch {
case statement.Vertragsstunden > 173.0:
statement.Gesundheitsbonus = 200.0
case statement.Vertragsstunden > 120:
statement.Gesundheitsbonus = 150.0
case statement.Vertragsstunden > 100:
statement.Gesundheitsbonus = 100.0
default:
statement.Gesundheitsbonus = 0.0
}
}
if input.DepartmentIsKtw {
statement.Urlaubsgeld = float64(input.HolidayDays)
statement.PScheinZulage = float64(input.ZulagePassengerTransport)
@@ -107,19 +125,30 @@ func (s *Service) LoadPayrollInput(ctx context.Context, month time.Time, employe
return domain.PayrollInput{}, err
}
isNotLeitstelle, err := s.isEmployeesDepartmentNotLeitstelle(ctx, month, int64(employee.ID))
if err != nil {
s.logger.Error("could not load employee department", "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)
sickdays, err := s.loadSickdays(ctx, int64(employee.ID))
if err != nil {
s.logger.Error("could not load sickdays of employee", "employeeId", employee.ID, "error", err)
}
input := domain.PayrollInput{
Firstname: employee.Firstname,
Surname: employee.Surname,
HourlyRate: employee.ContractSalaryHour,
ContractHours: int64(employee.ContractHoursWeek),
DepartmentIsKtw: isKtw,
Firstname: employee.Firstname,
Surname: employee.Surname,
HourlyRate: employee.ContractSalaryHour,
ContractHours: int64(employee.ContractHoursWeek),
DepartmentIsKtw: isKtw,
DepartmentNotLeitstelle: isNotLeitstelle,
WorkedHours: metrics.WorkedHours,
WorkedDays: int64(metrics.WorkedDays),
@@ -128,6 +157,8 @@ func (s *Service) LoadPayrollInput(ctx context.Context, month time.Time, employe
OvertimeHours: metrics.OvertimeHours,
TotalHours: metrics.TotalHours,
SickdaysYear: sickdays,
ZulageTourengeld: tourengeld,
ZulagePuenktlichkeit: punctuality,
Verpflegungspauschale: verpflegungsPauschale,
@@ -150,12 +181,15 @@ func (s *Service) LoadPayrollInput(ctx context.Context, month time.Time, employe
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) loadSickdays(ctx context.Context, employeeId int64) ([]database.ListSickdaysForEmployeeRow, error) {
return s.repo.ListSickdaysForEmployee(ctx, int32(employeeId))
}
func (s *Service) countVerpflegungspauschale(ctx context.Context, month time.Time, employeeId int64) (int64, error) {
startOfMonth := beginningOfMonth(month)
endOfMonth := endOfMonth(month)
@@ -172,6 +206,22 @@ func (s *Service) countVerpflegungspauschale(ctx context.Context, month time.Tim
return verpflegungsPauschale, nil
}
func (s *Service) isEmployeesDepartmentNotLeitstelle(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)
isNotLeitstelle, err := s.repo.IsEmployeesDepartmentNotLeitstelle(ctx, database.IsEmployeesDepartmentNotLeitstelleParams{
StartDate: &startOfMonth,
EndDate: &endOfMonth,
EmployeeID: employeeId,
})
if err != nil {
return false, fmt.Errorf("failed to fetch department for employee %d: %w", employeeId, err)
}
return isNotLeitstelle, nil
}
func (s *Service) isEmployeesDepartmentKTW(ctx context.Context, month time.Time, employeeId int64) (bool, error) {
startOfMonth := beginningOfMonth(month)
endOfMonth := endOfMonth(month)
@@ -217,7 +267,13 @@ type payrollMetrics struct {
func (s *Service) calculatePayrollMetrics(weeklyTimeData []domain.WeeklyTimeData, employee database.ListActiveEmployeesRow) payrollMetrics {
var m payrollMetrics
employeeDailyContractHours := employee.ContractHoursWeek / 5
employeeDailyContractHours := 0.0
switch {
case employee.ContractDaysWeek <= 0:
employeeDailyContractHours = employee.ContractHoursWeek / 5
default:
employeeDailyContractHours = employee.ContractHoursWeek / float64(employee.ContractDaysWeek)
}
for _, week := range weeklyTimeData {
m.WorkedHours += week.WorkedHours
@@ -261,6 +317,7 @@ func (s *Service) getWeeklyTimeData(ctx context.Context, month time.Time, employ
func (s *Service) fetchTourengeld(ctx context.Context, month time.Time, dlEmployeeId string) (float64, error) {
startOfMonth := beginningOfMonth(month)
endOfMonth := endOfMonth(month)
s.logger.Info("fetching tourengeld for employee", "employeeId", dlEmployeeId, "startDate", startOfMonth, "endDate", endOfMonth)
tourengeld, err := s.repo.CountTourengeldAmountInPeriod(ctx, database.CountTourengeldAmountInPeriodParams{
StartDate: startOfMonth,