new endpoints
This commit is contained in:
@@ -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,8 +18,7 @@ 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 (?, ?, ?, ?)
|
||||
@@ -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)
|
||||
}
|
||||
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
|
||||
return r.GetDepartmentByCode(ctx, d.Code)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
7
main.go
7
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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user