From c55e7d6774ae313cdb22e48272acab494dc7874f Mon Sep 17 00:00:00 2001 From: samantha42 Date: Sat, 21 Mar 2026 16:49:01 +0100 Subject: [PATCH] new endpoints --- internal/database/refrence-repo.go | 111 ++++++++++++++++++----------- internal/handler/refrence.go | 104 +++++++++++++++++++++++++-- internal/model/model.go | 39 +++++++--- main.go | 7 +- tests/refrence_test.go | 20 +++--- 5 files changed, 213 insertions(+), 68 deletions(-) diff --git a/internal/database/refrence-repo.go b/internal/database/refrence-repo.go index 8ad58dc..da75da7 100644 --- a/internal/database/refrence-repo.go +++ b/internal/database/refrence-repo.go @@ -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 diff --git a/internal/handler/refrence.go b/internal/handler/refrence.go index e20cb5d..8973a41 100644 --- a/internal/handler/refrence.go +++ b/internal/handler/refrence.go @@ -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) } diff --git a/internal/model/model.go b/internal/model/model.go index df9fdec..1cd52d4 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -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"` } diff --git a/main.go b/main.go index 3298e22..5ad4fb2 100644 --- a/main.go +++ b/main.go @@ -53,16 +53,15 @@ func main() { // Reference endpoints mux.HandleFunc("POST /api/v1/department/create", referenceH.CreateDepartment) + mux.HandleFunc("POST /api/v1/department/activity", referenceH.SetActivityDepartment) mux.HandleFunc("DELETE /api/v1/department/delete", referenceH.DeleteDepartment) mux.HandleFunc("GET /api/v1/department/list", referenceH.ListDepartments) - mux.HandleFunc("GET /api/v1/department/bugdet", referenceH.ListDepartments) - mux.HandleFunc("GET /api/v1/department/actual", referenceH.ListDepartments) + mux.HandleFunc("GET /api/v1/department/", referenceH.GetDepartment) mux.HandleFunc("POST /api/v1/gl-account/create", referenceH.CreateGLAccount) mux.HandleFunc("DELETE /api/v1/gl-account/delete", referenceH.DeleteGLAccount) mux.HandleFunc("GET /api/v1/gl-account/list", referenceH.ListGLAccounts) - mux.HandleFunc("GET /api/v1/gl-account/bugdet", referenceH.ListDepartments) - mux.HandleFunc("GET /api/v1/gl-account/actual", referenceH.ListDepartments) + mux.HandleFunc("GET /api/v1/gl-account", referenceH.ListGLAccounts) // Budget endpoints mux.HandleFunc("POST /api/v1/budget/create", budgetH.Create) diff --git a/tests/refrence_test.go b/tests/refrence_test.go index 422ed87..9d0abc1 100644 --- a/tests/refrence_test.go +++ b/tests/refrence_test.go @@ -50,7 +50,7 @@ func TestCreateDepartment_OK(t *testing.T) { testutil.AssertStatus(t, w, http.StatusCreated) - var got database.Department + var got model.Department testutil.DecodeJSON(t, w, &got) if got.Code != "ENG" { t.Errorf("code: got %q, want %q", got.Code, "ENG") @@ -72,7 +72,7 @@ func TestCreateDepartment_DefaultsActiveTrue(t *testing.T) { testutil.AssertStatus(t, w, http.StatusCreated) - var got database.Department + var got model.Department testutil.DecodeJSON(t, w, &got) if !got.Active { t.Error("expected active to default to true when omitted") @@ -116,7 +116,7 @@ func TestCreateDepartment_Upsert(t *testing.T) { w := testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "FIN", "name": "Finance Updated"}) testutil.AssertStatus(t, w, http.StatusCreated) - var got database.Department + var got model.Department testutil.DecodeJSON(t, w, &got) if got.Name != "Finance Updated" { t.Errorf("upsert name: got %q, want %q", got.Name, "Finance Updated") @@ -131,7 +131,7 @@ func TestListDepartments_Empty(t *testing.T) { w := testutil.Do(t, http.HandlerFunc(h.ListDepartments), http.MethodGet, "/", nil) testutil.AssertStatus(t, w, http.StatusOK) - var got []database.Department + var got []model.Department testutil.DecodeJSON(t, w, &got) if len(got) != 0 { t.Errorf("expected empty list, got %d", len(got)) @@ -149,7 +149,7 @@ func TestListDepartments_ReturnAll(t *testing.T) { w := testutil.Do(t, http.HandlerFunc(h.ListDepartments), http.MethodGet, "/", nil) testutil.AssertStatus(t, w, http.StatusOK) - var got []database.Department + var got []model.Department testutil.DecodeJSON(t, w, &got) if len(got) != 3 { t.Errorf("expected 3 departments, got %d", len(got)) @@ -167,7 +167,7 @@ func TestDeleteDepartment_OK(t *testing.T) { if err != nil { t.Fatal(err) } - var created database.Department + var created model.Department json.NewDecoder(resp.Body).Decode(&created) resp.Body.Close() @@ -209,7 +209,7 @@ func TestCreateGLAccount_OK(t *testing.T) { testutil.AssertStatus(t, w, http.StatusCreated) - var got database.GLAccount + var got model.GLAccount testutil.DecodeJSON(t, w, &got) if got.Code != "5001" { t.Errorf("code: got %q, want %q", got.Code, "5001") @@ -256,7 +256,7 @@ func TestListGLAccounts_Empty(t *testing.T) { w := testutil.Do(t, http.HandlerFunc(h.ListGLAccounts), http.MethodGet, "/", nil) testutil.AssertStatus(t, w, http.StatusOK) - var got []database.GLAccount + var got []model.GLAccount testutil.DecodeJSON(t, w, &got) if len(got) != 0 { t.Errorf("expected empty list, got %d", len(got)) @@ -273,7 +273,7 @@ func TestListGLAccounts_ReturnAll(t *testing.T) { w := testutil.Do(t, http.HandlerFunc(h.ListGLAccounts), http.MethodGet, "/", nil) testutil.AssertStatus(t, w, http.StatusOK) - var got []database.GLAccount + var got []model.GLAccount testutil.DecodeJSON(t, w, &got) if len(got) != 2 { t.Errorf("expected 2 GL accounts, got %d", len(got)) @@ -291,7 +291,7 @@ func TestDeleteGLAccount_OK(t *testing.T) { if err != nil { t.Fatal(err) } - var created database.GLAccount + var created model.GLAccount json.NewDecoder(resp.Body).Decode(&created) resp.Body.Close()