package handlers import ( "database/sql" "encoding/json" "net/http" "time" "github.com/go-chi/chi/v5" ) type PayrollHandler struct { DB *sql.DB } func NewPayrollHandler(db *sql.DB) *PayrollHandler { return &PayrollHandler{DB: db} } type PayrollRun struct { ID string `json:"id"` PeriodStart time.Time `json:"period_start"` PeriodEnd time.Time `json:"period_end"` PayDate time.Time `json:"pay_date"` Status string `json:"status"` TotalGross float64 `json:"total_gross"` TotalTax float64 `json:"total_tax"` TotalNet float64 `json:"total_net"` TotalEmployerTax float64 `json:"total_employer_tax"` Currency string `json:"currency"` CreatedAt time.Time `json:"created_at"` } type PayrollLine struct { ID string `json:"id"` EmployeeID string `json:"employee_id"` EmployeeName string `json:"employee_name"` GrossSalary float64 `json:"gross_salary"` TaxDeduction float64 `json:"tax_deduction"` SocialFees float64 `json:"social_fees"` Pension float64 `json:"pension"` OtherDeductions float64 `json:"other_deductions"` NetSalary float64 `json:"net_salary"` HoursWorked float64 `json:"hours_worked"` VacationDaysUsed float64 `json:"vacation_days_used"` SickDays float64 `json:"sick_days"` } func (h *PayrollHandler) ListPayrollRuns(w http.ResponseWriter, r *http.Request) { rows, err := h.DB.Query(` SELECT id, period_start, period_end, pay_date, status, total_gross, total_tax, total_net, total_employer_tax, currency, created_at FROM boc_payroll_runs ORDER BY period_start DESC LIMIT 50 `) if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } defer rows.Close() runs := []PayrollRun{} for rows.Next() { var pr PayrollRun if err := rows.Scan(&pr.ID, &pr.PeriodStart, &pr.PeriodEnd, &pr.PayDate, &pr.Status, &pr.TotalGross, &pr.TotalTax, &pr.TotalNet, &pr.TotalEmployerTax, &pr.Currency, &pr.CreatedAt); err != nil { continue } runs = append(runs, pr) } writeJSON(w, http.StatusOK, map[string]interface{}{ "payroll_runs": runs, "total": len(runs), }) } func (h *PayrollHandler) CreatePayrollRun(w http.ResponseWriter, r *http.Request) { var req struct { PeriodStart time.Time `json:"period_start"` PeriodEnd time.Time `json:"period_end"` PayDate time.Time `json:"pay_date"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } var id string err := h.DB.QueryRow(` INSERT INTO boc_payroll_runs (period_start, period_end, pay_date, status) VALUES ($1, $2, $3, 'draft') RETURNING id `, req.PeriodStart, req.PeriodEnd, req.PayDate).Scan(&id) if err != nil { writeError(w, http.StatusInternalServerError, "failed to create payroll run") return } writeJSON(w, http.StatusCreated, map[string]interface{}{ "id": id, "message": "Payroll run created", }) } func (h *PayrollHandler) GetPayrollRun(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var pr PayrollRun err := h.DB.QueryRow(` SELECT id, period_start, period_end, pay_date, status, total_gross, total_tax, total_net, total_employer_tax, currency, created_at FROM boc_payroll_runs WHERE id = $1 `, id).Scan(&pr.ID, &pr.PeriodStart, &pr.PeriodEnd, &pr.PayDate, &pr.Status, &pr.TotalGross, &pr.TotalTax, &pr.TotalNet, &pr.TotalEmployerTax, &pr.Currency, &pr.CreatedAt) if err == sql.ErrNoRows { writeError(w, http.StatusNotFound, "payroll run not found") return } if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } // Get lines rows, err := h.DB.Query(` SELECT pl.id, pl.employee_id, e.first_name || ' ' || e.last_name, pl.gross_salary, pl.tax_deduction, pl.social_fees, pl.pension, pl.other_deductions, pl.net_salary, pl.hours_worked, pl.vacation_days_used, pl.sick_days FROM boc_payroll_lines pl JOIN boc_employees e ON pl.employee_id = e.id WHERE pl.payroll_run_id = $1 `, id) if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } defer rows.Close() lines := []PayrollLine{} for rows.Next() { var l PayrollLine if err := rows.Scan(&l.ID, &l.EmployeeID, &l.EmployeeName, &l.GrossSalary, &l.TaxDeduction, &l.SocialFees, &l.Pension, &l.OtherDeductions, &l.NetSalary, &l.HoursWorked, &l.VacationDaysUsed, &l.SickDays); err != nil { continue } lines = append(lines, l) } writeJSON(w, http.StatusOK, map[string]interface{}{ "payroll_run": pr, "lines": lines, }) } func (h *PayrollHandler) ProcessPayroll(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") // Get all active employees rows, err := h.DB.Query(` SELECT id, salary, employment_type FROM boc_employees WHERE status = 'active' `) if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } defer rows.Close() tx, err := h.DB.Begin() if err != nil { writeError(w, http.StatusInternalServerError, "transaction error") return } defer tx.Rollback() var totalGross, totalTax, totalNet, totalEmployerTax float64 for rows.Next() { var empID string var salary float64 var empType string if err := rows.Scan(&empID, &salary, &empType); err != nil { continue } // Simple Swedish tax calculation (placeholder) gross := salary tax := gross * 0.30 // 30% income tax socialFees := gross * 0.3142 // 31.42% employer tax pension := gross * 0.045 // 4.5% pension net := gross - tax - pension _, err = tx.Exec(` INSERT INTO boc_payroll_lines (payroll_run_id, employee_id, gross_salary, tax_deduction, social_fees, pension, other_deductions, net_salary, hours_worked) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 160) `, id, empID, gross, tax, socialFees, pension, 0, net) if err != nil { writeError(w, http.StatusInternalServerError, "failed to create payroll line") return } totalGross += gross totalTax += tax totalNet += net totalEmployerTax += socialFees } _, err = tx.Exec(` UPDATE boc_payroll_runs SET status = 'processing', total_gross = $1, total_tax = $2, total_net = $3, total_employer_tax = $4 WHERE id = $5 `, totalGross, totalTax, totalNet, totalEmployerTax, id) if err != nil { writeError(w, http.StatusInternalServerError, "failed to update payroll run") return } if err := tx.Commit(); err != nil { writeError(w, http.StatusInternalServerError, "commit failed") return } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "Payroll processed", "summary": map[string]interface{}{ "total_gross": totalGross, "total_tax": totalTax, "total_net": totalNet, "total_employer_tax": totalEmployerTax, }, }) } func (h *PayrollHandler) ApprovePayroll(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") _, err := h.DB.Exec(` UPDATE boc_payroll_runs SET status = 'approved' WHERE id = $1 `, id) if err != nil { writeError(w, http.StatusInternalServerError, "failed to approve payroll") return } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "Payroll approved", }) }