implement payroll generator v0.9

This commit is contained in:
Marcel Arndt 2025-11-06 11:38:59 +01:00
parent 319e7f3614
commit 4abe9e44d8
27 changed files with 4273 additions and 0 deletions

View File

@ -0,0 +1,52 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/api"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,35 @@
# --- Build Stage ---
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Dependencies laden, bevor wir den Code kopieren, um den Layer-Cache zu nutzen
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# sqlc generieren und App bauen
# (Wir brauchen sqlc im Build-Container)
RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
RUN sqlc generate
RUN CGO_ENABLED=0 GOOS=linux go build -v -o /business-engine ./cmd/api
# --- Final Stage ---
FROM alpine:latest
# Root-Zertifikate für sichere Verbindungen
RUN apk --no-cache add ca-certificates
# User anlegen, um nicht als root zu laufen
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /home/appuser
COPY --from=builder /business-engine .
COPY config.yaml .
EXPOSE 8080
CMD ["./business-engine"]

View File

@ -0,0 +1,12 @@
.PHONY: db/update-schema
db/update-schema:
@echo "--> Dumping database schema..."
@docker exec -it data-connector-avicenna-db-1 pg_dump --schema-only --no-owner --no-privileges \
-U johndoe avicenna > ./db/migrations/db.sql
@echo "--> Schema dump complete"
@$(MAKE) generate/sqlc
.PHONY: generate/sqlc
generate/sqlc:
@echo "--> Generating SQLC code..."
@sqlc generate

View File

@ -0,0 +1,30 @@
package main
import (
"business-engine/internal/config"
"log/slog"
"os"
)
func main() {
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
logger := slog.New(handler)
slog.SetDefault(logger)
cfg, err := config.Load()
if err != nil {
logger.Error("failed to load configuration", "error", err)
os.Exit(1)
}
server, err := NewServer(cfg, logger)
if err != nil {
logger.Error("failed to create server", "error", err)
os.Exit(1)
}
server.Start()
}

View File

@ -0,0 +1,89 @@
package main
import (
"business-engine/internal/config"
"business-engine/internal/database"
"business-engine/internal/payroll"
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// Server contains all dependencies of the application
type Server struct {
config *config.Config
logger *slog.Logger
db *pgxpool.Pool
router *http.ServeMux
}
// NewServer constructs a new server
func NewServer(cfg *config.Config, logger *slog.Logger) (*Server, error) {
dbpool, err := pgxpool.New(context.Background(), cfg.DatabaseURL)
if err != nil {
logger.Error("failed to connect to database", "error", err)
os.Exit(1)
}
server := &Server{
config: cfg,
logger: logger,
db: dbpool,
router: http.NewServeMux(),
}
server.setupRoutes()
return server, nil
}
// setupRoutes initializes all routes and handlers
func (s *Server) setupRoutes() {
employeeRepo := database.New(s.db)
employeeService := payroll.NewService(employeeRepo, s.logger)
employeeHandler := payroll.NewEmployeeHandler(*employeeService, s.logger)
s.router.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
s.router.HandleFunc("GET /api/v1/employees", employeeHandler.ListEmployees)
s.router.HandleFunc("GET /api/v1/payroll", employeeHandler.RunPayroll)
}
func (s *Server) Start() {
httpServer := &http.Server{
Addr: ":" + s.config.Port,
Handler: s.router,
}
go func() {
s.logger.Info("server starting", "port", s.config.Port)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
s.logger.Error("server failed to start", "error", err)
os.Exit(1)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
s.logger.Info("shutting down server gracefully")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := httpServer.Shutdown(ctx); err != nil {
s.logger.Error("server shutdown failed", "error", err)
} else {
s.logger.Info("server stopped")
}
}

View File

@ -0,0 +1,23 @@
port: "8080"
logLevel: "info"
databaseUrl: "postgres://johndoe:randompassword@localhost:5432/avicenna"
# Unsere komplexe, statische Business-Logik-Konfiguration
surchargeRules:
- name: "Nachtzuschlag (Mo-Fr)"
combinationLogic: "TAKE_HIGHEST"
timeConditions:
- weekdays: [1, 2, 3, 4, 5] # Mo, Di, Mi, Do, Fr
startTime: "22:00"
endTime: "06:00"
calculation:
type: "PERCENTAGE"
value: 25.0
- name: "Sonntagszuschlag"
combinationLogic: "TAKE_HIGHEST"
timeConditions:
- weekdays: [0] # Sonntag
calculation:
type: "PERCENTAGE"
value: 50.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,181 @@
-- name: ListActiveEmployees :many
SELECT
de.id,
de.firstname,
de.surname,
de."contractStart",
de."contractEnd",
de."contractHoursWeek",
de."contractSalaryHour",
de."contractTypeName",
e.id as "dlId",
e."personTransportCertificate"
FROM "DyflexisEmployee" as de
INNER JOIN "Employee" as e ON de.id::text = e."personnelNumber"
WHERE de."contractStart" < CURRENT_DATE AND de."contractEnd" > (CURRENT_DATE - INTERVAL '2 months');
-- name: GetWorkSummaryForEmployeeInPeriod :one
WITH wt AS (
SELECT
COUNT(*) as days,
COALESCE(sum(duration), 0)::bigint as minutes
FROM "WorkTime"
WHERE "userId" = @employee_id AND "startDate" > @start_date AND "startDate" < @end_date
)
SELECT
wt.days,
(minutes / 60.0)::float as hours,
wt.minutes
FROM wt;
-- name: ListPlannedTimesForEmployeeInPeriod :many
SELECT
DISTINCT ON (day)
"startDate"::DATE as day,
"startDate",
"endDate",
department,
pause,
duration
FROM "PlannedTime"
WHERE
"userId" = @employee_id AND
"startDate" > @start_date AND
"startDate" < @end_date AND
department <> 'Fortbildung' AND
note IS NULL
ORDER BY day, "startDate" ASC;
-- name: ListWorkTimesForEmployeeInPeriod :many
SELECT * FROM "WorkTime"
WHERE "userId" = @employee_id AND "startDate" > @start_date AND "startDate" < @end_date;
-- name: GetSicknessSummaryForEmployeeInPeriod :one
WITH pt AS (
SELECT
COUNT(*) as days,
COALESCE(sum(duration), 0)::bigint as minutes
FROM "PlannedTime"
WHERE "userId" = @employee_id AND "startDate" > @start_date AND "startDate" < @end_date AND note = 'sick'
)
SELECT
pt.days,
(minutes / 60)::float as hours,
pt.minutes
FROM pt;
-- name: GetEmployeeDyflexis :one
SELECT * FROM "DyflexisEmployee"
WHERE
id = $1 AND
"contractStart" < CURRENT_DATE AND
"contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
LIMIT 1;
-- name: FetchDrivenToursInInterval :many
SELECT * FROM "Tour"
WHERE "check" <> 3
AND "deletedAt" IS NULL
AND (
"patientId" IS NULL
OR "patientId" NOT IN (
'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
'3554e1a9-9c07-45e5-b53c-692a77e4fa9a'
)
)
-- ID's for Pause and Feierabend respectively
AND (
"carName" IS NULL
OR "carName" NOT IN (
'Storno',
'Abgabe',
'Löschen'
)
)
AND "startDate" >= @start_date
AND "startDate" <= @end_date
AND ("driverId" = @employee_id OR "codriverId" = @employee_id)
ORDER BY "startDate" DESC;
-- name: CountTourengeldAmountInPeriod :one
WITH ToursInPeriod AS (
SELECT
(("startDate" AT TIME ZONE 'UTC') AT TIME ZONE 'Europe/Berlin')::DATE as day,
"operationId",
"occupiedKm",
CASE
WHEN "occupiedKm" <= 30 THEN 1.0
ELSE ROUND("occupiedKm" / 30.0)
END AS tour_value,
CASE
WHEN EXTRACT(ISODOW FROM "startDate") = 6 AND "occupiedKm" <= 30 THEN 0.5
ELSE 0.0
END AS weekend_bonus,
"infectionName",
CASE
WHEN "infectionName" = 'Covid-19' THEN 0.5
ELSE 0.0
END AS infection_bonus
FROM
"Tour"
WHERE
"check" <> 3
AND "deletedAt" IS NULL
AND (
"patientId" IS NULL
OR "patientId" NOT IN (
'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
'3554e1a9-9c07-45e5-b53c-692a77e4fa9a'
)
)
-- ID's for Pause and Feierabend respectively
AND (
"carName" IS NULL
OR "carName" NOT IN (
'Storno',
'Abgabe',
'Löschen'
)
AND "carName" LIKE '%KTW%'
)
AND "startDate" >= @start_date
AND "startDate" <= @end_date
AND ("driverId" ILIKE @employee_id OR "codriverId" ILIKE @employee_id)
ORDER BY
"day"
),
DailyCounts AS (
SELECT
day,
SUM(tour_value) + SUM(weekend_bonus) + SUM(infection_bonus) AS tour_amount
FROM
ToursInPeriod
GROUP BY
day
),
HighActivityDays AS (
SELECT
day,
GREATEST(0, tour_amount - 6) AS tourengeld
FROM
DailyCounts
)
SELECT
SUM(tourengeld)::float as tourengeld
FROM
HighActivityDays;
-- name: GetEmployeeByName :one
SELECT
dy.id as dyflexis_id,
dl.id as dispolive_id,
dy."contractSalaryHour"
FROM "DyflexisEmployee" as dy
INNER JOIN
"Employee" as dl ON dy.id::text = dl."personnelNumber"
WHERE
TRIM(dy.firstname) = @firstname AND
TRIM(dy.surname) = @lastname AND
"contractStart" < CURRENT_DATE AND
"contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
ORDER BY dy."contractEnd" DESC;

View File

@ -0,0 +1,34 @@
module business-engine
go 1.24.0
toolchain go1.24.9
require (
github.com/jackc/pgx/v5 v5.7.6
github.com/spf13/viper v1.21.0
)
require (
codeberg.org/go-pdf/fpdf v0.11.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgtype v1.14.4 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.30.0 // indirect
)

View File

@ -0,0 +1,255 @@
codeberg.org/go-pdf/fpdf v0.11.1 h1:U8+coOTDVLxHIXZgGvkfQEi/q0hYHYvEHFuGNX2GzGs=
codeberg.org/go-pdf/fpdf v0.11.1/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8=
github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=

View File

@ -0,0 +1,39 @@
package config
import (
"fmt"
"strings"
"github.com/spf13/viper"
)
type Config struct {
Port string
LogLevel string
DatabaseURL string `mapstructure:"databaseUrl"`
// SurchargeRules []models.SurchargeRule `mapstructure:"surchargeRules"`
}
func Load() (*Config, error) {
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/business-engine/")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
}
viper.BindEnv("databaseUrl", "DATABASE_URL")
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
return &cfg, nil
}

View File

@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package database
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@ -0,0 +1,714 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package database
import (
"database/sql/driver"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
type ApprovalState string
const (
ApprovalStateUNKNOWN ApprovalState = "UNKNOWN"
ApprovalStateFREE ApprovalState = "FREE"
ApprovalStateREQUIRED ApprovalState = "REQUIRED"
ApprovalStateREQUESTED ApprovalState = "REQUESTED"
ApprovalStateAPPROVED ApprovalState = "APPROVED"
)
func (e *ApprovalState) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ApprovalState(s)
case string:
*e = ApprovalState(s)
default:
return fmt.Errorf("unsupported scan type for ApprovalState: %T", src)
}
return nil
}
type NullApprovalState struct {
ApprovalState ApprovalState `json:"ApprovalState"`
Valid bool `json:"valid"` // Valid is true if ApprovalState is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullApprovalState) Scan(value interface{}) error {
if value == nil {
ns.ApprovalState, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ApprovalState.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullApprovalState) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ApprovalState), nil
}
type CostTypeVariant string
const (
CostTypeVariantNACHTZUSCHLAG CostTypeVariant = "NACHTZUSCHLAG"
CostTypeVariantWOCHENENDZUSCHLAG CostTypeVariant = "WOCHENENDZUSCHLAG"
CostTypeVariantFEIERTAGSZUSCHLAG CostTypeVariant = "FEIERTAGSZUSCHLAG"
CostTypeVariantDESINFEKTIONSZUSCHLAG CostTypeVariant = "DESINFEKTIONSZUSCHLAG"
CostTypeVariantKILOMETERZUSCHLAG CostTypeVariant = "KILOMETERZUSCHLAG"
CostTypeVariantFAHRZEUGZUSCHLAG CostTypeVariant = "FAHRZEUGZUSCHLAG"
CostTypeVariantTSWDIFFERENZIERUNG CostTypeVariant = "TSWDIFFERENZIERUNG"
CostTypeVariantGRUNDPREIS CostTypeVariant = "GRUNDPREIS"
CostTypeVariantGRUNDPREISZEIT CostTypeVariant = "GRUNDPREIS_ZEIT"
CostTypeVariantPRIVATVERSICHERUNG CostTypeVariant = "PRIVATVERSICHERUNG"
CostTypeVariantTRAGEHILFE CostTypeVariant = "TRAGEHILFE"
)
func (e *CostTypeVariant) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = CostTypeVariant(s)
case string:
*e = CostTypeVariant(s)
default:
return fmt.Errorf("unsupported scan type for CostTypeVariant: %T", src)
}
return nil
}
type NullCostTypeVariant struct {
CostTypeVariant CostTypeVariant `json:"CostTypeVariant"`
Valid bool `json:"valid"` // Valid is true if CostTypeVariant is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullCostTypeVariant) Scan(value interface{}) error {
if value == nil {
ns.CostTypeVariant, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.CostTypeVariant.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullCostTypeVariant) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.CostTypeVariant), nil
}
type TicketState string
const (
TicketStateCREATED TicketState = "CREATED"
TicketStateAPPROVALREQUIRED TicketState = "APPROVAL_REQUIRED"
TicketStateREADY TicketState = "READY"
TicketStateDONE TicketState = "DONE"
TicketStateBILLABLE TicketState = "BILLABLE"
TicketStateARCHIVED TicketState = "ARCHIVED"
)
func (e *TicketState) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = TicketState(s)
case string:
*e = TicketState(s)
default:
return fmt.Errorf("unsupported scan type for TicketState: %T", src)
}
return nil
}
type NullTicketState struct {
TicketState TicketState `json:"TicketState"`
Valid bool `json:"valid"` // Valid is true if TicketState is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullTicketState) Scan(value interface{}) error {
if value == nil {
ns.TicketState, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.TicketState.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullTicketState) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.TicketState), nil
}
type TicketValidationState string
const (
TicketValidationStateCREATED TicketValidationState = "CREATED"
TicketValidationStateUSERINTERACTIONREQUIRED TicketValidationState = "USER_INTERACTION_REQUIRED"
TicketValidationStateTDUNCERTAIN TicketValidationState = "TD_UNCERTAIN"
TicketValidationStateDOCUMENTSMISSING TicketValidationState = "DOCUMENTS_MISSING"
TicketValidationStateARCHIVED TicketValidationState = "ARCHIVED"
)
func (e *TicketValidationState) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = TicketValidationState(s)
case string:
*e = TicketValidationState(s)
default:
return fmt.Errorf("unsupported scan type for TicketValidationState: %T", src)
}
return nil
}
type NullTicketValidationState struct {
TicketValidationState TicketValidationState `json:"TicketValidationState"`
Valid bool `json:"valid"` // Valid is true if TicketValidationState is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullTicketValidationState) Scan(value interface{}) error {
if value == nil {
ns.TicketValidationState, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.TicketValidationState.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullTicketValidationState) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.TicketValidationState), nil
}
type Anomaly struct {
ID string `json:"id"`
Type string `json:"type"`
Description string `json:"description"`
GroupKey string `json:"groupKey"`
GroupDescription string `json:"groupDescription"`
WorkedOnBy string `json:"workedOnBy"`
SleepTimer int32 `json:"sleepTimer"`
SleepSince *time.Time `json:"sleepSince"`
ResolvedAt *time.Time `json:"resolvedAt"`
ManuallyResolved bool `json:"manuallyResolved"`
CreatedAt time.Time `json:"createdAt"`
AssociatedEntities []byte `json:"associatedEntities"`
}
type AttendanceRegistration struct {
AttendanceRegistrationId int32 `json:"attendanceRegistrationId"`
EmployeeId int32 `json:"employeeId"`
PersonnelNumber string `json:"personnelNumber"`
DateTime time.Time `json:"dateTime"`
Event string `json:"event"`
}
type AttendanceRegistrationSource struct {
InternalId pgtype.UUID `json:"internalId"`
InsertedAt time.Time `json:"insertedAt"`
JobId string `json:"jobId"`
AttendanceRegistrationId pgtype.Text `json:"attendanceRegistrationId"`
EmployeeId pgtype.Text `json:"employeeId"`
PersonnelNumber pgtype.Text `json:"personnelNumber"`
DateTime pgtype.Text `json:"dateTime"`
Event pgtype.Text `json:"event"`
}
type AttendanceRegistrationStaging struct {
InternalId pgtype.UUID `json:"internalId"`
AttendanceRegistrationId int32 `json:"attendanceRegistrationId"`
EmployeeId int32 `json:"employeeId"`
DateTime time.Time `json:"dateTime"`
Event string `json:"event"`
}
type AvicennaTenant struct {
ID pgtype.UUID `json:"id"`
HealthInsuranceNumber string `json:"healthInsuranceNumber"`
Name string `json:"name"`
}
type AvicennaTenantToTariffGroup struct {
A pgtype.UUID `json:"A"`
B pgtype.UUID `json:"B"`
}
type CostType struct {
ID pgtype.UUID `json:"id"`
Description string `json:"description"`
Variant CostTypeVariant `json:"variant"`
CostAmount int32 `json:"costAmount"`
TimeRange pgtype.Text `json:"timeRange"`
KmInclusive pgtype.Int4 `json:"kmInclusive"`
ExcludeCarNames []string `json:"excludeCarNames"`
InfectionType pgtype.Text `json:"infectionType"`
TariffId pgtype.UUID `json:"tariffId"`
}
type DyflexisEmployee struct {
ID int32 `json:"id"`
Firstname string `json:"firstname"`
Surname string `json:"surname"`
ContractStart time.Time `json:"contractStart"`
ContractEnd *time.Time `json:"contractEnd"`
ContractHoursWeek float64 `json:"contractHoursWeek"`
ContractSalaryHour float64 `json:"contractSalaryHour"`
ContractTypeName pgtype.Text `json:"contractTypeName"`
InternalId pgtype.UUID `json:"internalId"`
}
type Employee struct {
ID string `json:"id"`
Name string `json:"name"`
Surname string `json:"surname"`
PersonnelNumber pgtype.Text `json:"personnelNumber"`
Mobile pgtype.Text `json:"mobile"`
Qualification pgtype.Text `json:"qualification"`
OccuptationalHealthExamination *time.Time `json:"occuptationalHealthExamination"`
PersonTransportCertificate *time.Time `json:"personTransportCertificate"`
}
type EmployeeDispoLiveSource struct {
InternalId pgtype.UUID `json:"internalId"`
InsertedAt time.Time `json:"insertedAt"`
JobId string `json:"jobId"`
ID pgtype.Text `json:"id"`
PScheinAblaufDatum pgtype.Text `json:"pScheinAblaufDatum"`
Name pgtype.Text `json:"name"`
Surname pgtype.Text `json:"surname"`
PersId pgtype.Text `json:"persId"`
ImportPersId pgtype.Text `json:"importPersId"`
Qualifikation pgtype.Text `json:"qualifikation"`
}
type EmployeeDispoLiveStaging struct {
InternalId pgtype.UUID `json:"internalId"`
ID string `json:"id"`
Name pgtype.Text `json:"name"`
Surname pgtype.Text `json:"surname"`
PersonnelNumber pgtype.Text `json:"personnelNumber"`
Qualification pgtype.Text `json:"qualification"`
PersonTransportCertificate *time.Time `json:"personTransportCertificate"`
}
type EmployeeDyflexisSource struct {
InternalId pgtype.UUID `json:"internalId"`
InsertedAt time.Time `json:"insertedAt"`
JobId string `json:"jobId"`
ID pgtype.Text `json:"id"`
Firstname pgtype.Text `json:"firstname"`
Surname pgtype.Text `json:"surname"`
ContractID pgtype.Text `json:"contract_id"`
ContractStart pgtype.Text `json:"contract_start"`
ContractEnd pgtype.Text `json:"contract_end"`
ContractHoursWeek pgtype.Text `json:"contract_hours_week"`
ContractSalaryHour pgtype.Text `json:"contract_salary_hour"`
ContractTypeName pgtype.Text `json:"contract_type_name"`
}
type EmployeeDyflexisStaging struct {
InternalId pgtype.UUID `json:"internalId"`
ID int32 `json:"id"`
Firstname string `json:"firstname"`
Surname string `json:"surname"`
ContractStart time.Time `json:"contractStart"`
ContractEnd time.Time `json:"contractEnd"`
ContractHoursWeek float64 `json:"contractHoursWeek"`
ContractSalaryHour float64 `json:"contractSalaryHour"`
ContractTypeName pgtype.Text `json:"contractTypeName"`
}
type Patient struct {
ID string `json:"id"`
Surname pgtype.Text `json:"surname"`
Name pgtype.Text `json:"name"`
Street pgtype.Text `json:"street"`
Zip pgtype.Text `json:"zip"`
City pgtype.Text `json:"city"`
Birthday *time.Time `json:"birthday"`
HealthinsuranceId pgtype.Text `json:"healthinsuranceId"`
Healthinsurance pgtype.Text `json:"healthinsurance"`
CareDegree pgtype.Text `json:"careDegree"`
DisabilityMark1 pgtype.Text `json:"disabilityMark1"`
DisabilityMark2 pgtype.Text `json:"disabilityMark2"`
}
type PatientSource struct {
InternalId pgtype.UUID `json:"internalId"`
InsertedAt time.Time `json:"insertedAt"`
JobId string `json:"jobId"`
ID pgtype.Text `json:"id"`
Surname pgtype.Text `json:"surname"`
Name pgtype.Text `json:"name"`
Street pgtype.Text `json:"street"`
Zip pgtype.Text `json:"zip"`
City pgtype.Text `json:"city"`
Birthday pgtype.Text `json:"birthday"`
KkId pgtype.Text `json:"kkId"`
Krankenkasse pgtype.Text `json:"krankenkasse"`
Pflegestufe pgtype.Text `json:"pflegestufe"`
Behinderung1 pgtype.Text `json:"behinderung1"`
Behinderung2 pgtype.Text `json:"behinderung2"`
}
type PatientStaging struct {
InternalId pgtype.UUID `json:"internalId"`
ID pgtype.Text `json:"id"`
Surname pgtype.Text `json:"surname"`
Name pgtype.Text `json:"name"`
Street pgtype.Text `json:"street"`
Zip pgtype.Text `json:"zip"`
City pgtype.Text `json:"city"`
Birthday *time.Time `json:"birthday"`
HealthinsuranceId pgtype.Text `json:"healthinsuranceId"`
Healthinsurance pgtype.Text `json:"healthinsurance"`
CareDegree pgtype.Text `json:"careDegree"`
DisabilityMark1 pgtype.Text `json:"disabilityMark1"`
DisabilityMark2 pgtype.Text `json:"disabilityMark2"`
}
type PlannedTime struct {
ID string `json:"id"`
UserId string `json:"userId"`
Firstname pgtype.Text `json:"firstname"`
Surname pgtype.Text `json:"surname"`
StartDate *time.Time `json:"startDate"`
EndDate *time.Time `json:"endDate"`
Department pgtype.Text `json:"department"`
Pause int32 `json:"pause"`
Duration int32 `json:"duration"`
Note pgtype.Text `json:"note"`
}
type PlannedTimeSource struct {
InternalId pgtype.UUID `json:"internalId"`
InsertedAt time.Time `json:"insertedAt"`
JobId string `json:"jobId"`
ID pgtype.Text `json:"id"`
UserID pgtype.Text `json:"user_id"`
Firstname pgtype.Text `json:"firstname"`
Surname pgtype.Text `json:"surname"`
DepartmentID pgtype.Text `json:"department_id"`
DepartmentName pgtype.Text `json:"department_name"`
StartDate pgtype.Text `json:"start_date"`
EndDate pgtype.Text `json:"end_date"`
Pauze pgtype.Text `json:"pauze"`
Duration pgtype.Text `json:"duration"`
Deleted pgtype.Text `json:"deleted"`
Mark pgtype.Text `json:"mark"`
}
type PlannedTimeStaging struct {
InternalId pgtype.UUID `json:"internalId"`
ID int32 `json:"id"`
EmployeeId int32 `json:"employeeId"`
Firstname pgtype.Text `json:"firstname"`
Surname pgtype.Text `json:"surname"`
DepartmentName pgtype.Text `json:"departmentName"`
StartDate time.Time `json:"startDate"`
EndDate time.Time `json:"endDate"`
Deleted bool `json:"deleted"`
Mark pgtype.Text `json:"mark"`
Duration int32 `json:"duration"`
Pause int32 `json:"pause"`
}
type PrismaMigration struct {
ID string `json:"id"`
Checksum string `json:"checksum"`
FinishedAt *time.Time `json:"finished_at"`
MigrationName string `json:"migration_name"`
Logs pgtype.Text `json:"logs"`
RolledBackAt *time.Time `json:"rolled_back_at"`
StartedAt time.Time `json:"started_at"`
AppliedStepsCount int32 `json:"applied_steps_count"`
}
type SolutionTarget struct {
ID pgtype.UUID `json:"id"`
Name string `json:"name"`
AnomalyId pgtype.Text `json:"anomalyId"`
}
type SolutionTargetStep struct {
ID pgtype.UUID `json:"id"`
Description string `json:"description"`
SolutionTargetId pgtype.UUID `json:"solutionTargetId"`
}
type Tariff struct {
ID pgtype.UUID `json:"id"`
ValidFrom *time.Time `json:"validFrom"`
ValidTo *time.Time `json:"validTo"`
TariffGroupId pgtype.UUID `json:"tariffGroupId"`
}
type TariffGroup struct {
ID pgtype.UUID `json:"id"`
Description string `json:"description"`
OrdinanceType string `json:"ordinanceType"`
}
type Ticket struct {
ID pgtype.UUID `json:"id"`
CurrentState TicketValidationState `json:"currentState"`
Notes []string `json:"notes"`
Errors []string `json:"errors"`
IsUrgent bool `json:"isUrgent"`
Urgency time.Time `json:"urgency"`
ApprovalState NullApprovalState `json:"approvalState"`
DocumentInfo []byte `json:"documentInfo"`
CreatedAt time.Time `json:"createdAt"`
}
type TicketStateHistory struct {
ID pgtype.UUID `json:"id"`
State TicketValidationState `json:"state"`
CreatedAt time.Time `json:"createdAt"`
TicketId pgtype.UUID `json:"ticketId"`
}
type Tour struct {
ID pgtype.UUID `json:"id"`
Done bool `json:"done"`
Direction string `json:"direction"`
CarName pgtype.Text `json:"carName"`
OperationId string `json:"operationId"`
DriverId pgtype.Text `json:"driverId"`
DriverName pgtype.Text `json:"driverName"`
CodriverId pgtype.Text `json:"codriverId"`
CodriverName pgtype.Text `json:"codriverName"`
PatientId pgtype.Text `json:"patientId"`
PatientName pgtype.Text `json:"patientName"`
PatientSurname pgtype.Text `json:"patientSurname"`
PatientStreet pgtype.Text `json:"patientStreet"`
PatientZip pgtype.Text `json:"patientZip"`
PatientCity pgtype.Text `json:"patientCity"`
HealthInsurance pgtype.Text `json:"healthInsurance"`
HealthInsuranceNumber pgtype.Text `json:"healthInsuranceNumber"`
Type pgtype.Text `json:"type"`
Category pgtype.Text `json:"category"`
TransportType pgtype.Text `json:"transportType"`
OrdinanceType pgtype.Text `json:"ordinanceType"`
RangeEndDate *time.Time `json:"rangeEndDate"`
StartInstitution pgtype.Text `json:"startInstitution"`
StartStreet pgtype.Text `json:"startStreet"`
StartZip pgtype.Text `json:"startZip"`
StartCity pgtype.Text `json:"startCity"`
TargetInstitution pgtype.Text `json:"targetInstitution"`
TargetStreet pgtype.Text `json:"targetStreet"`
TargetZip pgtype.Text `json:"targetZip"`
TargetCity pgtype.Text `json:"targetCity"`
StartDate time.Time `json:"startDate"`
Check int32 `json:"check"`
OccupiedKm pgtype.Float8 `json:"occupiedKm"`
TotalKm pgtype.Float8 `json:"totalKm"`
StartBegin pgtype.Text `json:"startBegin"`
StartEnd pgtype.Text `json:"startEnd"`
Target pgtype.Text `json:"target"`
TargetBegin pgtype.Text `json:"targetBegin"`
Empty pgtype.Text `json:"empty"`
HasInfection bool `json:"hasInfection"`
InfectionName pgtype.Text `json:"infectionName"`
RevenueDispoLive pgtype.Float8 `json:"revenueDispoLive"`
BillDate *time.Time `json:"billDate"`
BillNumber pgtype.Text `json:"billNumber"`
Revenue float64 `json:"revenue"`
ConsumptionCosts float64 `json:"consumptionCosts"`
RevenueDeviation float64 `json:"revenueDeviation"`
CreatedAt *time.Time `json:"createdAt"`
DeletedAt *time.Time `json:"deletedAt"`
TicketId pgtype.UUID `json:"ticketId"`
UpdatedAt *time.Time `json:"updatedAt"`
}
type TourSource struct {
InternalId pgtype.UUID `json:"internalId"`
InsertedAt time.Time `json:"insertedAt"`
JobId string `json:"jobId"`
ID pgtype.Text `json:"id"`
Check pgtype.Text `json:"check"`
CreateTime pgtype.Text `json:"createTime"`
EmpfangenVonId pgtype.Text `json:"empfangenVonId"`
Endgen pgtype.Text `json:"endgen"`
SerienId pgtype.Text `json:"serienId"`
SerienKategorName pgtype.Text `json:"serienKategorName"`
SerienKategorie pgtype.Text `json:"serienKategorie"`
ZadStatus pgtype.Text `json:"zadStatus"`
AbrDate pgtype.Text `json:"abrDate"`
AbrNum pgtype.Text `json:"abrNum"`
AbrTime pgtype.Text `json:"abrTime"`
AusrueckZeit pgtype.Text `json:"ausrueckZeit"`
StartTime pgtype.Text `json:"startTime"`
StartDate pgtype.Text `json:"startDate"`
Date pgtype.Text `json:"date"`
DateIso pgtype.Text `json:"date_iso"`
Dauergenehmigung pgtype.Text `json:"dauergenehmigung"`
Direction pgtype.Text `json:"direction"`
DistanceValue pgtype.Text `json:"distanceValue"`
BesetztKm pgtype.Text `json:"besetztKm"`
Gefahren pgtype.Text `json:"gefahren"`
GesamtPreis pgtype.Text `json:"gesamtPreis"`
Infektion pgtype.Text `json:"infektion"`
ItDesc pgtype.Text `json:"itDesc"`
ItKrz pgtype.Text `json:"itKrz"`
ItName pgtype.Text `json:"itName"`
KkId pgtype.Text `json:"kkId"`
Krankenkasse pgtype.Text `json:"krankenkasse"`
PatId pgtype.Text `json:"patId"`
PatName pgtype.Text `json:"patName"`
PatSurname pgtype.Text `json:"patSurname"`
PatStreet pgtype.Text `json:"patStreet"`
PatZip pgtype.Text `json:"patZip"`
PatCity pgtype.Text `json:"patCity"`
StartInstitution pgtype.Text `json:"startInstitution"`
StartStreet pgtype.Text `json:"startStreet"`
StartZip pgtype.Text `json:"startZip"`
StartCity pgtype.Text `json:"startCity"`
TargetInstitution pgtype.Text `json:"targetInstitution"`
TargetStreet pgtype.Text `json:"targetStreet"`
TargetZip pgtype.Text `json:"targetZip"`
TargetCity pgtype.Text `json:"targetCity"`
TransportKrz pgtype.Text `json:"transportKrz"`
TransportName pgtype.Text `json:"transportName"`
Transportart pgtype.Text `json:"transportart"`
VerordnungsId pgtype.Text `json:"verordnungsId"`
VerordnungsName pgtype.Text `json:"verordnungsName"`
Type pgtype.Text `json:"type"`
StartBegin pgtype.Text `json:"startBegin"`
StartEnd pgtype.Text `json:"startEnd"`
Frei pgtype.Text `json:"frei"`
Ziel pgtype.Text `json:"ziel"`
ZielBegin pgtype.Text `json:"zielBegin"`
EndTime pgtype.Text `json:"endTime"`
EndDate pgtype.Text `json:"endDate"`
EinsatzNummer pgtype.Text `json:"einsatzNummer"`
CarName pgtype.Text `json:"carName"`
CarNo pgtype.Text `json:"carNo"`
BeifahrerName pgtype.Text `json:"beifahrerName"`
Beifahrer pgtype.Text `json:"beifahrer"`
FahrerName pgtype.Text `json:"fahrerName"`
Fahrer pgtype.Text `json:"fahrer"`
Color pgtype.Text `json:"color"`
LastUpdate pgtype.Text `json:"lastUpdate"`
}
type TourStaging struct {
InternalId pgtype.UUID `json:"internalId"`
ID pgtype.Text `json:"id"`
Done pgtype.Bool `json:"done"`
Direction pgtype.Text `json:"direction"`
CarName pgtype.Text `json:"carName"`
OperationId pgtype.Text `json:"operationId"`
DriverId pgtype.Text `json:"driverId"`
DriverName pgtype.Text `json:"driverName"`
CodriverId pgtype.Text `json:"codriverId"`
CodriverName pgtype.Text `json:"codriverName"`
PatientId pgtype.Text `json:"patientId"`
PatientName pgtype.Text `json:"patientName"`
PatientSurname pgtype.Text `json:"patientSurname"`
PatientStreet pgtype.Text `json:"patientStreet"`
PatientZip pgtype.Text `json:"patientZip"`
PatientCity pgtype.Text `json:"patientCity"`
HealthInsurance pgtype.Text `json:"healthInsurance"`
HealthInsuranceNumber pgtype.Text `json:"healthInsuranceNumber"`
Type pgtype.Text `json:"type"`
Category pgtype.Text `json:"category"`
TransportType pgtype.Text `json:"transportType"`
OrdinanceType pgtype.Text `json:"ordinanceType"`
RangeEndDate *time.Time `json:"rangeEndDate"`
StartInstitution pgtype.Text `json:"startInstitution"`
StartStreet pgtype.Text `json:"startStreet"`
StartZip pgtype.Text `json:"startZip"`
StartCity pgtype.Text `json:"startCity"`
TargetInstitution pgtype.Text `json:"targetInstitution"`
TargetStreet pgtype.Text `json:"targetStreet"`
TargetZip pgtype.Text `json:"targetZip"`
TargetCity pgtype.Text `json:"targetCity"`
StartDate *time.Time `json:"startDate"`
Check int32 `json:"check"`
OccupiedKm pgtype.Float8 `json:"occupiedKm"`
TotalKm pgtype.Float8 `json:"totalKm"`
StartBegin pgtype.Text `json:"startBegin"`
StartEnd pgtype.Text `json:"startEnd"`
Target pgtype.Text `json:"target"`
TargetBegin pgtype.Text `json:"targetBegin"`
Empty pgtype.Text `json:"empty"`
HasInfection bool `json:"hasInfection"`
InfectionName pgtype.Text `json:"infectionName"`
RevenueDispoLive pgtype.Float8 `json:"revenueDispoLive"`
BillDate *time.Time `json:"billDate"`
BillNumber pgtype.Text `json:"billNumber"`
Revenue float64 `json:"revenue"`
RevenueDeviation float64 `json:"revenueDeviation"`
ConsumptionCosts float64 `json:"consumptionCosts"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
}
type Trace struct {
ID pgtype.UUID `json:"id"`
Type string `json:"type"`
EntityKey string `json:"entityKey"`
CreatedAt time.Time `json:"createdAt"`
}
type WorkTime struct {
ID string `json:"id"`
UserId string `json:"userId"`
Firstname string `json:"firstname"`
Surname string `json:"surname"`
StartDate *time.Time `json:"startDate"`
EndDate *time.Time `json:"endDate"`
Department pgtype.Text `json:"department"`
Pause int32 `json:"pause"`
Duration int32 `json:"duration"`
}
type WorkTimeSource struct {
InternalId pgtype.UUID `json:"internalId"`
InsertedAt time.Time `json:"insertedAt"`
JobId string `json:"jobId"`
ID pgtype.Text `json:"id"`
UserID pgtype.Text `json:"user_id"`
Firstname pgtype.Text `json:"firstname"`
Surname pgtype.Text `json:"surname"`
DepartmentID pgtype.Text `json:"department_id"`
DepartmentName pgtype.Text `json:"department_name"`
StartDate pgtype.Text `json:"start_date"`
EndDate pgtype.Text `json:"end_date"`
Pauze pgtype.Text `json:"pauze"`
Duration pgtype.Text `json:"duration"`
}
type WorkTimeStaging struct {
InternalId pgtype.UUID `json:"internalId"`
ID int32 `json:"id"`
EmployeeId int32 `json:"employeeId"`
Firstname string `json:"firstname"`
Surname string `json:"surname"`
DepartmentName pgtype.Text `json:"departmentName"`
StartDate time.Time `json:"startDate"`
EndDate time.Time `json:"endDate"`
Duration int32 `json:"duration"`
Pause int32 `json:"pause"`
}

View File

@ -0,0 +1,490 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: payroll.sql
package database
import (
"context"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
const countTourengeldAmountInPeriod = `-- name: CountTourengeldAmountInPeriod :one
WITH ToursInPeriod AS (
SELECT
(("startDate" AT TIME ZONE 'UTC') AT TIME ZONE 'Europe/Berlin')::DATE as day,
"operationId",
"occupiedKm",
CASE
WHEN "occupiedKm" <= 30 THEN 1.0
ELSE ROUND("occupiedKm" / 30.0)
END AS tour_value,
CASE
WHEN EXTRACT(ISODOW FROM "startDate") = 6 AND "occupiedKm" <= 30 THEN 0.5
ELSE 0.0
END AS weekend_bonus,
"infectionName",
CASE
WHEN "infectionName" = 'Covid-19' THEN 0.5
ELSE 0.0
END AS infection_bonus
FROM
"Tour"
WHERE
"check" <> 3
AND "deletedAt" IS NULL
AND (
"patientId" IS NULL
OR "patientId" NOT IN (
'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
'3554e1a9-9c07-45e5-b53c-692a77e4fa9a'
)
)
-- ID's for Pause and Feierabend respectively
AND (
"carName" IS NULL
OR "carName" NOT IN (
'Storno',
'Abgabe',
'Löschen'
)
AND "carName" LIKE '%KTW%'
)
AND "startDate" >= $1
AND "startDate" <= $2
AND ("driverId" ILIKE $3 OR "codriverId" ILIKE $3)
ORDER BY
"day"
),
DailyCounts AS (
SELECT
day,
SUM(tour_value) + SUM(weekend_bonus) + SUM(infection_bonus) AS tour_amount
FROM
ToursInPeriod
GROUP BY
day
),
HighActivityDays AS (
SELECT
day,
GREATEST(0, tour_amount - 6) AS tourengeld
FROM
DailyCounts
)
SELECT
SUM(tourengeld)::float as tourengeld
FROM
HighActivityDays
`
type CountTourengeldAmountInPeriodParams struct {
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
EmployeeID pgtype.Text `json:"employee_id"`
}
func (q *Queries) CountTourengeldAmountInPeriod(ctx context.Context, arg CountTourengeldAmountInPeriodParams) (float64, error) {
row := q.db.QueryRow(ctx, countTourengeldAmountInPeriod, arg.StartDate, arg.EndDate, arg.EmployeeID)
var tourengeld float64
err := row.Scan(&tourengeld)
return tourengeld, err
}
const fetchDrivenToursInInterval = `-- name: FetchDrivenToursInInterval :many
SELECT id, done, direction, "carName", "operationId", "driverId", "driverName", "codriverId", "codriverName", "patientId", "patientName", "patientSurname", "patientStreet", "patientZip", "patientCity", "healthInsurance", "healthInsuranceNumber", type, category, "transportType", "ordinanceType", "rangeEndDate", "startInstitution", "startStreet", "startZip", "startCity", "targetInstitution", "targetStreet", "targetZip", "targetCity", "startDate", "check", "occupiedKm", "totalKm", "startBegin", "startEnd", target, "targetBegin", empty, "hasInfection", "infectionName", "revenueDispoLive", "billDate", "billNumber", revenue, "consumptionCosts", "revenueDeviation", "createdAt", "deletedAt", "ticketId", "updatedAt" FROM "Tour"
WHERE "check" <> 3
AND "deletedAt" IS NULL
AND (
"patientId" IS NULL
OR "patientId" NOT IN (
'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
'3554e1a9-9c07-45e5-b53c-692a77e4fa9a'
)
)
AND (
"carName" IS NULL
OR "carName" NOT IN (
'Storno',
'Abgabe',
'Löschen'
)
)
AND "startDate" >= $1
AND "startDate" <= $2
AND ("driverId" = $3 OR "codriverId" = $3)
ORDER BY "startDate" DESC
`
type FetchDrivenToursInIntervalParams struct {
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
EmployeeID pgtype.Text `json:"employee_id"`
}
// ID's for Pause and Feierabend respectively
func (q *Queries) FetchDrivenToursInInterval(ctx context.Context, arg FetchDrivenToursInIntervalParams) ([]Tour, error) {
rows, err := q.db.Query(ctx, fetchDrivenToursInInterval, arg.StartDate, arg.EndDate, arg.EmployeeID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Tour
for rows.Next() {
var i Tour
if err := rows.Scan(
&i.ID,
&i.Done,
&i.Direction,
&i.CarName,
&i.OperationId,
&i.DriverId,
&i.DriverName,
&i.CodriverId,
&i.CodriverName,
&i.PatientId,
&i.PatientName,
&i.PatientSurname,
&i.PatientStreet,
&i.PatientZip,
&i.PatientCity,
&i.HealthInsurance,
&i.HealthInsuranceNumber,
&i.Type,
&i.Category,
&i.TransportType,
&i.OrdinanceType,
&i.RangeEndDate,
&i.StartInstitution,
&i.StartStreet,
&i.StartZip,
&i.StartCity,
&i.TargetInstitution,
&i.TargetStreet,
&i.TargetZip,
&i.TargetCity,
&i.StartDate,
&i.Check,
&i.OccupiedKm,
&i.TotalKm,
&i.StartBegin,
&i.StartEnd,
&i.Target,
&i.TargetBegin,
&i.Empty,
&i.HasInfection,
&i.InfectionName,
&i.RevenueDispoLive,
&i.BillDate,
&i.BillNumber,
&i.Revenue,
&i.ConsumptionCosts,
&i.RevenueDeviation,
&i.CreatedAt,
&i.DeletedAt,
&i.TicketId,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getEmployeeByName = `-- name: GetEmployeeByName :one
SELECT
dy.id as dyflexis_id,
dl.id as dispolive_id,
dy."contractSalaryHour"
FROM "DyflexisEmployee" as dy
INNER JOIN
"Employee" as dl ON dy.id::text = dl."personnelNumber"
WHERE
TRIM(dy.firstname) = $1 AND
TRIM(dy.surname) = $2 AND
"contractStart" < CURRENT_DATE AND
"contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
ORDER BY dy."contractEnd" DESC
`
type GetEmployeeByNameParams struct {
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
}
type GetEmployeeByNameRow struct {
DyflexisID int32 `json:"dyflexis_id"`
DispoliveID string `json:"dispolive_id"`
ContractSalaryHour float64 `json:"contractSalaryHour"`
}
func (q *Queries) GetEmployeeByName(ctx context.Context, arg GetEmployeeByNameParams) (GetEmployeeByNameRow, error) {
row := q.db.QueryRow(ctx, getEmployeeByName, arg.Firstname, arg.Lastname)
var i GetEmployeeByNameRow
err := row.Scan(&i.DyflexisID, &i.DispoliveID, &i.ContractSalaryHour)
return i, err
}
const getEmployeeDyflexis = `-- name: GetEmployeeDyflexis :one
SELECT id, firstname, surname, "contractStart", "contractEnd", "contractHoursWeek", "contractSalaryHour", "contractTypeName", "internalId" FROM "DyflexisEmployee"
WHERE
id = $1 AND
"contractStart" < CURRENT_DATE AND
"contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
LIMIT 1
`
func (q *Queries) GetEmployeeDyflexis(ctx context.Context, id int32) (DyflexisEmployee, error) {
row := q.db.QueryRow(ctx, getEmployeeDyflexis, id)
var i DyflexisEmployee
err := row.Scan(
&i.ID,
&i.Firstname,
&i.Surname,
&i.ContractStart,
&i.ContractEnd,
&i.ContractHoursWeek,
&i.ContractSalaryHour,
&i.ContractTypeName,
&i.InternalId,
)
return i, err
}
const getSicknessSummaryForEmployeeInPeriod = `-- name: GetSicknessSummaryForEmployeeInPeriod :one
WITH pt AS (
SELECT
COUNT(*) as days,
COALESCE(sum(duration), 0)::bigint as minutes
FROM "PlannedTime"
WHERE "userId" = $1 AND "startDate" > $2 AND "startDate" < $3 AND note = 'sick'
)
SELECT
pt.days,
(minutes / 60)::float as hours,
pt.minutes
FROM pt
`
type GetSicknessSummaryForEmployeeInPeriodParams struct {
EmployeeID string `json:"employee_id"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
}
type GetSicknessSummaryForEmployeeInPeriodRow struct {
Days int64 `json:"days"`
Hours float64 `json:"hours"`
Minutes int64 `json:"minutes"`
}
func (q *Queries) GetSicknessSummaryForEmployeeInPeriod(ctx context.Context, arg GetSicknessSummaryForEmployeeInPeriodParams) (GetSicknessSummaryForEmployeeInPeriodRow, error) {
row := q.db.QueryRow(ctx, getSicknessSummaryForEmployeeInPeriod, arg.EmployeeID, arg.StartDate, arg.EndDate)
var i GetSicknessSummaryForEmployeeInPeriodRow
err := row.Scan(&i.Days, &i.Hours, &i.Minutes)
return i, err
}
const getWorkSummaryForEmployeeInPeriod = `-- name: GetWorkSummaryForEmployeeInPeriod :one
WITH wt AS (
SELECT
COUNT(*) as days,
COALESCE(sum(duration), 0)::bigint as minutes
FROM "WorkTime"
WHERE "userId" = $1 AND "startDate" > $2 AND "startDate" < $3
)
SELECT
wt.days,
(minutes / 60.0)::float as hours,
wt.minutes
FROM wt
`
type GetWorkSummaryForEmployeeInPeriodParams struct {
EmployeeID string `json:"employee_id"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
}
type GetWorkSummaryForEmployeeInPeriodRow struct {
Days int64 `json:"days"`
Hours float64 `json:"hours"`
Minutes int64 `json:"minutes"`
}
func (q *Queries) GetWorkSummaryForEmployeeInPeriod(ctx context.Context, arg GetWorkSummaryForEmployeeInPeriodParams) (GetWorkSummaryForEmployeeInPeriodRow, error) {
row := q.db.QueryRow(ctx, getWorkSummaryForEmployeeInPeriod, arg.EmployeeID, arg.StartDate, arg.EndDate)
var i GetWorkSummaryForEmployeeInPeriodRow
err := row.Scan(&i.Days, &i.Hours, &i.Minutes)
return i, err
}
const listActiveEmployees = `-- name: ListActiveEmployees :many
SELECT
de.id,
de.firstname,
de.surname,
de."contractStart",
de."contractEnd",
de."contractHoursWeek",
de."contractSalaryHour",
de."contractTypeName",
e.id as "dlId",
e."personTransportCertificate"
FROM "DyflexisEmployee" as de
INNER JOIN "Employee" as e ON de.id::text = e."personnelNumber"
WHERE de."contractStart" < CURRENT_DATE AND de."contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
`
type ListActiveEmployeesRow struct {
ID int32 `json:"id"`
Firstname string `json:"firstname"`
Surname string `json:"surname"`
ContractStart time.Time `json:"contractStart"`
ContractEnd *time.Time `json:"contractEnd"`
ContractHoursWeek float64 `json:"contractHoursWeek"`
ContractSalaryHour float64 `json:"contractSalaryHour"`
ContractTypeName pgtype.Text `json:"contractTypeName"`
DlId string `json:"dlId"`
PersonTransportCertificate *time.Time `json:"personTransportCertificate"`
}
func (q *Queries) ListActiveEmployees(ctx context.Context) ([]ListActiveEmployeesRow, error) {
rows, err := q.db.Query(ctx, listActiveEmployees)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListActiveEmployeesRow
for rows.Next() {
var i ListActiveEmployeesRow
if err := rows.Scan(
&i.ID,
&i.Firstname,
&i.Surname,
&i.ContractStart,
&i.ContractEnd,
&i.ContractHoursWeek,
&i.ContractSalaryHour,
&i.ContractTypeName,
&i.DlId,
&i.PersonTransportCertificate,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPlannedTimesForEmployeeInPeriod = `-- name: ListPlannedTimesForEmployeeInPeriod :many
SELECT
DISTINCT ON (day)
"startDate"::DATE as day,
"startDate",
"endDate",
department,
pause,
duration
FROM "PlannedTime"
WHERE
"userId" = $1 AND
"startDate" > $2 AND
"startDate" < $3 AND
department <> 'Fortbildung' AND
note IS NULL
ORDER BY "day"
`
type ListPlannedTimesForEmployeeInPeriodParams struct {
EmployeeID string `json:"employee_id"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
}
type ListPlannedTimesForEmployeeInPeriodRow struct {
Day pgtype.Date `json:"day"`
StartDate *time.Time `json:"startDate"`
EndDate *time.Time `json:"endDate"`
Department pgtype.Text `json:"department"`
Pause int32 `json:"pause"`
Duration int32 `json:"duration"`
}
func (q *Queries) ListPlannedTimesForEmployeeInPeriod(ctx context.Context, arg ListPlannedTimesForEmployeeInPeriodParams) ([]ListPlannedTimesForEmployeeInPeriodRow, error) {
rows, err := q.db.Query(ctx, listPlannedTimesForEmployeeInPeriod, arg.EmployeeID, arg.StartDate, arg.EndDate)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListPlannedTimesForEmployeeInPeriodRow
for rows.Next() {
var i ListPlannedTimesForEmployeeInPeriodRow
if err := rows.Scan(
&i.Day,
&i.StartDate,
&i.EndDate,
&i.Department,
&i.Pause,
&i.Duration,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listWorkTimesForEmployeeInPeriod = `-- name: ListWorkTimesForEmployeeInPeriod :many
SELECT id, "userId", firstname, surname, "startDate", "endDate", department, pause, duration FROM "WorkTime"
WHERE "userId" = $1 AND "startDate" > $2 AND "startDate" < $3
`
type ListWorkTimesForEmployeeInPeriodParams struct {
EmployeeID string `json:"employee_id"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
}
func (q *Queries) ListWorkTimesForEmployeeInPeriod(ctx context.Context, arg ListWorkTimesForEmployeeInPeriodParams) ([]WorkTime, error) {
rows, err := q.db.Query(ctx, listWorkTimesForEmployeeInPeriod, arg.EmployeeID, arg.StartDate, arg.EndDate)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkTime
for rows.Next() {
var i WorkTime
if err := rows.Scan(
&i.ID,
&i.UserId,
&i.Firstname,
&i.Surname,
&i.StartDate,
&i.EndDate,
&i.Department,
&i.Pause,
&i.Duration,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -0,0 +1,122 @@
package excelread
import (
"business-engine/internal/database"
"fmt"
"os"
"slices"
"strings"
"time"
"github.com/gocarina/gocsv"
)
func sortSliceByDate[T any](slice []T, getDate func(T) time.Time) {
compare := func(a, b T) int {
aDate := getDate(a)
bDate := getDate(b)
if aDate.Before(bDate) {
return -1
}
if aDate.After(bDate) {
return 1
}
return 0
}
slices.SortFunc(slice, compare)
}
// Zu kalkulieren
// - Puenktlichkeitspraemie
// - Tourengeld Anzahl
// - Tourengeld Bonus
func GeneratePayrollStatement(data EmployeeDataRow, input PayrollInputData) PayrollStatement {
isKTW := slices.IndexFunc(input.Plannedtimes, func(pt database.ListPlannedTimesForEmployeeInPeriodRow) bool {
return strings.Contains(pt.Department.String, "KTW")
}) >= 0 && slices.IndexFunc(input.Plannedtimes, func(pt database.ListPlannedTimesForEmployeeInPeriodRow) bool {
return strings.Contains(pt.Department.String, "Leitstelle")
}) == -1
statement := PayrollStatement{
Nachname: data.Nachname,
Vorname: data.Vorname,
Verpflegungspauschale: data.AuszahlenVerpflegungspauschaleTage,
VertragStundenlohn: input.ContractSalaryHour,
Vertragsstunden: data.IstSollVertragsStunden,
GearbeiteteStunden: data.GearbeiteteStunden,
GearbeiteteTage: data.GearbeiteteTage,
Krankheitstage: data.Krankheitstage,
Ueberstunden: data.IstSollPlusMinusStunden,
Wochenendzuschlag15: data.AuszahlenZuschlag15Stunden,
Sonntagszuschlag50: data.AuszahlenZuschlag50Stunden,
Nachtzuschlag25: data.AuszahlenZuschlag25Stunden,
Nachtzuschlag40: data.AuszahlenZuschlag40Stunden,
}
if data.Krankheitstage > 0 || !isKTW {
statement.GesundheitsbonusAbzug = "ja"
}
if isKTW && data.GenommeneUrlaubstage > 0 {
statement.Urlaubsgeld = data.GenommeneUrlaubstage
}
if isKTW && data.AuszahlenAnzahlPSchein > 0 {
statement.PScheinZulage = data.AuszahlenAnzahlPSchein
}
if isKTW && input.Tourengeld > 0 {
statement.Tourengeld = float64(input.Tourengeld)
}
if isKTW && len(input.Worktimes) > 0 && len(input.Plannedtimes) > 0 {
ptGetStartDate := func(pt database.ListPlannedTimesForEmployeeInPeriodRow) time.Time {
return *pt.StartDate
}
wtGetStartDate := func(pt database.WorkTime) time.Time {
return *pt.StartDate
}
sortSliceByDate(input.Plannedtimes, ptGetStartDate)
sortSliceByDate(input.Worktimes, wtGetStartDate)
var employeeOnTime []EmployeeOnTime
for _, pt := range input.Plannedtimes {
for _, wt := range input.Worktimes {
if isSameDay(ptGetStartDate(pt), wtGetStartDate(wt)) && (wtGetStartDate(wt).Before(ptGetStartDate(pt)) || wtGetStartDate(wt) == ptGetStartDate(pt)) {
statement.PuenktlichkeitspraemieAnzahl++
employeeOnTime = append(employeeOnTime, EmployeeOnTime{
EmployeeId: fmt.Sprintf("%s %s", data.Vorname, data.Nachname),
WorkTime: *wt.StartDate,
PlannedTime: *pt.StartDate,
IsOnTime: true,
})
}
}
}
if data.Vorname == "David" && data.Nachname == "Hastedt" {
GenerateEmployeesOnTime(employeeOnTime)
}
}
return statement
}
func GenerateEmployeesOnTime(employeesontime []EmployeeOnTime) error {
file, err := os.Create("employees_on_time.csv")
if err != nil {
return err
}
defer file.Close()
if err := gocsv.MarshalFile(&employeesontime, file); err != nil {
return err
}
return nil
}
// isSameDay prüft, ob zwei Zeitpunkte am selben Kalendertag liegen.
func isSameDay(t1, t2 time.Time) bool {
return t1.Year() == t2.Year() && t1.Month() == t2.Month() && t1.Day() == t2.Day()
}

View File

@ -0,0 +1,210 @@
package excelread
import (
"encoding/csv"
"fmt"
"log"
"os"
"strconv"
"strings"
)
// mapRowToStruct konvertiert eine einzelne CSV-Zeile (als []string)
// in die EmployeeDataRow-Struct.
func mapRowToStruct(row []string) EmployeeDataRow {
data := EmployeeDataRow{}
data.Personalnummer = parseInt(row[0])
data.Nachname = strings.TrimSpace(row[1])
data.Vorname = strings.TrimSpace(row[2])
data.Vertragsart = strings.TrimSpace(row[3])
data.HatPersonenbefoerderungsschein = strings.TrimSpace(row[4])
data.HatRechtAufZuschlaege = strings.TrimSpace(row[5])
data.Vertragsstart = strings.TrimSpace(row[6])
data.VertragstageProWoche = parseFloat(row[7])
data.VertragsstundenProWoche = parseFloat(row[8])
data.VertragsstundenMax = strings.TrimSpace(row[9])
data.UrlaubstageProJahr = parseFloat(row[10])
data.GearbeiteteTage = parseFloat(row[11])
data.GearbeiteteStunden = parseFloat(row[12])
data.GenommeneUrlaubstage = parseFloat(row[13])
data.GenommeneUrlaubsstunden = parseFloat(row[14])
data.Krankheitstage = parseFloat(row[15])
data.Krankheitsstunden = parseFloat(row[16])
// data.KrankengeldTage = parseFloat(row[17])
// data.KrankengeldStunden = parseFloat(row[18])
// data.UnbezahlterUrlaubTage = parseFloat(row[19])
// data.UnbezahlterUrlaubStunden = parseFloat(row[20])
data.IstSollGesamtTage = parseFloat(row[17])
data.IstSollGesamtStunden = parseFloat(row[18])
data.IstSollVertragsTage = parseFloat(row[19])
data.IstSollVertragsStunden = parseFloat(row[20])
data.IstSollPlusMinusStunden = parseFloat(row[21])
data.BerechnungMehrstunden = parseFloat(row[22])
data.BerechnungUeberstunden = parseFloat(row[23])
data.BerechnungAnzahlPSchein = parseFloat(row[24])
data.BerechnungKrankOhneAUTage = parseFloat(row[25])
data.BerechnungKrankOhneAUStunden = parseFloat(row[26])
data.BerechnungNachtzuschlag25Tage = parseFloat(row[27])
data.BerechnungNachtzuschlag25Stunden = parseFloat(row[28])
data.BerechnungNachtzuschlag40Tage = parseFloat(row[29])
data.BerechnungNachtzuschlag40Stunden = parseFloat(row[30])
data.BerechnungSonntagszuschlag50Tage = parseFloat(row[31])
data.BerechnungSonntagszuschlag50Stunden = parseFloat(row[32])
data.BerechnungVerpflegungspauschale14Tage = parseFloat(row[33])
data.BerechnungSamstagszuschlag15Tage = parseFloat(row[34])
data.BerechnungSamstagszuschlag15Stunden = parseFloat(row[35])
data.BerechnungZeitausgleichStart = parseFloat(row[36])
data.BerechnungZeitausgleichPlusMinus = parseFloat(row[37])
data.BerechnungZeitausgleichAenderungen = parseFloat(row[38])
data.BerechnungZeitausgleichAuszahlen = parseFloat(row[39])
data.BerechnungZeitausgleichJetzt = parseFloat(row[40])
data.SaldenUrlaubStart = parseFloat(row[41])
data.SaldenUrlaubAbbau = parseFloat(row[42])
data.SaldenUrlaubKorrektur = parseFloat(row[43])
data.SaldenUrlaubJetzt = parseFloat(row[44])
data.SaldenMehrstunden = parseFloat(row[45])
data.SaldenUeberstunden = parseFloat(row[46])
data.AuszahlenFahrtkostenKm = parseFloat(row[47])
data.AuszahlenVerpflegungspauschaleTage = parseFloat(row[48])
data.AuszahlenAnzahlPSchein = parseFloat(row[49])
data.AuszahlenZuschlag25Stunden = parseFloat(row[50])
data.AuszahlenZuschlag40Stunden = parseFloat(row[51])
data.AuszahlenZuschlag50Stunden = parseFloat(row[52])
data.AuszahlenZuschlag14Stunden = parseFloat(row[53])
data.AuszahlenZuschlag15Stunden = parseFloat(row[54])
return data
}
// map from: July 2025
// func mapRowToStruct(row []string) EmployeeDataRow {
// data := EmployeeDataRow{}
// data.Personalnummer = parseInt(row[0])
// data.Nachname = strings.TrimSpace(row[1])
// data.Vorname = strings.TrimSpace(row[2])
// data.Vertragsart = strings.TrimSpace(row[3])
// data.HatPersonenbefoerderungsschein = strings.TrimSpace(row[4])
// data.HatRechtAufZuschlaege = strings.TrimSpace(row[5])
// data.Vertragsstart = strings.TrimSpace(row[6])
// data.VertragstageProWoche = parseFloat(row[7])
// data.VertragsstundenProWoche = parseFloat(row[8])
// data.VertragsstundenMax = strings.TrimSpace(row[9])
// data.UrlaubstageProJahr = parseFloat(row[10])
// data.GearbeiteteTage = parseFloat(row[11])
// data.GearbeiteteStunden = parseFloat(row[12])
// data.GenommeneUrlaubstage = parseFloat(row[13])
// data.GenommeneUrlaubsstunden = parseFloat(row[14])
// data.Krankheitstage = parseFloat(row[15])
// data.Krankheitsstunden = parseFloat(row[16])
// data.KrankengeldTage = parseFloat(row[17])
// data.KrankengeldStunden = parseFloat(row[18])
// data.UnbezahlterUrlaubTage = parseFloat(row[19])
// data.UnbezahlterUrlaubStunden = parseFloat(row[20])
// data.IstSollGesamtTage = parseFloat(row[21])
// data.IstSollGesamtStunden = parseFloat(row[22])
// data.IstSollVertragsTage = parseFloat(row[23])
// data.IstSollVertragsStunden = parseFloat(row[24])
// data.IstSollPlusMinusStunden = parseFloat(row[25])
// data.BerechnungMehrstunden = parseFloat(row[26])
// data.BerechnungUeberstunden = parseFloat(row[27])
// data.BerechnungAnzahlPSchein = parseFloat(row[28])
// data.BerechnungKrankOhneAUTage = parseFloat(row[29])
// data.BerechnungKrankOhneAUStunden = parseFloat(row[30])
// data.BerechnungNachtzuschlag25Tage = parseFloat(row[31])
// data.BerechnungNachtzuschlag25Stunden = parseFloat(row[32])
// data.BerechnungNachtzuschlag40Tage = parseFloat(row[33])
// data.BerechnungNachtzuschlag40Stunden = parseFloat(row[34])
// data.BerechnungSonntagszuschlag50Tage = parseFloat(row[35])
// data.BerechnungSonntagszuschlag50Stunden = parseFloat(row[36])
// data.BerechnungVerpflegungspauschale14Tage = parseFloat(row[37])
// data.BerechnungSamstagszuschlag15Tage = parseFloat(row[38])
// data.BerechnungSamstagszuschlag15Stunden = parseFloat(row[39])
// data.BerechnungZeitausgleichStart = parseFloat(row[40])
// data.BerechnungZeitausgleichPlusMinus = parseFloat(row[41])
// data.BerechnungZeitausgleichAenderungen = parseFloat(row[42])
// data.BerechnungZeitausgleichAuszahlen = parseFloat(row[43])
// data.BerechnungZeitausgleichJetzt = parseFloat(row[44])
// data.SaldenUrlaubStart = parseFloat(row[45])
// data.SaldenUrlaubAbbau = parseFloat(row[46])
// data.SaldenUrlaubJetzt = parseFloat(row[47])
// data.SaldenMehrstunden = parseFloat(row[48])
// data.SaldenUeberstunden = parseFloat(row[49])
// data.AuszahlenFahrtkostenKm = parseFloat(row[50])
// data.AuszahlenVerpflegungspauschaleTage = parseFloat(row[51])
// data.AuszahlenAnzahlPSchein = parseFloat(row[52])
// data.AuszahlenZuschlag25Stunden = parseFloat(row[53])
// data.AuszahlenZuschlag40Stunden = parseFloat(row[54])
// data.AuszahlenZuschlag50Stunden = parseFloat(row[55])
// data.AuszahlenZuschlag14Stunden = parseFloat(row[56])
// data.AuszahlenZuschlag15Stunden = parseFloat(row[57])
// return data
// }
func ReadEmployeeData() []EmployeeDataRow {
content, err := os.ReadFile("daten.csv")
if err != nil {
log.Fatalf("Fehler beim Lesen der Datei: %v", err)
}
reader := csv.NewReader(strings.NewReader(string(content)))
reader.Comma = ';'
reader.FieldsPerRecord = -1
records, err := reader.ReadAll()
if err != nil {
log.Fatalf("Fehler beim Parsen der CSV: %v", err)
}
if len(records) < 5 {
log.Fatal("Die CSV-Datei hat nicht genügend Zeilen.")
}
dataRows := records[3 : len(records)-1]
var employees []EmployeeDataRow
for i, row := range dataRows {
fmt.Printf("%v", len(row))
if row[1] != "Shahrasebi" {
employee := mapRowToStruct(row)
employees = append(employees, employee)
fmt.Printf("Zeile %d geladen: %s, %s\n", i+4, employee.Nachname, employee.Vorname)
}
}
fmt.Printf("\n--- Insgesamt %d Mitarbeiterdatensätze erfolgreich geladen. ---\n", len(employees))
return employees
}
// Helfer-Funktion, um Strings mit Komma als Dezimaltrennzeichen in float64 umzuwandeln.
func parseFloat(s string) float64 {
s = strings.TrimSpace(s)
if s == "" {
return 0.0
}
// Ersetze das Komma durch einen Punkt für die Standard-Konvertierung
s = strings.Replace(s, ",", ".", 1)
val, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0.0 // Bei Fehler einfach 0 zurückgeben
}
return val
}
// Helfer-Funktion, um einen String in einen Integer umzuwandeln.
func parseInt(s string) int {
s = strings.TrimSpace(s)
if s == "" {
return 0
}
val, err := strconv.Atoi(s)
if err != nil {
return 0
}
return val
}

View File

@ -0,0 +1,131 @@
package excelread
import (
"business-engine/internal/database"
"fmt"
"time"
)
// EmployeeDataRow repräsentiert eine einzelne, vollständige Datenzeile aus der CSV.
type EmployeeDataRow struct {
Personalnummer int `json:"personalnummer"`
Nachname string `json:"nachname"`
Vorname string `json:"vorname"`
Vertragsart string `json:"vertragsart"`
HatPersonenbefoerderungsschein string `json:"hatPersonenbefoerderungsschein"`
HatRechtAufZuschlaege string `json:"hatRechtAufZuschlaege"`
Vertragsstart string `json:"vertragsstart"`
VertragstageProWoche float64 `json:"vertragstageProWoche"`
VertragsstundenProWoche float64 `json:"vertragsstundenProWoche"`
VertragsstundenMax string `json:"vertragsstundenMax"`
UrlaubstageProJahr float64 `json:"urlaubstageProJahr"`
GearbeiteteTage float64 `json:"gearbeiteteTage"`
GearbeiteteStunden float64 `json:"gearbeiteteStunden"`
GenommeneUrlaubstage float64 `json:"genommeneUrlaubstage"`
GenommeneUrlaubsstunden float64 `json:"genommeneUrlaubsstunden"`
Krankheitstage float64 `json:"krankheitstage"`
Krankheitsstunden float64 `json:"krankheitsstunden"`
KrankengeldTage float64 `json:"krankengeldTage"`
KrankengeldStunden float64 `json:"krankengeldStunden"`
UnbezahlterUrlaubTage float64 `json:"unbezahlterUrlaubTage"`
UnbezahlterUrlaubStunden float64 `json:"unbezahlterUrlaubStunden"`
EinbehaltungGesamtTage float64 `json:"einbehaltungGesamtTage"`
EinbehaltungGesamtStunden float64 `json:"einbehaltungGesamtStunden"`
IstSollGesamtTage float64 `json:"istSollGesamtTage"`
IstSollGesamtStunden float64 `json:"istSollGesamtStunden"`
IstSollVertragsTage float64 `json:"istSollVertragsTage"`
IstSollVertragsStunden float64 `json:"istSollVertragsStunden"`
IstSollPlusMinusStunden float64 `json:"istSollPlusMinusStunden"`
BerechnungMehrstunden float64 `json:"berechnungMehrstunden"`
BerechnungUeberstunden float64 `json:"berechnungUeberstunden"`
BerechnungAnzahlPSchein float64 `json:"berechnungAnzahlPSchein"`
BerechnungKrankOhneAUTage float64 `json:"berechnungKrankOhneAUTage"`
BerechnungKrankOhneAUStunden float64 `json:"berechnungKrankOhneAUStunden"`
BerechnungNachtzuschlag25Tage float64 `json:"berechnungNachtzuschlag25Tage"`
BerechnungNachtzuschlag25Stunden float64 `json:"berechnungNachtzuschlag25Stunden"`
BerechnungNachtzuschlag40Tage float64 `json:"berechnungNachtzuschlag40Tage"`
BerechnungNachtzuschlag40Stunden float64 `json:"berechnungNachtzuschlag40Stunden"`
BerechnungSonntagszuschlag50Tage float64 `json:"berechnungSonntagszuschlag50Tage"`
BerechnungSonntagszuschlag50Stunden float64 `json:"berechnungSonntagszuschlag50Stunden"`
BerechnungVerpflegungspauschale14Tage float64 `json:"berechnungVerpflegungspauschale14Tage"`
BerechnungSamstagszuschlag15Tage float64 `json:"berechnungSamstagszuschlag15Tage"`
BerechnungSamstagszuschlag15Stunden float64 `json:"berechnungSamstagszuschlag15Stunden"`
BerechnungZeitausgleichStart float64 `json:"berechnungZeitausgleichStart"`
BerechnungZeitausgleichPlusMinus float64 `json:"berechnungZeitausgleichPlusMinus"`
BerechnungZeitausgleichAenderungen float64 `json:"berechnungZeitausgleichAenderungen"`
BerechnungZeitausgleichAuszahlen float64 `json:"berechnungZeitausgleichAuszahlen"`
BerechnungZeitausgleichJetzt float64 `json:"berechnungZeitausgleichJetzt"`
SaldenUrlaubStart float64 `json:"saldenUrlaubStart"`
SaldenUrlaubAbbau float64 `json:"saldenUrlaubAbbau"`
SaldenUrlaubKorrektur float64 `json:"saldenUrlaubKorrektur"`
SaldenUrlaubJetzt float64 `json:"saldenUrlaubJetzt"`
SaldenMehrstunden float64 `json:"saldenMehrstunden"`
SaldenUeberstunden float64 `json:"saldenUeberstunden"`
AuszahlenFahrtkostenKm float64 `json:"auszahlenFahrtkostenKm"`
AuszahlenVerpflegungspauschaleTage float64 `json:"auszahlenVerpflegungspauschaleTage"`
AuszahlenAnzahlPSchein float64 `json:"auszahlenAnzahlPSchein"`
AuszahlenZuschlag25Stunden float64 `json:"auszahlenZuschlag25Stunden"`
AuszahlenZuschlag40Stunden float64 `json:"auszahlenZuschlag40Stunden"`
AuszahlenZuschlag50Stunden float64 `json:"auszahlenZuschlag50Stunden"`
AuszahlenZuschlag14Stunden float64 `json:"auszahlenZuschlag14Stunden"`
AuszahlenZuschlag15Stunden float64 `json:"auszahlenZuschlag15Stunden"`
}
type PayrollInputData struct {
ContractSalaryHour float64
Tourengeld float64
Worktimes []database.WorkTime
Plannedtimes []database.ListPlannedTimesForEmployeeInPeriodRow
}
func NewPayrollInputData(salaryHour float64, tourengeld float64, worktimes []database.WorkTime, plannedtimes []database.ListPlannedTimesForEmployeeInPeriodRow) PayrollInputData {
return PayrollInputData{
ContractSalaryHour: salaryHour,
Tourengeld: tourengeld,
Worktimes: worktimes,
Plannedtimes: plannedtimes,
}
}
type PayrollStatement struct {
Nachname string `json:"nachname" csv:"Nachname"`
Vorname string `json:"vorname" csv:"Vorname"`
Verpflegungspauschale float64 `json:"verpflegungspauschale" csv:"Verpflegungspauschale"`
VertragStundenlohn float64 `json:"stundenlohn" csv:"Vertr. Stundenlohn"`
Vertragsstunden float64 `json:"vertragsstunden" csv:"Arbeitsstunden lt. Vertrag"`
GearbeiteteStunden float64 `json:"gearbeiteteStunden" csv:"Gearbeitet Stunden"`
GearbeiteteTage float64 `json:"gearbeiteteTage" csv:"Gearbeitet Tage"`
PuenktlichkeitspraemieAnzahl float64 `json:"puenktlichkeitspraemieAnzahl" csv:"Pünktlichkeitsprämie anzahl"`
Krankheitstage float64 `json:"krankheitstage" csv:"Krankheitstage x 8 Stunden"`
Ueberstunden float64 `json:"ueberstunden" csv:"davon Überstunden"`
Tourengeld float64 `json:"tourengeld" csv:"Tourengeld Anzahl"`
TourengeldBonus float64 `json:"tourengeldBonus" csv:"Tourengeld-Bonus (Platz 1-3, EUR)"`
GesundheitsbonusAbzug string `json:"gesundheitsbonusAbzug" csv:"Gesundheitsbonus abzuziehen? (ja/nein)"`
Urlaubsgeld float64 `json:"urlaubsgeld" csv:"Urlaubsgeld (44 EUR pro tag)"`
PScheinZulage float64 `json:"pScheinZulage" csv:"P-schein Zulage"`
Wochenendzuschlag15 float64 `json:"wochenendzuschlag15" csv:"Wochenendzuschlag 15%"`
Sonntagszuschlag50 float64 `json:"sonntagszuschlag50" csv:"Sonntagszuschlag 50%"`
Feiertagszuschlag125 float64 `json:"feiertagszuschlag125" csv:"Feiertagszuschlag 125%"`
Nachtzuschlag25 float64 `json:"nachtzuschlag25" csv:"Nachtzuschlag 25%"`
Nachtzuschlag40 float64 `json:"nachtzuschlag40" csv:"Nachtzuschlag 40%"`
}
type EmployeeOnTime struct {
EmployeeId string `csv:"EmployeeId"`
WorkTime time.Time `csv:"Arbeitszeit"`
PlannedTime time.Time `csv:"Schichtplanung"`
IsOnTime bool `csv:"Pünktlich?"`
}
func NewEmployeeOnTime(employeeId string, worktime, plannedtime time.Time) EmployeeOnTime {
loc, err := time.LoadLocation("Europe/Berlin")
if err != nil {
fmt.Println("Fehler beim Laden der Zeitzone:", err)
}
return EmployeeOnTime{
EmployeeId: employeeId,
WorkTime: worktime.In(loc),
PlannedTime: plannedtime.In(loc),
IsOnTime: worktime.Before(plannedtime) || worktime == plannedtime,
}
}

View File

@ -0,0 +1,42 @@
package gencsv
import (
excelread "business-engine/internal/excel_read"
"encoding/csv"
"io"
"os"
"github.com/gocarina/gocsv"
)
func GeneratePayrollStatementCSV(stmts []excelread.PayrollStatement) error {
file, err := os.Create("payroll_statement.csv")
if err != nil {
return err
}
defer file.Close()
gocsv.SetCSVWriter(func(out io.Writer) *gocsv.SafeCSVWriter {
writer := csv.NewWriter(out)
writer.Comma = ';'
return gocsv.NewSafeCSVWriter(writer)
})
if err := gocsv.MarshalFile(&stmts, file); err != nil {
return err
}
return nil
}
func GenerateEmployeesOnTime(employeesontime []excelread.EmployeeOnTime) error {
file, err := os.Create("employees_on_time.csv")
if err != nil {
return err
}
defer file.Close()
if err := gocsv.MarshalFile(&employeesontime, file); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,306 @@
package genpdf
import (
excelread "business-engine/internal/excel_read"
"fmt"
"slices"
"time"
"codeberg.org/go-pdf/fpdf"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
// LineItem repräsentiert eine einzelne Position in der Abrechnung.
type LineItem struct {
Lohnart string
Beschreibung string
Anzahl float64
Betrag float64
Faktor float64
}
type StatementInfo struct {
GearbeiteteStunden float64
GearbeiteteTage float64
Urlaubsgeld float64
Krankheitstage float64
}
// Total berechnet den Gesamtwert der Position.
func (item LineItem) Total() float64 {
if item.Faktor > 0 {
return item.Anzahl * item.Betrag * (item.Faktor / 100)
}
return item.Anzahl * item.Betrag
}
func GenerateSalaryStatement(stmt excelread.PayrollStatement, filename string, month time.Time) error {
pdf := fpdf.New("P", "mm", "A4", "")
pdf.AddUTF8Font("DejaVu", "", "DejaVuSans.ttf")
pdf.AddUTF8Font("DejaVu", "B", "DejaVuSans-Bold.ttf")
pdf.AddPage()
pdf.SetFont("DejaVu", "", 12)
pdf.Cell(0, 10, month.Format("01-2006"))
pdf.Ln(12)
pdf.SetDrawColor(200, 200, 200)
pdf.SetLineWidth(0.5)
x, y := pdf.GetXY()
pdf.Line(x, y, x+40, y)
pdf.Ln(10)
pdf.SetFont("DejaVu", "B", 16)
title := fmt.Sprintf("Gehaltsaufstellung für %s %s", stmt.Vorname, stmt.Nachname)
pdf.Cell(0, 10, title)
pdf.Ln(15)
drawInfoBlock(pdf, StatementInfo{
GearbeiteteStunden: stmt.GearbeiteteStunden,
GearbeiteteTage: stmt.GearbeiteteTage,
Urlaubsgeld: stmt.Urlaubsgeld,
Krankheitstage: stmt.Krankheitstage,
})
pdf.Ln(15)
drawHeader(pdf)
items := genLineItemsFromStatement(stmt)
var gesamtbetrag float64
for _, item := range items {
drawRow(pdf, item)
gesamtbetrag += item.Total()
}
drawSummary(pdf, gesamtbetrag)
if err := pdf.OutputFileAndClose(filename); err != nil {
return err
}
if pdf.Err() {
return pdf.Error()
}
return nil
}
func genLineItemsFromStatement(stmt excelread.PayrollStatement) []LineItem {
var items []LineItem
// --- Gehalt & Stunden ---
if stmt.Vertragsstunden > 0 && stmt.VertragStundenlohn > 0 {
items = append(items, LineItem{
Lohnart: "0005",
Beschreibung: "Gehalt",
Anzahl: stmt.Vertragsstunden,
Betrag: stmt.VertragStundenlohn,
})
}
if stmt.Ueberstunden > 0 && stmt.VertragStundenlohn > 0 {
items = append(items, LineItem{
Lohnart: "1250",
Beschreibung: "Überstundenvergütung",
Anzahl: stmt.Ueberstunden,
Betrag: stmt.VertragStundenlohn,
})
}
if stmt.Verpflegungspauschale > 0 {
items = append(items, LineItem{
Lohnart: "0227",
Beschreibung: "Verpflegungsmehrauf. 14€",
Anzahl: stmt.Verpflegungspauschale,
Betrag: 14.00,
})
}
if stmt.Urlaubsgeld > 0 {
items = append(items, LineItem{
Lohnart: "0052",
Beschreibung: "Urlaubsgeld Angestellte",
Anzahl: stmt.Urlaubsgeld,
Betrag: 44.00,
})
}
if stmt.TourengeldBonus > 0 {
items = append(items, LineItem{
Lohnart: "5019",
Beschreibung: "Bonus Tourengeld",
Anzahl: 1,
Betrag: stmt.TourengeldBonus,
})
}
if stmt.Tourengeld > 0 {
items = append(items, LineItem{
Lohnart: "1030",
Beschreibung: "Tourengeld",
Anzahl: stmt.Tourengeld,
Betrag: 2.5,
})
}
if stmt.PuenktlichkeitspraemieAnzahl > 0 {
items = append(items, LineItem{
Lohnart: "0109",
Beschreibung: "Verpflegung 25% (Pünktlichkeit)",
Anzahl: stmt.PuenktlichkeitspraemieAnzahl,
Betrag: 14.0,
})
}
if stmt.GesundheitsbonusAbzug == "" {
items = append(items, LineItem{
Lohnart: "1032",
Beschreibung: "Gesundheitsbonus",
Anzahl: 1,
Betrag: 200.0,
})
}
if stmt.PScheinZulage > 0 {
items = append(items, LineItem{
Lohnart: "1024",
Beschreibung: "Zulage Personenbeförd.",
Anzahl: stmt.PScheinZulage,
Betrag: 5.0,
})
}
if stmt.Wochenendzuschlag15 > 0 {
items = append(items, LineItem{
Lohnart: "1063",
Beschreibung: "Samstagszuschlag 15%",
Anzahl: stmt.Wochenendzuschlag15,
Betrag: stmt.VertragStundenlohn,
Faktor: 15,
})
}
if stmt.Sonntagszuschlag50 > 0 {
items = append(items, LineItem{
Lohnart: "0581",
Beschreibung: "Sonntagszuschlag 50%",
Anzahl: stmt.Sonntagszuschlag50,
Betrag: stmt.VertragStundenlohn,
Faktor: 50,
})
}
if stmt.Feiertagszuschlag125 > 0 {
items = append(items, LineItem{
Lohnart: "0551",
Beschreibung: "Feiertagszuschlag 125%",
Anzahl: stmt.Feiertagszuschlag125,
Betrag: stmt.VertragStundenlohn,
Faktor: 125,
})
}
if stmt.Nachtzuschlag25 > 0 {
items = append(items, LineItem{
Lohnart: "0505",
Beschreibung: "Nachtzuschl. 20:00-24:00h",
Anzahl: stmt.Nachtzuschlag25,
Betrag: stmt.VertragStundenlohn,
Faktor: 25,
})
}
if stmt.Nachtzuschlag40 > 0 {
items = append(items, LineItem{
Lohnart: "0501",
Beschreibung: "Nachtzuschl. 0:00-4:00h",
Anzahl: stmt.Nachtzuschlag40,
Betrag: stmt.VertragStundenlohn,
Faktor: 40,
})
}
slices.SortFunc(items, func(a, b LineItem) int {
return int(b.Total() - a.Total())
})
return items
}
func drawInfoBlock(pdf *fpdf.Fpdf, info StatementInfo) {
const padding = 3.0
const elementHeight = 4.0 + 6.0
x, y := pdf.GetXY()
drawLabelWithValue(pdf, x+padding, y, "Anw.-Std.", formatFloat(info.GearbeiteteStunden))
drawLabelWithValue(pdf, x+padding+18, y, "Anw.-Tage.", formatFloat(info.GearbeiteteTage))
pdf.Line(x+padding+38, y-padding, x+padding+38, y+elementHeight+padding)
drawLabelWithValue(pdf, x+padding+40, y, "Urlaub", formatFloat(float64(info.Urlaubsgeld)))
drawLabelWithValue(pdf, x+padding+55, y, "Krank.-Tage", formatFloat(float64(info.Krankheitstage)))
rectX := x
rectY := y - padding
rectWidth := 4*20 + 2*padding - 5
rectHeight := elementHeight + 2*padding
pdf.SetFillColor(180, 200, 220)
pdf.Rect(rectX, rectY, rectWidth, rectHeight, "D")
}
func drawLabelWithValue(pdf *fpdf.Fpdf, x, y float64, label, value string) {
const labelHeight = 4.0
const valueheight = 6.0
pdf.SetFont("DejaVu", "", 8)
pdf.SetTextColor(80, 100, 120)
pdf.SetXY(x, y)
pdf.Cell(20, labelHeight, label)
pdf.SetFont("DejaVu", "", 10)
pdf.SetTextColor(0, 0, 0)
pdf.SetXY(x, y+labelHeight)
pdf.Cell(20, valueheight, value)
}
func drawHeader(pdf *fpdf.Fpdf) {
pdf.SetFont("DejaVu", "B", 10)
pdf.SetFillColor(240, 240, 240)
pdf.SetX(10)
pdf.CellFormat(20, 7, "Lohnart", "1", 0, "C", true, 0, "")
pdf.SetX(30)
pdf.CellFormat(70, 7, "Bezeichnung", "1", 0, "L", true, 0, "")
pdf.SetX(100)
pdf.CellFormat(25, 7, "Menge", "1", 0, "R", true, 0, "")
pdf.SetX(125)
pdf.CellFormat(25, 7, "%-Zuschlag", "1", 0, "R", true, 0, "")
pdf.SetX(150)
pdf.CellFormat(25, 7, "Betrag", "1", 0, "R", true, 0, "")
pdf.SetX(175)
pdf.CellFormat(30, 7, "Gesamt", "1", 0, "R", true, 0, "")
pdf.Ln(7)
}
func drawRow(pdf *fpdf.Fpdf, item LineItem) {
pdf.SetFont("DejaVu", "", 10)
pdf.SetX(10)
pdf.CellFormat(20, 7, item.Lohnart, "1", 0, "C", false, 0, "")
pdf.SetX(30)
pdf.CellFormat(70, 7, item.Beschreibung, "1", 0, "L", false, 0, "")
pdf.SetX(100)
pdf.CellFormat(25, 7, formatFloat(item.Anzahl), "1", 0, "R", false, 0, "")
pdf.SetX(125)
pdf.CellFormat(25, 7, formatFloat(item.Faktor), "1", 0, "R", false, 0, "")
pdf.SetX(150)
pdf.CellFormat(25, 7, formatCurrency(item.Betrag), "1", 0, "R", false, 0, "")
pdf.SetX(175)
pdf.CellFormat(30, 7, formatCurrency(item.Total()), "1", 0, "R", false, 0, "")
pdf.Ln(7)
}
func drawSummary(pdf *fpdf.Fpdf, total float64) {
pdf.SetFont("DejaVu", "B", 11)
pdf.SetY(pdf.GetY() + 5) // Etwas Abstand
pdf.SetX(120)
pdf.CellFormat(50, 8, "Gesamtbetrag:", "T", 0, "L", false, 0, "")
pdf.SetX(170)
pdf.CellFormat(30, 8, formatCurrency(total), "T", 0, "R", false, 0, "")
}
func formatFloat(f float64) string {
p := message.NewPrinter(language.German)
str := p.Sprintf("%.2f", f)
return str //strings.Replace(str, ".", ",", 1)
}
func formatCurrency(val float64) string {
return formatFloat(val) + "€"
}

View File

@ -0,0 +1,14 @@
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
}

View File

@ -0,0 +1,90 @@
package payroll
import (
excelread "business-engine/internal/excel_read"
"business-engine/internal/gencsv"
"business-engine/internal/genpdf"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"slices"
"time"
)
type EmployeeHandler struct {
service Service
logger *slog.Logger
}
func NewEmployeeHandler(service Service, logger *slog.Logger) *EmployeeHandler {
return &EmployeeHandler{
service: service,
logger: logger,
}
}
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)
if err != nil {
h.logger.Error("failed to get employees", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
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)
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)
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)
}
}

View File

@ -0,0 +1,29 @@
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,
}
}

View File

@ -0,0 +1,21 @@
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),
},
}
}

View File

@ -0,0 +1,177 @@
package payroll
import (
"business-engine/internal/database"
excelread "business-engine/internal/excel_read"
"context"
"fmt"
"log/slog"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
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
}
type Service struct {
repo Repository
engine Engine
logger *slog.Logger
}
func NewService(repo Repository, logger *slog.Logger) *Service {
return &Service{
repo: repo,
logger: logger,
}
}
func (s *Service) FetchPayrollInput(ctx context.Context, month time.Time, employeeFirstname, employeeLastname string) (excelread.PayrollInputData, error) {
s.logger.Info("fetching payroll input", "month", month)
employeeIDs, err := s.repo.GetEmployeeByName(ctx, database.GetEmployeeByNameParams{
Firstname: employeeFirstname,
Lastname: employeeLastname,
})
if err != nil {
s.logger.Error("could not find employee with that name", "firstname", employeeFirstname, "lastname", employeeLastname, "error", err)
return excelread.PayrollInputData{}, err
}
s.logger.Debug("got employee data", "employee", employeeIDs)
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)
tourengeld, err := s.repo.CountTourengeldAmountInPeriod(ctx, database.CountTourengeldAmountInPeriodParams{
StartDate: startOfMonth,
EndDate: endOfMonth,
EmployeeID: pgtype.Text{
String: employeeIDs.DispoliveID,
Valid: true,
},
})
if err != nil {
s.logger.Error("could not count tourengeld for employee", "employeeId", employeeIDs.DyflexisID, "error", 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
}
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 {
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, 999,
eom.Location(),
)
}

View File

@ -0,0 +1,26 @@
version: "2"
sql:
- engine: "postgresql"
queries: "./db/queries"
schema: "./db/migrations"
gen:
go:
package: "database"
out: "./internal/database"
sql_package: "pgx/v5"
emit_json_tags: true
overrides:
- db_type: "pg_catalog.timestamp"
go_type:
type: "time.Time"
- db_type: "pg_catalog.timestamp"
nullable: true
go_type:
type: "*time.Time"
- db_type: "pg_catalog.timestamptz"
go_type:
type: "time.Time"
- db_type: "pg_catalog.timestamptz"
nullable: true
go_type:
type: "*time.Time"