new endpoints

This commit is contained in:
samantha42
2026-03-21 16:49:01 +01:00
parent 3490dd13d4
commit c55e7d6774
5 changed files with 213 additions and 68 deletions

View File

@@ -1,31 +1,12 @@
package database
import (
"Engine/internal/model"
"context"
"database/sql"
"fmt"
)
// ── Domain types ──────────────────────────────────────────────────────────────
type Department struct {
ID int `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
CostCenter string `json:"cost_center"`
Active bool `json:"active"`
CreatedAt string `json:"created_at"`
}
type GLAccount struct {
ID int `json:"id"`
Code string `json:"code"`
Description string `json:"description"`
Type string `json:"type"` // revenue | cogs | opex | capex | headcount
FavourHigh bool `json:"favour_high"` // true = over-budget is good (revenue accounts)
Active bool `json:"active"`
}
// ── ReferenceRepo ─────────────────────────────────────────────────────────────
type ReferenceRepo struct {
@@ -37,15 +18,14 @@ func NewReferenceRepo(db *sql.DB) *ReferenceRepo {
}
// ── Department operations ─────────────────────────────────────────────────────
func (r *ReferenceRepo) CreateDepartment(ctx context.Context, d Department) (*Department, error) {
func (r *ReferenceRepo) CreateDepartment(ctx context.Context, d model.CreateDepartmentRequest) (*model.Department, error) {
res, err := r.db.ExecContext(ctx, `
INSERT INTO departments (code, name, cost_center, active)
VALUES (?, ?, ?, ?)
ON CONFLICT(code) DO UPDATE
SET name = excluded.name,
cost_center = excluded.cost_center,
active = excluded.active`,
INSERT INTO departments (code, name, cost_center, active)
VALUES (?, ?, ?, ?)
ON CONFLICT(code) DO UPDATE
SET name = excluded.name,
cost_center = excluded.cost_center,
active = excluded.active`,
d.Code, d.Name, d.CostCenter, boolToInt(d.Active),
)
if err != nil {
@@ -55,14 +35,14 @@ func (r *ReferenceRepo) CreateDepartment(ctx context.Context, d Department) (*De
id, err := res.LastInsertId()
if err != nil || id == 0 {
// ON CONFLICT branch — fetch existing row
return r.getDepartmentByCode(ctx, d.Code)
return r.GetDepartmentByCode(ctx, d.Code)
}
d.ID = int(id)
return &d, nil
return r.GetDepartmentByCode(ctx, d.Code)
}
func (r *ReferenceRepo) getDepartmentByCode(ctx context.Context, code string) (*Department, error) {
var d Department
func (r *ReferenceRepo) GetDepartmentByCode(ctx context.Context, code string) (*model.Department, error) {
var d model.Department
var active int
err := r.db.QueryRowContext(ctx,
`SELECT id, code, name, cost_center, active, created_at FROM departments WHERE code = ?`, code,
@@ -74,7 +54,54 @@ func (r *ReferenceRepo) getDepartmentByCode(ctx context.Context, code string) (*
return &d, nil
}
func (r *ReferenceRepo) ListDepartments(ctx context.Context) ([]Department, error) {
func (r *ReferenceRepo) GetDepartmentByCostCenter(ctx context.Context, cc string) (*model.Department, error) {
var d model.Department
var active int
err := r.db.QueryRowContext(ctx,
`SELECT id, code, name, cost_center, active, created_at FROM departments WHERE cost_center = ?`, cc,
).Scan(&d.ID, &d.Code, &d.Name, &d.CostCenter, &active, &d.CreatedAt)
if err != nil {
return nil, fmt.Errorf("fetch department by code: %w", err)
}
d.Active = active == 1
return &d, nil
}
func (r *ReferenceRepo) GetDepartmentByName(ctx context.Context, name string) (*model.Department, error) {
var d model.Department
var active int
err := r.db.QueryRowContext(ctx,
`SELECT id, code, name, cost_center, active, created_at FROM departments WHERE name = ?`, name,
).Scan(&d.ID, &d.Code, &d.Name, &d.CostCenter, &active, &d.CreatedAt)
if err != nil {
return nil, fmt.Errorf("fetch department by code: %w", err)
}
d.Active = active == 1
return &d, nil
}
func (r *ReferenceRepo) SetDepartmentActivityByCode(ctx context.Context, code string, active bool) error {
_, err := r.db.ExecContext(ctx,
`UPDATE departments SET active = ? WHERE code = ?`,
boolToInt(active), code)
return err
}
func (r *ReferenceRepo) SetDepartmentActivityByName(ctx context.Context, name string, active bool) error {
_, err := r.db.ExecContext(ctx,
`UPDATE departments SET active = ? WHERE name = ?`,
boolToInt(active), name)
return err
}
func (r *ReferenceRepo) SetDepartmentActivityByCostCenter(ctx context.Context, cc string, active bool) error {
_, err := r.db.ExecContext(ctx,
`UPDATE departments SET active = ? WHERE cost_center = ?`,
boolToInt(active), cc)
return err
}
func (r *ReferenceRepo) ListDepartments(ctx context.Context) ([]model.Department, error) {
rows, err := r.db.QueryContext(ctx,
`SELECT id, code, name, cost_center, active, created_at
FROM departments ORDER BY code`)
@@ -83,9 +110,9 @@ func (r *ReferenceRepo) ListDepartments(ctx context.Context) ([]Department, erro
}
defer rows.Close()
var depts []Department
var depts []model.Department
for rows.Next() {
var d Department
var d model.Department
var active int
if err := rows.Scan(&d.ID, &d.Code, &d.Name, &d.CostCenter, &active, &d.CreatedAt); err != nil {
return nil, err
@@ -103,7 +130,7 @@ func (r *ReferenceRepo) DeleteDepartment(ctx context.Context, id int) error {
// ── GL Account operations ─────────────────────────────────────────────────────
func (r *ReferenceRepo) CreateGLAccount(ctx context.Context, a GLAccount) (*GLAccount, error) {
func (r *ReferenceRepo) CreateGLAccount(ctx context.Context, a model.GLAccount) (*model.GLAccount, error) {
res, err := r.db.ExecContext(ctx, `
INSERT INTO gl_accounts (code, description, type, favour_high, active)
VALUES (?, ?, ?, ?, ?)
@@ -126,8 +153,8 @@ func (r *ReferenceRepo) CreateGLAccount(ctx context.Context, a GLAccount) (*GLAc
return &a, nil
}
func (r *ReferenceRepo) getGLAccountByCode(ctx context.Context, code string) (*GLAccount, error) {
var a GLAccount
func (r *ReferenceRepo) getGLAccountByCode(ctx context.Context, code string) (*model.GLAccount, error) {
var a model.GLAccount
var favourHigh, active int
err := r.db.QueryRowContext(ctx,
`SELECT id, code, description, type, favour_high, active FROM gl_accounts WHERE code = ?`, code,
@@ -140,7 +167,7 @@ func (r *ReferenceRepo) getGLAccountByCode(ctx context.Context, code string) (*G
return &a, nil
}
func (r *ReferenceRepo) ListGLAccounts(ctx context.Context) ([]GLAccount, error) {
func (r *ReferenceRepo) ListGLAccounts(ctx context.Context) ([]model.GLAccount, error) {
rows, err := r.db.QueryContext(ctx,
`SELECT id, code, description, type, favour_high, active
FROM gl_accounts ORDER BY code`)
@@ -149,9 +176,9 @@ func (r *ReferenceRepo) ListGLAccounts(ctx context.Context) ([]GLAccount, error)
}
defer rows.Close()
var accts []GLAccount
var accts []model.GLAccount
for rows.Next() {
var a GLAccount
var a model.GLAccount
var favourHigh, active int
if err := rows.Scan(&a.ID, &a.Code, &a.Description, &a.Type, &favourHigh, &active); err != nil {
return nil, err

View File

@@ -8,6 +8,7 @@ import (
"strings"
"Engine/internal/database"
"Engine/internal/model"
)
type ReferenceHandler struct {
@@ -23,7 +24,7 @@ func NewReferenceHandler(repo *database.ReferenceRepo) *ReferenceHandler {
// 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 database.Department
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
@@ -55,11 +56,106 @@ func (h *ReferenceHandler) ListDepartments(w http.ResponseWriter, r *http.Reques
return
}
if depts == nil {
depts = []database.Department{}
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 {
@@ -90,7 +186,7 @@ var validGLTypes = map[string]bool{
// POST /api/v1/gl-accounts
func (h *ReferenceHandler) CreateGLAccount(w http.ResponseWriter, r *http.Request) {
var req database.GLAccount
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
@@ -126,7 +222,7 @@ func (h *ReferenceHandler) ListGLAccounts(w http.ResponseWriter, r *http.Request
return
}
if accts == nil {
accts = []database.GLAccount{}
accts = []model.GLAccount{}
}
writeJSON(w, http.StatusOK, accts)
}

View File

@@ -25,6 +25,37 @@ type Department struct {
Code string `json:"code"`
Name string `json:"name"`
CostCenter string `json:"cost_center"`
Active bool `json:"active"`
CreatedAt string `json:"created_at"`
}
type CreateDepartmentRequest struct {
Code string `json:"code"`
Name string `json:"name"`
CostCenter string `json:"cost_center"`
Active bool `json:"active"`
}
type GetDepartmentRequest struct {
Code *string `json:"code"`
Name *string `json:"name"`
CostCenter *string `json:"cost_center"`
}
type SetDepartmentActivity struct {
Active bool `json:"active"`
Code *string `json:"code"`
Name *string `json:"name"`
CostCenter *string `json:"cost_center"`
}
type GLAccount struct {
ID int `json:"id"`
Code string `json:"code"`
Description string `json:"description"`
Type string `json:"type"` // revenue | cogs | opex | capex | headcount
FavourHigh bool `json:"favour_high"` // true = over-budget is good (revenue accounts)
Active bool `json:"active"`
}
type GetDepartmentBudget struct {
@@ -35,14 +66,6 @@ type GetDepartmentActual struct {
Code string `json:"code"`
}
type GLAccount struct {
ID int `json:"id"`
Code string `json:"code"`
Description string `json:"description"`
Type GLAccountType `json:"type"`
FavourHigh bool `json:"favour_high"`
}
type GetGLAccountBudget struct {
Code string `json:"code"`
}