feat(boc): v1.0 - Complete Business Operations Center
- Go backend API with full CRUD for all modules - Rust analytics service with parallel processing - C runtime with POSIX shared memory IPC - PostgreSQL schema with 30+ tables - Redis cache, Kafka event streaming - WebSocket hub, automation engine - PDF generation, Resend email integration - JWT auth, multi-tenant - Docker Compose deployment - Nginx reverse proxy Refs: BOC-001
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
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",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user