diff --git a/internal/database/budget-repo.go b/internal/database/budget-repo.go index b2b85db..4c31d8b 100644 --- a/internal/database/budget-repo.go +++ b/internal/database/budget-repo.go @@ -107,20 +107,20 @@ func (r *BudgetRepo) Create(ctx context.Context, req model.CreateBudgetRequest) return b, nil } -func (r *BudgetRepo) Update(ctx context.Context, id int, req model.UpdateBudgetRequest) (*model.Budget, error) { +func (r *BudgetRepo) Update(ctx context.Context, req model.UpdateBudgetRequest) (*model.Budget, error) { _, err := r.db.ExecContext(ctx, ` UPDATE budgets SET version=?, amount=?, notes=?, updated_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id=?`, - req.Version, req.Amount, req.Notes, id, + req.Version, req.Amount, req.Notes, req.ID, ) if err != nil { return nil, fmt.Errorf("update budget: %w", err) } row := r.db.QueryRowContext(ctx, - `SELECT`+budgetSelectCols+`FROM budgets WHERE id = ?`, id) + `SELECT`+budgetSelectCols+`FROM budgets WHERE id = ?`, req.ID) b, err := scanBudget(row) if err != nil { return nil, fmt.Errorf("fetch updated budget: %w", err) @@ -128,8 +128,8 @@ func (r *BudgetRepo) Update(ctx context.Context, id int, req model.UpdateBudgetR return b, nil } -func (r *BudgetRepo) Delete(ctx context.Context, id int) error { - _, err := r.db.ExecContext(ctx, `DELETE FROM budgets WHERE id = ?`, id) +func (r *BudgetRepo) Delete(ctx context.Context, req model.DeleteBudgetRequest) error { + _, err := r.db.ExecContext(ctx, `DELETE FROM budgets WHERE id = ?`, req.ID) if err != nil { return fmt.Errorf("delete budget: %w", err) } diff --git a/internal/handler/budget.go b/internal/handler/budget.go index 7ff0721..3ba6bbb 100644 --- a/internal/handler/budget.go +++ b/internal/handler/budget.go @@ -3,7 +3,6 @@ package handler import ( "encoding/json" "net/http" - "strconv" "strings" "Engine/internal/model" @@ -39,12 +38,8 @@ func (h *BudgetHandler) Create(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusCreated, budget) } +// PUT /api/v1/budgets/update func (h *BudgetHandler) Update(w http.ResponseWriter, r *http.Request) { - id, err := strconv.Atoi(r.PathValue("id")) - if err != nil { - writeError(w, http.StatusBadRequest, "invalid id") - return - } var req model.UpdateBudgetRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") @@ -57,7 +52,7 @@ func (h *BudgetHandler) Update(w http.ResponseWriter, r *http.Request) { return } - budget, err := h.svc.Update(r.Context(), id, req) + budget, err := h.svc.Update(r.Context(), req) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return @@ -65,13 +60,16 @@ func (h *BudgetHandler) Update(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, budget) } +// DELETE /api/v1/budgets/delete func (h *BudgetHandler) Delete(w http.ResponseWriter, r *http.Request) { - id, err := strconv.Atoi(r.PathValue("id")) - if err != nil { + var req struct { + ID int `json:"id"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } - if err := h.svc.Delete(r.Context(), id); err != nil { + if err := h.svc.Delete(r.Context(), req); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } diff --git a/internal/handler/refrence.go b/internal/handler/refrence.go index 33181d8..e20cb5d 100644 --- a/internal/handler/refrence.go +++ b/internal/handler/refrence.go @@ -20,7 +20,7 @@ func NewReferenceHandler(repo *database.ReferenceRepo) *ReferenceHandler { // ── Departments ─────────────────────────────────────────────────────────────── -// POST /api/v1/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 database.Department @@ -47,7 +47,7 @@ func (h *ReferenceHandler) CreateDepartment(w http.ResponseWriter, r *http.Reque writeJSON(w, http.StatusCreated, dept) } -// GET /api/v1/departments +// 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 { @@ -60,8 +60,16 @@ func (h *ReferenceHandler) ListDepartments(w http.ResponseWriter, r *http.Reques writeJSON(w, http.StatusOK, depts) } -// DELETE /api/v1/departments/{id} +// 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") diff --git a/internal/model/model.go b/internal/model/model.go index db0bea8..b65321c 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -27,6 +27,14 @@ type Department struct { CostCenter string `json:"cost_center"` } +type GetDepartmentBudget struct { + Code string `json:"code"` +} + +type GetDepartmentActual struct { + Code string `json:"code"` +} + type GLAccount struct { ID int `json:"id"` Code string `json:"code"` @@ -35,6 +43,14 @@ type GLAccount struct { FavourHigh bool `json:"favour_high"` } +type GetGLAccountBudget struct { + Code string `json:"code"` +} + +type GetGLAccountActual struct { + Code string `json:"code"` +} + type Budget struct { ID int `json:"id"` FiscalYear int `json:"fiscal_year"` @@ -94,44 +110,64 @@ func (cBudget *CreateBudgetRequest) Valid() []string { } type UpdateBudgetRequest struct { - ID int `json:"id"` - FiscalYear int `json:"fiscal_year"` - FiscalPeriod int `json:"fiscal_period"` - Version BudgetVersion `json:"version"` - DepartmentID int `json:"department_id"` - GLAccountID int `json:"gl_account_id"` - Amount float64 `json:"amount"` - Currency string `json:"currency"` - Notes string `json:"notes"` - ChangedBy string `json:"created_by"` + ID int // required, not a pointer + ChangedBy string // required, not a pointer + FiscalYear *int + FiscalPeriod *int + Version *string + DepartmentID *int + GLAccountID *int + Amount *float64 + Currency *string + Notes *string +} + +type DeleteBudgetRequest struct { + ID int `json:"id"` } func (uBudget *UpdateBudgetRequest) Valid() []string { var errs []string - if uBudget.FiscalYear < 1 || uBudget.FiscalYear > 2200 { - errs = append(errs, "fiscal_year must be between 1 and 2200") - } - if uBudget.FiscalPeriod < 1 || uBudget.FiscalPeriod > 12 { - errs = append(errs, "fiscal_period must be between 1 and 12") - } - if uBudget.Version == "" { - errs = append(errs, "version is required") - } - if uBudget.DepartmentID == 0 { - errs = append(errs, "department_id is required") - } - if uBudget.GLAccountID == 0 { - errs = append(errs, "gl_account_id is required") - } - if uBudget.Amount <= 0 { - errs = append(errs, "amount must be greater than 0") - } - if uBudget.Currency == "" { - errs = append(errs, "currency is required") + // Always required: identity + audit + if uBudget.ID == 0 { + errs = append(errs, "id is required") } if uBudget.ChangedBy == "" { - errs = append(errs, "created_by is required") + errs = append(errs, "changed_by is required") + } + + // Validate fields only if they were provided + if uBudget.FiscalYear != nil { + if *uBudget.FiscalYear < 1 || *uBudget.FiscalYear > 2200 { + errs = append(errs, "fiscal_year must be between 1 and 2200") + } + } + if uBudget.FiscalPeriod != nil { + if *uBudget.FiscalPeriod < 1 || *uBudget.FiscalPeriod > 12 { + errs = append(errs, "fiscal_period must be between 1 and 12") + } + } + if uBudget.Amount != nil && *uBudget.Amount <= 0 { + errs = append(errs, "amount must be greater than 0") + } + if uBudget.Version != nil && *uBudget.Version == "" { + errs = append(errs, "version cannot be empty") + } + if uBudget.Currency != nil && *uBudget.Currency == "" { + errs = append(errs, "currency cannot be empty") + } + + // At least one field must be set — otherwise there's nothing to do + if uBudget.FiscalYear == nil && + uBudget.FiscalPeriod == nil && + uBudget.Version == nil && + uBudget.DepartmentID == nil && + uBudget.GLAccountID == nil && + uBudget.Amount == nil && + uBudget.Currency == nil && + uBudget.Notes == nil { + errs = append(errs, "at least one field must be provided to update") } return errs diff --git a/internal/service/bugdet-service.go b/internal/service/bugdet-service.go index 6a41cd1..9bea6cd 100644 --- a/internal/service/bugdet-service.go +++ b/internal/service/bugdet-service.go @@ -19,10 +19,10 @@ func (s *BudgetService) Create(ctx context.Context, req model.CreateBudgetReques return s.repo.Create(ctx, req) } -func (s *BudgetService) Update(ctx context.Context, id int, req model.UpdateBudgetRequest) (*model.Budget, error) { - return s.repo.Update(ctx, id, req) +func (s *BudgetService) Update(ctx context.Context, req model.UpdateBudgetRequest) (*model.Budget, error) { + return s.repo.Update(ctx, req) } -func (s *BudgetService) Delete(ctx context.Context, id int) error { - return s.repo.Delete(ctx, id) +func (s *BudgetService) Delete(ctx context.Context, req model.DeleteBudgetRequest) error { + return s.repo.Delete(ctx, req) } diff --git a/main.go b/main.go index 89822a3..b556da8 100644 --- a/main.go +++ b/main.go @@ -49,18 +49,22 @@ func main() { mux := http.NewServeMux() // Reference endpoints - mux.HandleFunc("POST /api/v1/departments", referenceH.CreateDepartment) - mux.HandleFunc("GET /api/v1/departments", referenceH.ListDepartments) - mux.HandleFunc("DELETE /api/v1/departments/{id}", referenceH.DeleteDepartment) + mux.HandleFunc("POST /api/v1/department/create", referenceH.CreateDepartment) + 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("POST /api/v1/gl-accounts", referenceH.CreateGLAccount) - mux.HandleFunc("GET /api/v1/gl-accounts", referenceH.ListGLAccounts) - mux.HandleFunc("DELETE /api/v1/gl-accounts/{id}", referenceH.DeleteGLAccount) + mux.HandleFunc("POST /api/v1/gl-account/create", referenceH.CreateGLAccount) + mux.HandleFunc("DELETE /api/v1/gl-accounts/delete", referenceH.DeleteGLAccount) + mux.HandleFunc("GET /api/v1/gl-account/list", referenceH.ListGLAccounts) + mux.HandleFunc("GET /api/v1/gl-accounts/bugdet", referenceH.ListDepartments) + mux.HandleFunc("GET /api/v1/gl-accounts/actual", referenceH.ListDepartments) // Budget endpoints - mux.HandleFunc("POST /api/v1/budgets", budgetH.Create) - mux.HandleFunc("PUT /api/v1/budgets/{id}", budgetH.Update) - mux.HandleFunc("DELETE /api/v1/budgets/{id}", budgetH.Delete) + mux.HandleFunc("POST /api/v1/budget/create", budgetH.Create) + mux.HandleFunc("PUT /api/v1/budgets/update", budgetH.Update) + mux.HandleFunc("DELETE /api/v1/budgets/delete", budgetH.Delete) // Actuals + variance mux.HandleFunc("POST /api/v1/actuals/ingest", actualsH.Ingest) diff --git a/tests/budget_test.go b/tests/budget_test.go index b7b0e68..96f3289 100644 --- a/tests/budget_test.go +++ b/tests/budget_test.go @@ -25,9 +25,9 @@ func newBudgetServer(t *testing.T) *httptest.Server { h := handler.NewBudgetHandler(service.NewBudgetService(repo)) mux := http.NewServeMux() - mux.HandleFunc("POST /api/v1/budgets", h.Create) - mux.HandleFunc("PUT /api/v1/budgets/{id}", h.Update) - mux.HandleFunc("DELETE /api/v1/budgets/{id}", h.Delete) + mux.HandleFunc("POST /api/v1/budget/create", h.Create) + mux.HandleFunc("PUT /api/v1/budgets/update", h.Update) + mux.HandleFunc("DELETE /api/v1/budgets/delete", h.Delete) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) @@ -108,40 +108,76 @@ func TestUpdateBudget_OK(t *testing.T) { srv := newBudgetServer(t) client := srv.Client() - // Create first - resp, err := client.Post(srv.URL+"/api/v1/budgets", "application/json", - mustJSON(t, validBudget())) + // Create a budget to update + resp, err := client.Post( + srv.URL+"/api/v1/budget/create", + "application/json", + mustJSON(t, validBudget()), + ) if err != nil { t.Fatal(err) } + defer resp.Body.Close() + var created model.Budget - json.NewDecoder(resp.Body).Decode(&created) - resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(&created); err != nil { + t.Fatalf("decode created budget: %v", err) + } + if created.ID == 0 { + t.Fatal("expected a non-zero ID from create") + } - // Update amount - updated := validBudget() - updated["amount"] = 9999.99 + // Update only the fields the endpoint is meant to change + wantAmount := 9999.99 + wantNotes := "updated in test" + updateReq := model.UpdateBudgetRequest{ + ID: created.ID, + Amount: &wantAmount, + Notes: &wantNotes, + ChangedBy: created.CreatedBy, + } - req, _ := http.NewRequest(http.MethodPut, - fmt.Sprintf("%s/api/v1/budgets/%d", srv.URL, created.ID), - mustJSON(t, updated)) + req, err := http.NewRequest( + http.MethodPut, + srv.URL+"/api/v1/budget/update", + mustJSON(t, updateReq), + ) + if err != nil { + t.Fatal(err) + } req.Header.Set("Content-Type", "application/json") + resp2, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp2.Body.Close() - if resp2.StatusCode != http.StatusOK && resp2.StatusCode != http.StatusNoContent { - t.Errorf("update: got %d, want 200 or 204", resp2.StatusCode) + if resp2.StatusCode != http.StatusOK { + t.Fatalf("update: got %d, want 200", resp2.StatusCode) } - if resp2.StatusCode == http.StatusOK { - var got model.Budget - json.NewDecoder(resp2.Body).Decode(&got) - if got.Amount != 9999.99 { - t.Errorf("updated amount: got %v, want 9999.99", got.Amount) - } + var got model.Budget + if err := json.NewDecoder(resp2.Body).Decode(&got); err != nil { + t.Fatalf("decode updated budget: %v", err) + } + + if got.Amount != wantAmount { + t.Errorf("amount: got %v, want %v", got.Amount, wantAmount) + } + if got.Notes != wantNotes { + t.Errorf("notes: got %q, want %q", got.Notes, wantNotes) + } + if got.ID != created.ID { + t.Errorf("ID changed after update: got %v, want %v", got.ID, created.ID) + } + + // Fields not included in the update should be unchanged + if got.FiscalYear != created.FiscalYear { + t.Errorf("fiscal_year changed unexpectedly: got %v, want %v", got.FiscalYear, created.FiscalYear) + } + if got.DepartmentID != created.DepartmentID { + t.Errorf("department_id changed unexpectedly: got %v, want %v", got.DepartmentID, created.DepartmentID) } }