293 lines
10 KiB
Go
293 lines
10 KiB
Go
package payroll
|
|
|
|
import (
|
|
"business-engine/internal/database"
|
|
"business-engine/internal/domain"
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
type Repository interface {
|
|
ListActiveEmployees(ctx context.Context) ([]database.ListActiveEmployeesRow, error)
|
|
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)
|
|
IsEmployeesDepartmentKTW(ctx context.Context, args database.IsEmployeesDepartmentKTWParams) (bool, error)
|
|
CountVerpflegungspauschale(ctx context.Context, args database.CountVerpflegungspauschaleParams) (int64, error)
|
|
}
|
|
|
|
type Service struct {
|
|
repo Repository
|
|
logger *slog.Logger
|
|
zuschlagParser *ZuschlagParser
|
|
}
|
|
|
|
func NewService(repo Repository, logger *slog.Logger, zParser *ZuschlagParser) *Service {
|
|
return &Service{
|
|
repo: repo,
|
|
logger: logger,
|
|
zuschlagParser: zParser,
|
|
}
|
|
}
|
|
|
|
func (s *Service) FetchActiveEmployees(ctx context.Context) ([]database.ListActiveEmployeesRow, error) {
|
|
s.logger.Info("fetching active employees")
|
|
|
|
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 {
|
|
return 0, fmt.Errorf("failed to count VerpflegungsPauschale: %w", err)
|
|
}
|
|
return verpflegungsPauschale, nil
|
|
}
|
|
|
|
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: dlEmployeeId,
|
|
Valid: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return 0.0, fmt.Errorf("failed to count tourengeld %w", err)
|
|
}
|
|
return tourengeld, nil
|
|
}
|
|
|
|
func beginningOfMonth(date time.Time) time.Time {
|
|
return date.AddDate(0, 0, -date.Day()+1)
|
|
}
|
|
|
|
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, 999999999,
|
|
eom.Location(),
|
|
)
|
|
}
|