249 lines
7.0 KiB
Go
249 lines
7.0 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"Engine/internal/database"
|
|
"Engine/internal/model"
|
|
)
|
|
|
|
type ReferenceHandler struct {
|
|
repo *database.ReferenceRepo
|
|
}
|
|
|
|
func NewReferenceHandler(repo *database.ReferenceRepo) *ReferenceHandler {
|
|
return &ReferenceHandler{repo: repo}
|
|
}
|
|
|
|
// ── Departments ───────────────────────────────────────────────────────────────
|
|
|
|
// POST /api/v1/department/create
|
|
// PUT /api/v1/departments/{id} (same body, id from path)
|
|
func (h *ReferenceHandler) CreateDepartment(w http.ResponseWriter, r *http.Request) {
|
|
var req model.CreateDepartmentRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid body: %v", err))
|
|
return
|
|
}
|
|
req.Code = strings.TrimSpace(req.Code)
|
|
req.Name = strings.TrimSpace(req.Name)
|
|
if req.Code == "" || req.Name == "" {
|
|
writeError(w, http.StatusBadRequest, "code and name are required")
|
|
return
|
|
}
|
|
// Default active to true when not explicitly set to false
|
|
if !req.Active {
|
|
req.Active = true
|
|
}
|
|
|
|
dept, err := h.repo.CreateDepartment(r.Context(), req)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("create department: %v", err))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, dept)
|
|
}
|
|
|
|
// GET /api/v1/department/list
|
|
func (h *ReferenceHandler) ListDepartments(w http.ResponseWriter, r *http.Request) {
|
|
depts, err := h.repo.ListDepartments(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("list departments: %v", err))
|
|
return
|
|
}
|
|
if depts == nil {
|
|
depts = []model.Department{}
|
|
}
|
|
writeJSON(w, http.StatusOK, depts)
|
|
}
|
|
|
|
// GET /api/v1/department/
|
|
func (h *ReferenceHandler) GetDepartment(w http.ResponseWriter, r *http.Request) {
|
|
var req model.GetDepartmentRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid body: %v", err))
|
|
return
|
|
}
|
|
|
|
// exactly one field must be set
|
|
set := 0
|
|
if req.Code != nil {
|
|
set++
|
|
}
|
|
if req.Name != nil {
|
|
set++
|
|
}
|
|
if req.CostCenter != nil {
|
|
set++
|
|
}
|
|
if set == 0 {
|
|
writeError(w, http.StatusBadRequest, "one of code, name, cost_center is required")
|
|
return
|
|
}
|
|
if set > 1 {
|
|
writeError(w, http.StatusBadRequest, "only one of code, name, cost_center may be set")
|
|
return
|
|
}
|
|
|
|
var (
|
|
dept *model.Department
|
|
err error
|
|
)
|
|
switch {
|
|
case req.Code != nil:
|
|
dept, err = h.repo.GetDepartmentByCode(r.Context(), *req.Code)
|
|
case req.Name != nil:
|
|
dept, err = h.repo.GetDepartmentByName(r.Context(), *req.Name)
|
|
case req.CostCenter != nil:
|
|
dept, err = h.repo.GetDepartmentByCostCenter(r.Context(), *req.CostCenter)
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if dept == nil {
|
|
writeError(w, http.StatusNotFound, "department not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, dept)
|
|
}
|
|
|
|
func (h *ReferenceHandler) SetActivityDepartment(w http.ResponseWriter, r *http.Request) {
|
|
var req model.SetDepartmentActivity
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid body: %v", err))
|
|
return
|
|
}
|
|
|
|
set := 0
|
|
if req.Code != nil {
|
|
set++
|
|
}
|
|
if req.Name != nil {
|
|
set++
|
|
}
|
|
if req.CostCenter != nil {
|
|
set++
|
|
}
|
|
if set == 0 {
|
|
writeError(w, http.StatusBadRequest, "one of code, name, cost_center is required")
|
|
return
|
|
}
|
|
if set > 1 {
|
|
writeError(w, http.StatusBadRequest, "only one of code, name, cost_center may be set")
|
|
return
|
|
}
|
|
|
|
var err error
|
|
switch {
|
|
case req.Code != nil:
|
|
err = h.repo.SetDepartmentActivityByCode(r.Context(), *req.Code, req.Active)
|
|
case req.Name != nil:
|
|
err = h.repo.SetDepartmentActivityByName(r.Context(), *req.Name, req.Active)
|
|
case req.CostCenter != nil:
|
|
err = h.repo.SetDepartmentActivityByCostCenter(r.Context(), *req.CostCenter, req.Active)
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// DELETE /api/v1/department/delete
|
|
func (h *ReferenceHandler) DeleteDepartment(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
ID int `json:"id"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid body: %v", err))
|
|
return
|
|
}
|
|
|
|
id, err := pathID(r, "id")
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
if err := h.repo.DeleteDepartment(r.Context(), id); err != nil {
|
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("delete department: %v", err))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// ── GL Accounts ───────────────────────────────────────────────────────────────
|
|
|
|
var validGLTypes = map[string]bool{
|
|
"revenue": true, "cogs": true, "opex": true, "capex": true, "headcount": true,
|
|
}
|
|
|
|
// POST /api/v1/gl-accounts
|
|
func (h *ReferenceHandler) CreateGLAccount(w http.ResponseWriter, r *http.Request) {
|
|
var req model.GLAccount
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid body: %v", err))
|
|
return
|
|
}
|
|
req.Code = strings.TrimSpace(req.Code)
|
|
req.Description = strings.TrimSpace(req.Description)
|
|
if req.Code == "" || req.Description == "" {
|
|
writeError(w, http.StatusBadRequest, "code and description are required")
|
|
return
|
|
}
|
|
if !validGLTypes[req.Type] {
|
|
writeError(w, http.StatusBadRequest,
|
|
"type must be one of: revenue, cogs, opex, capex, headcount")
|
|
return
|
|
}
|
|
if !req.Active {
|
|
req.Active = true
|
|
}
|
|
|
|
acct, err := h.repo.CreateGLAccount(r.Context(), req)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("create gl_account: %v", err))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, acct)
|
|
}
|
|
|
|
// GET /api/v1/gl-accounts
|
|
func (h *ReferenceHandler) ListGLAccounts(w http.ResponseWriter, r *http.Request) {
|
|
accts, err := h.repo.ListGLAccounts(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("list gl_accounts: %v", err))
|
|
return
|
|
}
|
|
if accts == nil {
|
|
accts = []model.GLAccount{}
|
|
}
|
|
writeJSON(w, http.StatusOK, accts)
|
|
}
|
|
|
|
// DELETE /api/v1/gl-accounts/{id}
|
|
func (h *ReferenceHandler) DeleteGLAccount(w http.ResponseWriter, r *http.Request) {
|
|
id, err := pathID(r, "id")
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
if err := h.repo.DeleteGLAccount(r.Context(), id); err != nil {
|
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("delete gl_account: %v", err))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// ── Shared helpers ────────────────────────────────────────────────────────────
|
|
|
|
func pathID(r *http.Request, key string) (int, error) {
|
|
return strconv.Atoi(r.PathValue(key))
|
|
}
|