avicenna/service/data-hub/business-engine/internal/payroll/service.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(),
)
}