diff --git a/internal/handler/budget.go b/internal/handler/budget.go index a08617b..7ff0721 100644 --- a/internal/handler/budget.go +++ b/internal/handler/budget.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "strconv" + "strings" "Engine/internal/model" "Engine/internal/service" @@ -23,6 +24,13 @@ func (h *BudgetHandler) Create(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "invalid request body") return } + + errs := req.Valid() + if len(errs) > 0 { + writeError(w, http.StatusBadRequest, strings.Join(errs, "; ")) + return + } + budget, err := h.svc.Create(r.Context(), req) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) @@ -42,6 +50,13 @@ func (h *BudgetHandler) Update(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "invalid request body") return } + + errs := req.Valid() + if len(errs) > 0 { + writeError(w, http.StatusBadRequest, strings.Join(errs, "; ")) + return + } + budget, err := h.svc.Update(r.Context(), id, req) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) diff --git a/internal/model/model.go b/internal/model/model.go index 1a05687..db0bea8 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -62,12 +62,79 @@ type CreateBudgetRequest struct { CreatedBy string `json:"created_by"` } +func (cBudget *CreateBudgetRequest) Valid() []string { + var errs []string + + if cBudget.FiscalYear < 1 || cBudget.FiscalYear > 2200 { + errs = append(errs, "fiscal_year must be between 1 and 2200") + } + if cBudget.FiscalPeriod < 1 || cBudget.FiscalPeriod > 12 { + errs = append(errs, "fiscal_period must be between 1 and 12") + } + if cBudget.Version == "" { + errs = append(errs, "version is required") + } + if cBudget.DepartmentID == 0 { + errs = append(errs, "department_id is required") + } + if cBudget.GLAccountID == 0 { + errs = append(errs, "gl_account_id is required") + } + if cBudget.Amount <= 0 { + errs = append(errs, "amount must be greater than 0") + } + if cBudget.Currency == "" { + errs = append(errs, "currency is required") + } + if cBudget.CreatedBy == "" { + errs = append(errs, "created_by is required") + } + + return errs +} + type UpdateBudgetRequest struct { - ID int `json:"id"` - Amount float64 `json:"amount"` - Notes string `json:"notes"` - Version BudgetVersion `json:"version"` - ChangedBy string `json:"created_by"` + 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"` +} + +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") + } + if uBudget.ChangedBy == "" { + errs = append(errs, "created_by is required") + } + + return errs } type Actual struct { @@ -93,6 +160,34 @@ type IngestActualsRequest struct { Source string `json:"source"` } +func (i *IngestActualsRequest) Valid() []string { + var errs []string + + if i.FiscalYear < 1 || i.FiscalYear > 2200 { + errs = append(errs, "fiscal_year must be between 1 and 2200") + } + if i.FiscalPeriod < 1 || i.FiscalPeriod > 12 { + errs = append(errs, "fiscal_period must be between 1 and 12") + } + if i.DeptCode == "" { + errs = append(errs, "dept_code is required") + } + if i.GLCode == "" { + errs = append(errs, "gl_code is required") + } + if i.Amount <= 0 { + errs = append(errs, "amount must be greater than 0") + } + if i.Currency == "" { + errs = append(errs, "currency is required") + } + if i.Source == "" { + errs = append(errs, "source is required") + } + + return errs +} + type VarianceStatus string const ( diff --git a/internal/test/refrence_test.go b/internal/test/refrence_test.go new file mode 100644 index 0000000..c88ba77 --- /dev/null +++ b/internal/test/refrence_test.go @@ -0,0 +1,334 @@ +package test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "Engine/internal/database" + "Engine/internal/handler" + "Engine/internal/model" + "Engine/tests/internal/testutil" +) + +// ── wire helpers ────────────────────────────────────────────────────────────── + +func newReferenceHandler(t *testing.T) *handler.ReferenceHandler { + t.Helper() + return handler.NewReferenceHandler(database.NewReferenceRepo(testutil.NewTestDB(t))) +} + +// newReferenceServer spins up a real httptest.Server with the production +// mux routes. Use this for delete/path-param tests that need {id} routing. +func newReferenceServer(t *testing.T) *httptest.Server { + t.Helper() + h := newReferenceHandler(t) + + mux := http.NewServeMux() + mux.HandleFunc("POST /api/v1/departments", h.CreateDepartment) + mux.HandleFunc("GET /api/v1/departments", h.ListDepartments) + mux.HandleFunc("DELETE /api/v1/departments/{id}", h.DeleteDepartment) + mux.HandleFunc("POST /api/v1/gl-accounts", h.CreateGLAccount) + mux.HandleFunc("GET /api/v1/gl-accounts", h.ListGLAccounts) + mux.HandleFunc("DELETE /api/v1/gl-accounts/{id}", h.DeleteGLAccount) + + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + return srv +} + +// ── Department: Create ──────────────────────────────────────────────────────── + +func TestCreateDepartment_OK(t *testing.T) { + h := newReferenceHandler(t) + + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", + map[string]any{"code": "ENG", "name": "Engineering", "active": true}) + + testutil.AssertStatus(t, w, http.StatusCreated) + + var got database.Department + testutil.DecodeJSON(t, w, &got) + if got.Code != "ENG" { + t.Errorf("code: got %q, want %q", got.Code, "ENG") + } + if got.ID == 0 { + t.Error("expected non-zero ID in response") + } + if !got.Active { + t.Error("expected active=true") + } +} + +func TestCreateDepartment_DefaultsActiveTrue(t *testing.T) { + h := newReferenceHandler(t) + + // active field omitted — handler must default it to true + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", + map[string]any{"code": "MKT", "name": "Marketing"}) + + testutil.AssertStatus(t, w, http.StatusCreated) + + var got database.Department + testutil.DecodeJSON(t, w, &got) + if !got.Active { + t.Error("expected active to default to true when omitted") + } +} + +func TestCreateDepartment_MissingCode(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", + map[string]any{"name": "Engineering"}) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateDepartment_MissingName(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", + map[string]any{"code": "ENG"}) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateDepartment_WhitespaceOnly(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", + map[string]any{"code": " ", "name": " "}) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateDepartment_InvalidJSON(t *testing.T) { + h := newReferenceHandler(t) + // nil body → empty body → JSON decode fails → 400 + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", nil) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateDepartment_Upsert(t *testing.T) { + h := newReferenceHandler(t) + fn := http.HandlerFunc(h.CreateDepartment) + + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "FIN", "name": "Finance"}) + + 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 + testutil.DecodeJSON(t, w, &got) + if got.Name != "Finance Updated" { + t.Errorf("upsert name: got %q, want %q", got.Name, "Finance Updated") + } +} + +// ── Department: List ────────────────────────────────────────────────────────── + +func TestListDepartments_Empty(t *testing.T) { + h := newReferenceHandler(t) + + w := testutil.Do(t, http.HandlerFunc(h.ListDepartments), http.MethodGet, "/", nil) + testutil.AssertStatus(t, w, http.StatusOK) + + var got []database.Department + testutil.DecodeJSON(t, w, &got) + if len(got) != 0 { + t.Errorf("expected empty list, got %d", len(got)) + } +} + +func TestListDepartments_ReturnAll(t *testing.T) { + h := newReferenceHandler(t) + fn := http.HandlerFunc(h.CreateDepartment) + + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "HR", "name": "Human Resources"}) + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "IT", "name": "Information Technology"}) + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "OPS", "name": "Operations"}) + + w := testutil.Do(t, http.HandlerFunc(h.ListDepartments), http.MethodGet, "/", nil) + testutil.AssertStatus(t, w, http.StatusOK) + + var got []database.Department + testutil.DecodeJSON(t, w, &got) + if len(got) != 3 { + t.Errorf("expected 3 departments, got %d", len(got)) + } +} + +// ── Department: Delete ──────────────────────────────────────────────────────── + +func TestDeleteDepartment_OK(t *testing.T) { + srv := newReferenceServer(t) + client := srv.Client() + + resp, err := client.Post(srv.URL+"/api/v1/departments", "application/json", + mustJSON(t, map[string]any{"code": "OPS", "name": "Operations"})) + if err != nil { + t.Fatal(err) + } + var created database.Department + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/departments/"+strconv.Itoa(created.ID), nil) + resp2, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + resp2.Body.Close() + + if resp2.StatusCode != http.StatusNoContent && resp2.StatusCode != http.StatusOK { + t.Errorf("delete: got %d, want 200 or 204", resp2.StatusCode) + } +} + +func TestDeleteDepartment_NotFound(t *testing.T) { + srv := newReferenceServer(t) + + req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/departments/9999", nil) + resp, err := srv.Client().Do(req) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + + // Accept 404 or 204 — adjust to match your handler's behaviour + if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNoContent { + t.Errorf("delete non-existent: got %d, want 404 or 204", resp.StatusCode) + } +} + +// ── GL Account: Create ──────────────────────────────────────────────────────── + +func TestCreateGLAccount_OK(t *testing.T) { + h := newReferenceHandler(t) + + w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/", + map[string]any{"code": "5001", "name": "Travel Expenses", "type": "expense"}) + + testutil.AssertStatus(t, w, http.StatusCreated) + + var got database.GLAccount + testutil.DecodeJSON(t, w, &got) + if got.Code != "5001" { + t.Errorf("code: got %q, want %q", got.Code, "5001") + } + if got.ID == 0 { + t.Error("expected non-zero ID") + } +} + +func TestCreateGLAccount_MissingCode(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/", + map[string]any{"name": "Travel Expenses"}) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateGLAccount_MissingName(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/", + map[string]any{"code": "5001"}) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateGLAccount_Upsert(t *testing.T) { + h := newReferenceHandler(t) + fn := http.HandlerFunc(h.CreateGLAccount) + + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue"}) + + w := testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue Updated"}) + testutil.AssertStatus(t, w, http.StatusCreated) + + var got model.GLAccount + testutil.DecodeJSON(t, w, &got) + if got.Name != "Revenue Updated" { + t.Errorf("upsert name: got %q, want %q", got.Name, "Revenue Updated") + } +} + +// ── GL Account: List ────────────────────────────────────────────────────────── + +func TestListGLAccounts_Empty(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.ListGLAccounts), http.MethodGet, "/", nil) + testutil.AssertStatus(t, w, http.StatusOK) + + var got []database.GLAccount + testutil.DecodeJSON(t, w, &got) + if len(got) != 0 { + t.Errorf("expected empty list, got %d", len(got)) + } +} + +func TestListGLAccounts_ReturnAll(t *testing.T) { + h := newReferenceHandler(t) + fn := http.HandlerFunc(h.CreateGLAccount) + + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue"}) + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "5001", "name": "COGS"}) + + w := testutil.Do(t, http.HandlerFunc(h.ListGLAccounts), http.MethodGet, "/", nil) + testutil.AssertStatus(t, w, http.StatusOK) + + var got []database.GLAccount + testutil.DecodeJSON(t, w, &got) + if len(got) != 2 { + t.Errorf("expected 2 GL accounts, got %d", len(got)) + } +} + +// ── GL Account: Delete ──────────────────────────────────────────────────────── + +func TestDeleteGLAccount_OK(t *testing.T) { + srv := newReferenceServer(t) + client := srv.Client() + + resp, err := client.Post(srv.URL+"/api/v1/gl-accounts", "application/json", + mustJSON(t, map[string]any{"code": "6001", "name": "Rent"})) + if err != nil { + t.Fatal(err) + } + var created database.GLAccount + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/gl-accounts/"+strconv.Itoa(created.ID), nil) + resp2, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + resp2.Body.Close() + + if resp2.StatusCode != http.StatusNoContent && resp2.StatusCode != http.StatusOK { + t.Errorf("delete: got %d, want 200 or 204", resp2.StatusCode) + } +} + +func TestDeleteGLAccount_NotFound(t *testing.T) { + srv := newReferenceServer(t) + + req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/gl-accounts/9999", nil) + resp, err := srv.Client().Do(req) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + + if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNoContent { + t.Errorf("delete non-existent: got %d, want 404 or 204", resp.StatusCode) + } +} + +// ── local helpers ───────────────────────────────────────────────────────────── + +func mustJSON(t *testing.T, v any) *bytes.Reader { + t.Helper() + b, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + return bytes.NewReader(b) +} diff --git a/testing/README.md b/testing/README.md deleted file mode 100644 index 0c77d27..0000000 --- a/testing/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# FP&A Test Data Platform - -Python tooling to generate, validate, and load realistic FP&A data into your Go API. - -## Structure - -``` -testing/ -├── generators/ -│ └── generate_data.py # Creates all CSV files -├── loaders/ -│ └── api_loader.py # POSTs CSVs to your Go API -├── tests/ -│ └── test_fpa.py # Data integrity + API tests -├── data/ -│ └── csv/ # Generated CSV files land here -└── requirements.txt -``` - -## Quick Start - -```bash -pip install -r requirements.txt - -# 1. Generate all CSV data -python generators/generate_data.py - -# 2. Validate data integrity (no API needed) -pytest tests/test_fpa.py -v - -# 3. Load into your Go API (dry-run first) -python loaders/api_loader.py --dry-run - -# 4. Load for real -python loaders/api_loader.py --url http://localhost:8080 -``` - -## Generated Datasets - -| File | Rows | Description | -|------|------|-------------| -| `revenue_budget_vs_actuals.csv` | 48 | Product & Service revenue — budget vs actuals, 24 months | -| `opex_budget_vs_actuals.csv` | ~2,688 | Dept × category opex — budget vs actuals | -| `pl_income_statement.csv` | 24 | Monthly P&L: revenue, COGS, gross profit, EBITDA, net income | -| `cash_flow.csv` | 24 | Operating / investing / financing cash flows, rolling balance | -| `headcount_workforce.csv` | ~1,000+ | Employee snapshots per month, with hire/term dates & salaries | - -## Key Concepts - -**Budget vs Actuals** — Every financial row has a `budget_amount` (what was planned) and -`actual_amount` (what really happened). The `variance` = actual − budget. Positive variance -on revenue = good. Positive variance on spend = over budget. - -**Product vs Service revenue** — Product is recurring SaaS subscriptions (~70%, higher margin). -Service is consulting/support (~30%, lower margin). Both grow monthly at different rates. - -**Cash Flow** — Separate from revenue. Collections can lag invoicing (DSO effect). Includes -a simulated Series A raise in June 2023. - -## API Endpoints (from main.go) - -``` -POST /api/v1/budgets ← create one budget line -PUT /api/v1/budgets/{id} ← update a budget line -DELETE /api/v1/budgets/{id} ← delete a budget line -POST /api/v1/actuals/ingest ← bulk ingest actuals { "records": [...] } -GET /api/v1/variance ← variance report (budget vs actuals) -GET /api/v1/variance/alerts ← over/under budget alerts -GET /api/v1/health ← db health check -``` - -## Load Order - -The seeder always loads in this order — **budgets must exist before actuals**: - -1. **Budgets** — `POST /api/v1/budgets` individually (revenue + opex lines) -2. **Actuals** — `POST /api/v1/actuals/ingest` in batches of 50 -3. **Variance check** — `GET /api/v1/variance` to confirm data landed - -## Loader Options - -```bash -python loaders/api_loader.py --help - - --url API base URL (default: http://localhost:8080) - --batch Records per actuals/ingest request (default: 50) - --dry-run Print payloads without sending - --token Bearer token for auth header - --only Run one step only: budgets | actuals | variance | alerts -``` \ No newline at end of file diff --git a/testing/data/csv/cash_flow.csv b/testing/data/csv/cash_flow.csv deleted file mode 100644 index 9d1d76d..0000000 --- a/testing/data/csv/cash_flow.csv +++ /dev/null @@ -1,25 +0,0 @@ -company,year,month,period,cash_collected_product,cash_collected_service,cash_paid_opex,cash_paid_cogs,net_operating_cash_flow,capex,net_investing_cash_flow,loan_repayment,equity_raised,net_financing_cash_flow,net_change_in_cash,closing_cash_balance -AcmeSaaS Inc.,2023,1,2023-01,163879.88,63878.3,261115.75,83104.06,-116461.63,6403.87,-6403.87,0.0,0.0,0.0,-122865.5,1077134.5 -AcmeSaaS Inc.,2023,2,2023-02,178115.66,69069.82,255846.74,76652.29,-85313.55,5786.84,-5786.84,0.0,0.0,0.0,-91100.39,986034.11 -AcmeSaaS Inc.,2023,3,2023-03,183078.97,71995.04,275445.39,76630.76,-97002.14,0.0,-0.0,5282.91,0.0,-5282.91,-102285.05,883749.06 -AcmeSaaS Inc.,2023,4,2023-04,185844.77,72290.05,275275.82,85242.75,-102383.75,0.0,-0.0,0.0,0.0,0.0,-102383.75,781365.31 -AcmeSaaS Inc.,2023,5,2023-05,199095.24,71446.54,276860.19,80225.21,-86543.62,7899.21,-7899.21,0.0,0.0,0.0,-94442.83,686922.48 -AcmeSaaS Inc.,2023,6,2023-06,186599.26,73957.63,282518.88,93079.84,-115041.83,0.0,-0.0,4741.08,500000.0,495258.92,380217.09,1067139.57 -AcmeSaaS Inc.,2023,7,2023-07,199774.68,71671.33,273476.83,86473.31,-88504.13,0.0,-0.0,0.0,0.0,0.0,-88504.13,978635.44 -AcmeSaaS Inc.,2023,8,2023-08,206079.53,71897.22,286478.13,88563.47,-97064.85,7988.9,-7988.9,0.0,0.0,0.0,-105053.75,873581.69 -AcmeSaaS Inc.,2023,9,2023-09,208715.04,76023.16,284930.26,98913.54,-99105.6,0.0,-0.0,4705.79,0.0,-4705.79,-103811.39,769770.3 -AcmeSaaS Inc.,2023,10,2023-10,202037.42,77115.77,302273.74,99395.35,-122515.9,10447.72,-10447.72,0.0,0.0,0.0,-132963.62,636806.68 -AcmeSaaS Inc.,2023,11,2023-11,224870.49,81643.64,292828.92,97493.13,-83807.92,0.0,-0.0,0.0,0.0,0.0,-83807.92,552998.76 -AcmeSaaS Inc.,2023,12,2023-12,224481.39,82875.26,304003.52,105371.0,-102017.87,0.0,-0.0,5589.79,0.0,-5589.79,-107607.66,445391.1 -AcmeSaaS Inc.,2024,1,2024-01,225535.21,86372.23,306496.22,94450.84,-89039.62,0.0,-0.0,0.0,0.0,0.0,-89039.62,356351.48 -AcmeSaaS Inc.,2024,2,2024-02,231002.6,81570.67,309999.41,104620.68,-102046.82,0.0,-0.0,0.0,0.0,0.0,-102046.82,254304.66 -AcmeSaaS Inc.,2024,3,2024-03,255843.55,86327.17,312424.94,108228.6,-78482.82,0.0,-0.0,5631.5,0.0,-5631.5,-84114.32,170190.34 -AcmeSaaS Inc.,2024,4,2024-04,243746.7,86737.49,333771.4,110195.35,-113482.56,0.0,-0.0,0.0,0.0,0.0,-113482.56,56707.78 -AcmeSaaS Inc.,2024,5,2024-05,253076.43,92057.75,335130.33,110912.96,-100909.11,0.0,-0.0,0.0,0.0,0.0,-100909.11,-44201.33 -AcmeSaaS Inc.,2024,6,2024-06,264225.36,88606.97,327425.58,118967.82,-93561.07,0.0,-0.0,4037.87,0.0,-4037.87,-97598.94,-141800.27 -AcmeSaaS Inc.,2024,7,2024-07,260739.02,83323.95,340807.79,120273.6,-117018.42,5198.6,-5198.6,0.0,0.0,0.0,-122217.02,-264017.29 -AcmeSaaS Inc.,2024,8,2024-08,261496.73,85375.07,335055.79,111383.21,-99567.2,6990.89,-6990.89,0.0,0.0,0.0,-106558.09,-370575.38 -AcmeSaaS Inc.,2024,9,2024-09,275303.9,94869.09,351269.66,114058.86,-95155.53,0.0,-0.0,5116.17,0.0,-5116.17,-100271.7,-470847.08 -AcmeSaaS Inc.,2024,10,2024-10,277726.03,99258.56,336970.32,118601.63,-78587.36,0.0,-0.0,0.0,0.0,0.0,-78587.36,-549434.44 -AcmeSaaS Inc.,2024,11,2024-11,309709.89,98087.4,341625.12,132656.81,-66484.64,10909.5,-10909.5,0.0,0.0,0.0,-77394.14,-626828.58 -AcmeSaaS Inc.,2024,12,2024-12,312778.51,91878.14,353027.96,122611.82,-70983.13,6093.24,-6093.24,5077.16,0.0,-5077.16,-82153.53,-708982.11 diff --git a/testing/data/csv/headcount_workforce.csv b/testing/data/csv/headcount_workforce.csv deleted file mode 100644 index 4aab9af..0000000 --- a/testing/data/csv/headcount_workforce.csv +++ /dev/null @@ -1,422 +0,0 @@ -company,employee_id,department,role,hire_date,termination_date,status,annual_salary_budget,actual_salary_paid_ytd,year,month,period,headcount_fte -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,14850.0,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,13466.67,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,13466.67,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,9900.0,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,5308.33,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,,Active,65000,5362.5,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,5416.67,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,6800.0,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,9075.0,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,6666.67,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,9258.33,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,7916.67,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,9775.0,2023,1,2023-01,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,7425.0,2023,1,2023-01,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,30300.0,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,26400.0,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,26933.33,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,20200.0,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,10941.67,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,,Active,65000,10616.67,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,10725.0,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,13200.0,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,18516.67,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,13200.0,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,18333.33,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,15991.67,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,19166.67,2023,2,2023-02,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,15000.0,2023,2,2023-02,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,44100.0,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,40800.0,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,40000.0,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,30600.0,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,15925.0,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,,Active,65000,16250.0,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,16250.0,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,19800.0,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,27225.0,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,20000.0,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,27500.0,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,23512.5,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,28462.5,2023,3,2023-03,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,22275.0,2023,3,2023-03,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,60000.0,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,54400.0,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,53866.67,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,39600.0,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,21233.33,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,,Active,65000,22100.0,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,21450.0,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,26666.67,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,36300.0,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,26400.0,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,36300.0,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,31983.33,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,38333.33,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,30000.0,2023,4,2023-04,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,6250.0,2023,4,2023-04,1.0 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,75750.0,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,66000.0,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,66000.0,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,50500.0,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,27354.17,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,,Active,65000,26541.67,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,27083.33,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,33666.67,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,44916.67,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,33333.33,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,44916.67,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,39583.33,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,47916.67,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,37500.0,2023,5,2023-05,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,12375.0,2023,5,2023-05,1.0 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,88200.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,79200.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,80000.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,58800.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,32500.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,,Active,65000,32825.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,32500.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,40000.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,55000.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,39200.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,55000.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,47975.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,58075.0,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,45000.0,2023,6,2023-06,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,18937.5,2023,6,2023-06,1.0 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,102900.0,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,95200.0,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,95200.0,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,68600.0,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,38675.0,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,,Active,65000,37158.33,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,37916.67,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,47133.33,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,64808.33,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,47600.0,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,64808.33,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,55416.67,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,68425.0,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,52500.0,2023,7,2023-07,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,25250.0,2023,7,2023-07,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,10200.0,2023,7,2023-07,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,121200.0,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,106666.67,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,107733.33,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,79200.0,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,42900.0,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,,Active,65000,43333.33,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,43333.33,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,53866.67,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,72600.0,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,52266.67,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,71866.67,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,62700.0,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,77433.33,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,60000.0,2023,8,2023-08,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,31250.0,2023,8,2023-08,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,20200.0,2023,8,2023-08,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,133650.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,122400.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,121200.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,88200.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,49237.5,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,,Active,65000,48750.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,49237.5,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,61200.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,80850.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,59400.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,80850.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,69825.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,86250.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,68175.0,2023,9,2023-09,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,36750.0,2023,9,2023-09,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,30300.0,2023,9,2023-09,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,148500.0,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,134666.67,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,130666.67,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,100000.0,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,53625.0,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,,Active,65000,54708.33,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,54708.33,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,66000.0,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,91666.67,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,67333.33,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,90750.0,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,78375.0,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,94875.0,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,75750.0,2023,10,2023-10,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,43312.5,2023,10,2023-10,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,39600.0,2023,10,2023-10,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,166650.0,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,146666.67,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,146666.67,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,112200.0,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,60775.0,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,60179.17,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,60179.17,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,72600.0,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,100833.33,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,72600.0,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,100833.33,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,87954.17,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,105416.67,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,83325.0,2023,11,2023-11,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,49500.0,2023,11,2023-11,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,50500.0,2023,11,2023-11,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,178200.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,158400.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,158400.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,121200.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,66300.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,65650.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,63700.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,80800.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,108900.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,79200.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,111100.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,,Active,95000,95000.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,116150.0,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,89100.0,2023,12,2023-12,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,56812.5,2023,12,2023-12,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,60600.0,2023,12,2023-12,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,10833.33,2023,12,2023-12,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,14850.0,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,13600.0,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,13200.0,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,9900.0,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,5470.83,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,5470.83,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,5470.83,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,6800.0,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,,Active,110000,9258.33,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,6733.33,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,9166.67,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,7837.5,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,9487.5,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,7425.0,2024,1,2024-01,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,6250.0,2024,1,2024-01,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,10100.0,2024,1,2024-01,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,10725.0,2024,1,2024-01,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,29400.0,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,27200.0,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,26666.67,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,20200.0,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,11050.0,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,10725.0,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,10833.33,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,13333.33,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,2024-02-01,Terminated,110000,18150.0,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,13200.0,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,18150.0,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,16150.0,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,19166.67,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,15000.0,2024,2,2024-02,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,12375.0,2024,2,2024-02,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,19800.0,2024,2,2024-02,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,21666.67,2024,2,2024-02,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,45450.0,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,40800.0,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,40400.0,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,30000.0,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,16087.5,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,16412.5,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,16087.5,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,20200.0,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,2024-02-01,Terminated,110000,27500.0,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,20400.0,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,27500.0,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,23987.5,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,28175.0,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,22050.0,2024,3,2024-03,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,18937.5,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,30600.0,2024,3,2024-03,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,32825.0,2024,3,2024-03,0.5 -AcmeSaaS Inc.,EMP1017,Operations,Finance Analyst,2024-03-01,,Active,95000,7995.83,2024,3,2024-03,1.0 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,59400.0,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,53866.67,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,52266.67,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,40000.0,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,21450.0,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,21666.67,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,21883.33,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,26666.67,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,2024-02-01,Terminated,110000,36666.67,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,26666.67,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,35933.33,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,31666.67,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,37566.67,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,30000.0,2024,4,2024-04,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,24750.0,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,40800.0,2024,4,2024-04,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,42466.67,2024,4,2024-04,0.5 -AcmeSaaS Inc.,EMP1017,Operations,Finance Analyst,2024-03-01,,Active,95000,15991.67,2024,4,2024-04,1.0 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,74250.0,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,66666.67,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,66000.0,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,49500.0,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,27354.17,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,26541.67,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,27625.0,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,33333.33,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,2024-02-01,Terminated,110000,45375.0,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,33666.67,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,45375.0,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,39979.17,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,46958.33,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,38250.0,2024,5,2024-05,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,31562.5,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,49000.0,2024,5,2024-05,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,54166.67,2024,5,2024-05,0.5 -AcmeSaaS Inc.,EMP1017,Operations,Finance Analyst,2024-03-01,,Active,95000,23750.0,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1018,Sales,Account Executive,2024-05-01,,Active,90000,7500.0,2024,5,2024-05,1.0 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,90900.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,79200.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,81600.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,61200.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,33150.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,33150.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,32175.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,40800.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,2024-02-01,Terminated,110000,55550.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,40000.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,54450.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,48450.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,58075.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,44550.0,2024,6,2024-06,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,37500.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,60600.0,2024,6,2024-06,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,64350.0,2024,6,2024-06,0.5 -AcmeSaaS Inc.,EMP1017,Operations,Finance Analyst,2024-03-01,,Active,95000,31983.33,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1018,Sales,Account Executive,2024-05-01,,Active,90000,14700.0,2024,6,2024-06,1.0 -AcmeSaaS Inc.,EMP1019,Sales,Account Executive,2024-06-01,,Active,90000,7425.0,2024,6,2024-06,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,106050.0,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,95200.0,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,92400.0,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,70000.0,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,37916.67,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,38295.83,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,38295.83,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,46666.67,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,2024-02-01,Terminated,110000,64166.67,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,47133.33,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,64808.33,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,55970.83,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,65741.67,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,53025.0,2024,7,2024-07,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,43312.5,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,68600.0,2024,7,2024-07,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,75075.0,2024,7,2024-07,0.5 -AcmeSaaS Inc.,EMP1017,Operations,Finance Analyst,2024-03-01,,Active,95000,38791.67,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1018,Sales,Account Executive,2024-05-01,,Active,90000,22500.0,2024,7,2024-07,1.0 -AcmeSaaS Inc.,EMP1019,Sales,Account Executive,2024-06-01,,Active,90000,15000.0,2024,7,2024-07,0.5 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,120000.0,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,105600.0,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,106666.67,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,80000.0,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,42900.0,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,43333.33,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,43333.33,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,53866.67,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,2024-02-01,Terminated,110000,72600.0,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,52800.0,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,71866.67,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,62700.0,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,76666.67,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,60600.0,2024,8,2024-08,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,49500.0,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,80000.0,2024,8,2024-08,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,85800.0,2024,8,2024-08,0.5 -AcmeSaaS Inc.,EMP1017,Operations,Finance Analyst,2024-03-01,,Active,95000,46550.0,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1018,Sales,Account Executive,2024-05-01,,Active,90000,30600.0,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1019,Sales,Account Executive,2024-06-01,,Active,90000,22500.0,2024,8,2024-08,0.5 -AcmeSaaS Inc.,EMP1020,Marketing,Marketing Manager,2024-08-01,,Active,110000,9258.33,2024,8,2024-08,1.0 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,135000.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,120000.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,120000.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,88200.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,47775.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,48750.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,48750.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,60600.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,2024-02-01,Terminated,110000,83325.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,58800.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,82500.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,71962.5,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,87112.5,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,67500.0,2024,9,2024-09,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,57375.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,89100.0,2024,9,2024-09,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,97500.0,2024,9,2024-09,0.5 -AcmeSaaS Inc.,EMP1017,Operations,Finance Analyst,2024-03-01,,Active,95000,55970.83,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1018,Sales,Account Executive,2024-05-01,,Active,90000,37125.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1019,Sales,Account Executive,2024-06-01,,Active,90000,29700.0,2024,9,2024-09,0.5 -AcmeSaaS Inc.,EMP1020,Marketing,Marketing Manager,2024-08-01,,Active,110000,18150.0,2024,9,2024-09,1.0 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,148500.0,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,132000.0,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,134666.67,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,99000.0,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,55250.0,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,54708.33,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,54166.67,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,66666.67,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,2024-02-01,Terminated,110000,91666.67,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,66000.0,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,89833.33,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,79166.67,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,96791.67,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,75000.0,2024,10,2024-10,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,63125.0,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,100000.0,2024,10,2024-10,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,108333.33,2024,10,2024-10,0.5 -AcmeSaaS Inc.,EMP1017,Operations,Finance Analyst,2024-03-01,,Active,95000,63966.67,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1018,Sales,Account Executive,2024-05-01,,Active,90000,45450.0,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1019,Sales,Account Executive,2024-06-01,,Active,90000,37500.0,2024,10,2024-10,0.5 -AcmeSaaS Inc.,EMP1020,Marketing,Marketing Manager,2024-08-01,,Active,110000,27225.0,2024,10,2024-10,1.0 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,165000.0,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,145200.0,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,148133.33,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,111100.0,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,60775.0,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,60179.17,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,58987.5,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,72600.0,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,2024-02-01,Terminated,110000,101841.67,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,72600.0,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,102850.0,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,86212.5,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,107525.0,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,81675.0,2024,11,2024-11,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,69437.5,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,107800.0,2024,11,2024-11,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,120358.33,2024,11,2024-11,0.5 -AcmeSaaS Inc.,EMP1017,Operations,Finance Analyst,2024-03-01,,Active,95000,72675.0,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1018,Sales,Account Executive,2024-05-01,,Active,90000,51975.0,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1019,Sales,Account Executive,2024-06-01,,Active,90000,45450.0,2024,11,2024-11,0.5 -AcmeSaaS Inc.,EMP1020,Marketing,Marketing Manager,2024-08-01,,Active,110000,37033.33,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1021,Sales,Sales Manager,2024-11-01,,Active,140000,11433.33,2024,11,2024-11,1.0 -AcmeSaaS Inc.,EMP1000,Engineering,Engineering Manager,2022-03-04,,Active,180000,178200.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1001,Engineering,Senior Engineer,2022-04-02,,Active,160000,158400.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1002,Engineering,Senior Engineer,2022-11-08,,Active,160000,158400.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1003,Engineering,Software Engineer,2022-02-14,,Active,120000,120000.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1004,Sales,SDR,2022-08-04,,Active,65000,65000.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1005,Sales,SDR,2022-03-01,2023-11-01,Terminated,65000,65000.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1006,Sales,SDR,2022-03-14,,Active,65000,65000.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1007,Marketing,Content Strategist,2022-08-21,,Active,80000,80000.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1008,Marketing,Marketing Manager,2022-05-11,2024-02-01,Terminated,110000,110000.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1009,Marketing,Content Strategist,2022-11-02,,Active,80000,81600.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1010,Marketing,Marketing Manager,2022-11-19,,Active,110000,111100.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1011,Operations,Finance Analyst,2022-12-24,2024-01-01,Terminated,95000,95000.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1012,Operations,Operations Manager,2022-03-14,,Active,115000,112700.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1013,Sales,Account Executive,2023-01-01,,Active,90000,91800.0,2024,12,2024-12,0.5 -AcmeSaaS Inc.,EMP1014,Operations,Customer Success,2023-04-01,,Active,75000,75750.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1015,Engineering,Software Engineer,2023-07-01,,Active,120000,121200.0,2024,12,2024-12,0.5 -AcmeSaaS Inc.,EMP1016,Engineering,DevOps Engineer,2023-12-01,,Active,130000,130000.0,2024,12,2024-12,0.5 -AcmeSaaS Inc.,EMP1017,Operations,Finance Analyst,2024-03-01,,Active,95000,80750.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1018,Sales,Account Executive,2024-05-01,,Active,90000,58800.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1019,Sales,Account Executive,2024-06-01,,Active,90000,51975.0,2024,12,2024-12,0.5 -AcmeSaaS Inc.,EMP1020,Marketing,Marketing Manager,2024-08-01,,Active,110000,46291.67,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1021,Sales,Sales Manager,2024-11-01,,Active,140000,23100.0,2024,12,2024-12,1.0 -AcmeSaaS Inc.,EMP1022,Operations,Finance Analyst,2024-12-01,,Active,95000,7916.67,2024,12,2024-12,0.5 diff --git a/testing/data/csv/opex_budget_vs_actuals.csv b/testing/data/csv/opex_budget_vs_actuals.csv deleted file mode 100644 index 2348afe..0000000 --- a/testing/data/csv/opex_budget_vs_actuals.csv +++ /dev/null @@ -1,673 +0,0 @@ -company,department,year,month,period,category,budget_amount,actual_amount,variance,variance_pct -AcmeSaaS Inc.,Engineering,2023,1,2023-01,Salaries,16575.56,17009.7,434.14,2.62 -AcmeSaaS Inc.,Engineering,2023,1,2023-01,Software & Tools,10980.09,10113.47,-866.62,-7.89 -AcmeSaaS Inc.,Engineering,2023,1,2023-01,Travel,11090.59,11700.47,609.88,5.5 -AcmeSaaS Inc.,Engineering,2023,1,2023-01,Marketing Spend,7771.27,7143.48,-627.79,-8.08 -AcmeSaaS Inc.,Engineering,2023,1,2023-01,Cloud Infrastructure,8958.55,8699.37,-259.18,-2.89 -AcmeSaaS Inc.,Engineering,2023,1,2023-01,Contractors,22793.22,25471.1,2677.88,11.75 -AcmeSaaS Inc.,Engineering,2023,1,2023-01,Office & Facilities,16830.71,17396.22,565.51,3.36 -AcmeSaaS Inc.,Sales,2023,1,2023-01,Salaries,11002.74,10389.42,-613.32,-5.57 -AcmeSaaS Inc.,Sales,2023,1,2023-01,Software & Tools,12943.91,12046.07,-897.84,-6.94 -AcmeSaaS Inc.,Sales,2023,1,2023-01,Travel,15349.95,16981.62,1631.67,10.63 -AcmeSaaS Inc.,Sales,2023,1,2023-01,Marketing Spend,14333.45,15628.17,1294.72,9.03 -AcmeSaaS Inc.,Sales,2023,1,2023-01,Cloud Infrastructure,6016.93,5749.31,-267.62,-4.45 -AcmeSaaS Inc.,Sales,2023,1,2023-01,Contractors,3022.29,3135.04,112.75,3.73 -AcmeSaaS Inc.,Sales,2023,1,2023-01,Office & Facilities,7330.73,7147.11,-183.62,-2.5 -AcmeSaaS Inc.,Marketing,2023,1,2023-01,Salaries,13332.55,14605.51,1272.96,9.55 -AcmeSaaS Inc.,Marketing,2023,1,2023-01,Software & Tools,7713.33,7527.1,-186.23,-2.41 -AcmeSaaS Inc.,Marketing,2023,1,2023-01,Travel,5321.44,4962.97,-358.47,-6.74 -AcmeSaaS Inc.,Marketing,2023,1,2023-01,Marketing Spend,5096.37,5704.92,608.55,11.94 -AcmeSaaS Inc.,Marketing,2023,1,2023-01,Cloud Infrastructure,8977.47,8998.0,20.53,0.23 -AcmeSaaS Inc.,Marketing,2023,1,2023-01,Contractors,5295.07,4775.19,-519.88,-9.82 -AcmeSaaS Inc.,Marketing,2023,1,2023-01,Office & Facilities,9263.77,8256.87,-1006.9,-10.87 -AcmeSaaS Inc.,Operations,2023,1,2023-01,Salaries,2424.21,2441.15,16.94,0.7 -AcmeSaaS Inc.,Operations,2023,1,2023-01,Software & Tools,6967.02,7754.7,787.68,11.31 -AcmeSaaS Inc.,Operations,2023,1,2023-01,Travel,8411.4,9139.72,728.32,8.66 -AcmeSaaS Inc.,Operations,2023,1,2023-01,Marketing Spend,5165.97,4560.29,-605.68,-11.72 -AcmeSaaS Inc.,Operations,2023,1,2023-01,Cloud Infrastructure,2019.57,2126.55,106.98,5.3 -AcmeSaaS Inc.,Operations,2023,1,2023-01,Contractors,4810.3,5020.08,209.78,4.36 -AcmeSaaS Inc.,Operations,2023,1,2023-01,Office & Facilities,10201.53,10292.05,90.52,0.89 -AcmeSaaS Inc.,Engineering,2023,2,2023-02,Salaries,8498.07,8015.49,-482.58,-5.68 -AcmeSaaS Inc.,Engineering,2023,2,2023-02,Software & Tools,15832.55,15834.78,2.23,0.01 -AcmeSaaS Inc.,Engineering,2023,2,2023-02,Travel,5454.13,5033.49,-420.64,-7.71 -AcmeSaaS Inc.,Engineering,2023,2,2023-02,Marketing Spend,11790.32,12957.92,1167.6,9.9 -AcmeSaaS Inc.,Engineering,2023,2,2023-02,Cloud Infrastructure,12161.98,13243.48,1081.5,8.89 -AcmeSaaS Inc.,Engineering,2023,2,2023-02,Contractors,21965.66,20903.11,-1062.55,-4.84 -AcmeSaaS Inc.,Engineering,2023,2,2023-02,Office & Facilities,20437.29,21118.83,681.54,3.33 -AcmeSaaS Inc.,Sales,2023,2,2023-02,Salaries,12174.64,11660.84,-513.8,-4.22 -AcmeSaaS Inc.,Sales,2023,2,2023-02,Software & Tools,5015.07,4436.7,-578.37,-11.53 -AcmeSaaS Inc.,Sales,2023,2,2023-02,Travel,14584.67,16086.65,1501.98,10.3 -AcmeSaaS Inc.,Sales,2023,2,2023-02,Marketing Spend,11082.32,12089.63,1007.31,9.09 -AcmeSaaS Inc.,Sales,2023,2,2023-02,Cloud Infrastructure,14837.62,16018.69,1181.07,7.96 -AcmeSaaS Inc.,Sales,2023,2,2023-02,Contractors,10940.65,10435.23,-505.42,-4.62 -AcmeSaaS Inc.,Sales,2023,2,2023-02,Office & Facilities,2625.03,2346.52,-278.51,-10.61 -AcmeSaaS Inc.,Marketing,2023,2,2023-02,Salaries,11304.51,10296.31,-1008.2,-8.92 -AcmeSaaS Inc.,Marketing,2023,2,2023-02,Software & Tools,12050.51,11979.02,-71.49,-0.59 -AcmeSaaS Inc.,Marketing,2023,2,2023-02,Travel,2730.37,2763.01,32.64,1.2 -AcmeSaaS Inc.,Marketing,2023,2,2023-02,Marketing Spend,7062.45,6664.22,-398.23,-5.64 -AcmeSaaS Inc.,Marketing,2023,2,2023-02,Cloud Infrastructure,2552.46,2780.61,228.15,8.94 -AcmeSaaS Inc.,Marketing,2023,2,2023-02,Contractors,10034.04,9848.94,-185.1,-1.84 -AcmeSaaS Inc.,Marketing,2023,2,2023-02,Office & Facilities,10090.66,9392.7,-697.96,-6.92 -AcmeSaaS Inc.,Operations,2023,2,2023-02,Salaries,5656.81,5680.67,23.86,0.42 -AcmeSaaS Inc.,Operations,2023,2,2023-02,Software & Tools,7184.34,6530.86,-653.48,-9.1 -AcmeSaaS Inc.,Operations,2023,2,2023-02,Travel,2947.29,2752.55,-194.74,-6.61 -AcmeSaaS Inc.,Operations,2023,2,2023-02,Marketing Spend,3833.23,3684.27,-148.96,-3.89 -AcmeSaaS Inc.,Operations,2023,2,2023-02,Cloud Infrastructure,9309.51,9506.82,197.31,2.12 -AcmeSaaS Inc.,Operations,2023,2,2023-02,Contractors,6542.89,6119.09,-423.8,-6.48 -AcmeSaaS Inc.,Operations,2023,2,2023-02,Office & Facilities,4845.93,4520.54,-325.39,-6.71 -AcmeSaaS Inc.,Engineering,2023,3,2023-03,Salaries,5542.88,5767.67,224.79,4.06 -AcmeSaaS Inc.,Engineering,2023,3,2023-03,Software & Tools,18606.18,17330.11,-1276.07,-6.86 -AcmeSaaS Inc.,Engineering,2023,3,2023-03,Travel,9226.68,8412.47,-814.21,-8.82 -AcmeSaaS Inc.,Engineering,2023,3,2023-03,Marketing Spend,25004.01,27617.51,2613.5,10.45 -AcmeSaaS Inc.,Engineering,2023,3,2023-03,Cloud Infrastructure,23936.18,24344.3,408.12,1.71 -AcmeSaaS Inc.,Engineering,2023,3,2023-03,Contractors,5539.71,5503.38,-36.33,-0.66 -AcmeSaaS Inc.,Engineering,2023,3,2023-03,Office & Facilities,9438.05,10082.75,644.7,6.83 -AcmeSaaS Inc.,Sales,2023,3,2023-03,Salaries,16387.89,17069.75,681.86,4.16 -AcmeSaaS Inc.,Sales,2023,3,2023-03,Software & Tools,6006.93,6704.93,698.0,11.62 -AcmeSaaS Inc.,Sales,2023,3,2023-03,Travel,4434.38,4007.0,-427.38,-9.64 -AcmeSaaS Inc.,Sales,2023,3,2023-03,Marketing Spend,10055.12,9820.12,-235.0,-2.34 -AcmeSaaS Inc.,Sales,2023,3,2023-03,Cloud Infrastructure,9929.42,9546.47,-382.95,-3.86 -AcmeSaaS Inc.,Sales,2023,3,2023-03,Contractors,10660.29,11585.62,925.33,8.68 -AcmeSaaS Inc.,Sales,2023,3,2023-03,Office & Facilities,15068.65,14159.67,-908.98,-6.03 -AcmeSaaS Inc.,Marketing,2023,3,2023-03,Salaries,4905.59,5331.02,425.43,8.67 -AcmeSaaS Inc.,Marketing,2023,3,2023-03,Software & Tools,8457.6,8559.75,102.15,1.21 -AcmeSaaS Inc.,Marketing,2023,3,2023-03,Travel,8090.15,7217.56,-872.59,-10.79 -AcmeSaaS Inc.,Marketing,2023,3,2023-03,Marketing Spend,6119.85,6853.18,733.33,11.98 -AcmeSaaS Inc.,Marketing,2023,3,2023-03,Cloud Infrastructure,5724.81,6186.5,461.69,8.06 -AcmeSaaS Inc.,Marketing,2023,3,2023-03,Contractors,14982.14,16668.52,1686.38,11.26 -AcmeSaaS Inc.,Marketing,2023,3,2023-03,Office & Facilities,8382.24,9239.98,857.74,10.23 -AcmeSaaS Inc.,Operations,2023,3,2023-03,Salaries,11094.12,12386.3,1292.18,11.65 -AcmeSaaS Inc.,Operations,2023,3,2023-03,Software & Tools,3638.2,3433.18,-205.02,-5.64 -AcmeSaaS Inc.,Operations,2023,3,2023-03,Travel,7127.29,7613.21,485.92,6.82 -AcmeSaaS Inc.,Operations,2023,3,2023-03,Marketing Spend,4156.51,4111.63,-44.88,-1.08 -AcmeSaaS Inc.,Operations,2023,3,2023-03,Cloud Infrastructure,6202.92,6088.3,-114.62,-1.85 -AcmeSaaS Inc.,Operations,2023,3,2023-03,Contractors,2461.71,2731.9,270.19,10.98 -AcmeSaaS Inc.,Operations,2023,3,2023-03,Office & Facilities,5961.81,6670.68,708.87,11.89 -AcmeSaaS Inc.,Engineering,2023,4,2023-04,Salaries,14276.52,15126.17,849.65,5.95 -AcmeSaaS Inc.,Engineering,2023,4,2023-04,Software & Tools,17490.56,15631.66,-1858.9,-10.63 -AcmeSaaS Inc.,Engineering,2023,4,2023-04,Travel,6352.66,6481.0,128.34,2.02 -AcmeSaaS Inc.,Engineering,2023,4,2023-04,Marketing Spend,9157.05,9163.31,6.26,0.07 -AcmeSaaS Inc.,Engineering,2023,4,2023-04,Cloud Infrastructure,22436.93,24336.28,1899.35,8.47 -AcmeSaaS Inc.,Engineering,2023,4,2023-04,Contractors,14739.18,13527.38,-1211.8,-8.22 -AcmeSaaS Inc.,Engineering,2023,4,2023-04,Office & Facilities,14008.3,15557.43,1549.13,11.06 -AcmeSaaS Inc.,Sales,2023,4,2023-04,Salaries,4615.79,4334.65,-281.14,-6.09 -AcmeSaaS Inc.,Sales,2023,4,2023-04,Software & Tools,6593.07,6742.63,149.56,2.27 -AcmeSaaS Inc.,Sales,2023,4,2023-04,Travel,14247.02,14655.22,408.2,2.87 -AcmeSaaS Inc.,Sales,2023,4,2023-04,Marketing Spend,15746.68,15441.41,-305.27,-1.94 -AcmeSaaS Inc.,Sales,2023,4,2023-04,Cloud Infrastructure,7516.67,7667.61,150.94,2.01 -AcmeSaaS Inc.,Sales,2023,4,2023-04,Contractors,5359.75,5389.06,29.31,0.55 -AcmeSaaS Inc.,Sales,2023,4,2023-04,Office & Facilities,19769.48,21832.02,2062.54,10.43 -AcmeSaaS Inc.,Marketing,2023,4,2023-04,Salaries,5320.63,5642.25,321.62,6.04 -AcmeSaaS Inc.,Marketing,2023,4,2023-04,Software & Tools,12663.88,11364.7,-1299.18,-10.26 -AcmeSaaS Inc.,Marketing,2023,4,2023-04,Travel,5814.45,5756.24,-58.21,-1.0 -AcmeSaaS Inc.,Marketing,2023,4,2023-04,Marketing Spend,8067.92,9033.08,965.16,11.96 -AcmeSaaS Inc.,Marketing,2023,4,2023-04,Cloud Infrastructure,12025.54,13457.34,1431.8,11.91 -AcmeSaaS Inc.,Marketing,2023,4,2023-04,Contractors,6693.91,6008.34,-685.57,-10.24 -AcmeSaaS Inc.,Marketing,2023,4,2023-04,Office & Facilities,6926.0,6449.19,-476.81,-6.88 -AcmeSaaS Inc.,Operations,2023,4,2023-04,Salaries,3224.88,3382.41,157.53,4.88 -AcmeSaaS Inc.,Operations,2023,4,2023-04,Software & Tools,8213.48,8433.62,220.14,2.68 -AcmeSaaS Inc.,Operations,2023,4,2023-04,Travel,7822.23,8736.93,914.7,11.69 -AcmeSaaS Inc.,Operations,2023,4,2023-04,Marketing Spend,7810.33,8098.96,288.63,3.7 -AcmeSaaS Inc.,Operations,2023,4,2023-04,Cloud Infrastructure,4003.92,3530.97,-472.95,-11.81 -AcmeSaaS Inc.,Operations,2023,4,2023-04,Contractors,2422.49,2606.85,184.36,7.61 -AcmeSaaS Inc.,Operations,2023,4,2023-04,Office & Facilities,7470.37,7110.68,-359.69,-4.81 -AcmeSaaS Inc.,Engineering,2023,5,2023-05,Salaries,20932.03,21458.66,526.63,2.52 -AcmeSaaS Inc.,Engineering,2023,5,2023-05,Software & Tools,27880.52,29336.63,1456.11,5.22 -AcmeSaaS Inc.,Engineering,2023,5,2023-05,Travel,7589.44,7049.55,-539.89,-7.11 -AcmeSaaS Inc.,Engineering,2023,5,2023-05,Marketing Spend,7113.78,7342.97,229.19,3.22 -AcmeSaaS Inc.,Engineering,2023,5,2023-05,Cloud Infrastructure,6902.13,6511.17,-390.96,-5.66 -AcmeSaaS Inc.,Engineering,2023,5,2023-05,Contractors,18153.93,18103.96,-49.97,-0.28 -AcmeSaaS Inc.,Engineering,2023,5,2023-05,Office & Facilities,11070.91,12147.9,1076.99,9.73 -AcmeSaaS Inc.,Sales,2023,5,2023-05,Salaries,18054.52,17023.05,-1031.47,-5.71 -AcmeSaaS Inc.,Sales,2023,5,2023-05,Software & Tools,4616.54,4883.82,267.28,5.79 -AcmeSaaS Inc.,Sales,2023,5,2023-05,Travel,10522.17,10652.68,130.51,1.24 -AcmeSaaS Inc.,Sales,2023,5,2023-05,Marketing Spend,7903.48,7766.31,-137.17,-1.74 -AcmeSaaS Inc.,Sales,2023,5,2023-05,Cloud Infrastructure,3034.35,2677.27,-357.08,-11.77 -AcmeSaaS Inc.,Sales,2023,5,2023-05,Contractors,16717.78,15013.54,-1704.24,-10.19 -AcmeSaaS Inc.,Sales,2023,5,2023-05,Office & Facilities,14328.88,15646.36,1317.48,9.19 -AcmeSaaS Inc.,Marketing,2023,5,2023-05,Salaries,13535.8,14831.93,1296.13,9.58 -AcmeSaaS Inc.,Marketing,2023,5,2023-05,Software & Tools,9005.24,9645.24,640.0,7.11 -AcmeSaaS Inc.,Marketing,2023,5,2023-05,Travel,12659.2,13755.09,1095.89,8.66 -AcmeSaaS Inc.,Marketing,2023,5,2023-05,Marketing Spend,9472.02,10378.89,906.87,9.57 -AcmeSaaS Inc.,Marketing,2023,5,2023-05,Cloud Infrastructure,3979.6,3702.69,-276.91,-6.96 -AcmeSaaS Inc.,Marketing,2023,5,2023-05,Contractors,3718.53,3495.0,-223.53,-6.01 -AcmeSaaS Inc.,Marketing,2023,5,2023-05,Office & Facilities,6004.6,5432.18,-572.42,-9.53 -AcmeSaaS Inc.,Operations,2023,5,2023-05,Salaries,6732.88,7502.38,769.5,11.43 -AcmeSaaS Inc.,Operations,2023,5,2023-05,Software & Tools,7472.59,8029.93,557.34,7.46 -AcmeSaaS Inc.,Operations,2023,5,2023-05,Travel,4075.1,4448.13,373.03,9.15 -AcmeSaaS Inc.,Operations,2023,5,2023-05,Marketing Spend,5598.95,4960.38,-638.57,-11.41 -AcmeSaaS Inc.,Operations,2023,5,2023-05,Cloud Infrastructure,2284.3,2413.99,129.69,5.68 -AcmeSaaS Inc.,Operations,2023,5,2023-05,Contractors,7797.91,7483.85,-314.06,-4.03 -AcmeSaaS Inc.,Operations,2023,5,2023-05,Office & Facilities,7333.71,8091.98,758.27,10.34 -AcmeSaaS Inc.,Engineering,2023,6,2023-06,Salaries,17206.74,18687.59,1480.85,8.61 -AcmeSaaS Inc.,Engineering,2023,6,2023-06,Software & Tools,18304.76,17085.37,-1219.39,-6.66 -AcmeSaaS Inc.,Engineering,2023,6,2023-06,Travel,17357.94,18676.81,1318.87,7.6 -AcmeSaaS Inc.,Engineering,2023,6,2023-06,Marketing Spend,7698.04,7624.7,-73.34,-0.95 -AcmeSaaS Inc.,Engineering,2023,6,2023-06,Cloud Infrastructure,16942.83,16150.68,-792.15,-4.68 -AcmeSaaS Inc.,Engineering,2023,6,2023-06,Contractors,4879.51,5225.38,345.87,7.09 -AcmeSaaS Inc.,Engineering,2023,6,2023-06,Office & Facilities,18448.65,17242.53,-1206.12,-6.54 -AcmeSaaS Inc.,Sales,2023,6,2023-06,Salaries,3263.35,3184.78,-78.57,-2.41 -AcmeSaaS Inc.,Sales,2023,6,2023-06,Software & Tools,6168.95,6881.32,712.37,11.55 -AcmeSaaS Inc.,Sales,2023,6,2023-06,Travel,8485.88,8559.64,73.76,0.87 -AcmeSaaS Inc.,Sales,2023,6,2023-06,Marketing Spend,17677.52,19541.03,1863.51,10.54 -AcmeSaaS Inc.,Sales,2023,6,2023-06,Cloud Infrastructure,19435.57,17641.32,-1794.25,-9.23 -AcmeSaaS Inc.,Sales,2023,6,2023-06,Contractors,7643.4,8506.31,862.91,11.29 -AcmeSaaS Inc.,Sales,2023,6,2023-06,Office & Facilities,13856.24,12787.32,-1068.92,-7.71 -AcmeSaaS Inc.,Marketing,2023,6,2023-06,Salaries,14588.95,14628.95,40.0,0.27 -AcmeSaaS Inc.,Marketing,2023,6,2023-06,Software & Tools,5583.03,5429.2,-153.83,-2.76 -AcmeSaaS Inc.,Marketing,2023,6,2023-06,Travel,3553.81,3619.13,65.32,1.84 -AcmeSaaS Inc.,Marketing,2023,6,2023-06,Marketing Spend,7767.72,7310.46,-457.26,-5.89 -AcmeSaaS Inc.,Marketing,2023,6,2023-06,Cloud Infrastructure,11565.87,12145.42,579.55,5.01 -AcmeSaaS Inc.,Marketing,2023,6,2023-06,Contractors,6205.9,5463.71,-742.19,-11.96 -AcmeSaaS Inc.,Marketing,2023,6,2023-06,Office & Facilities,9985.33,11005.21,1019.88,10.21 -AcmeSaaS Inc.,Operations,2023,6,2023-06,Salaries,5946.87,5704.52,-242.35,-4.08 -AcmeSaaS Inc.,Operations,2023,6,2023-06,Software & Tools,7473.21,7139.45,-333.76,-4.47 -AcmeSaaS Inc.,Operations,2023,6,2023-06,Travel,7663.14,8303.19,640.05,8.35 -AcmeSaaS Inc.,Operations,2023,6,2023-06,Marketing Spend,7061.63,7434.07,372.44,5.27 -AcmeSaaS Inc.,Operations,2023,6,2023-06,Cloud Infrastructure,4477.43,4262.86,-214.57,-4.79 -AcmeSaaS Inc.,Operations,2023,6,2023-06,Contractors,1995.79,1904.44,-91.35,-4.58 -AcmeSaaS Inc.,Operations,2023,6,2023-06,Office & Facilities,7007.73,6853.66,-154.07,-2.2 -AcmeSaaS Inc.,Engineering,2023,7,2023-07,Salaries,11772.37,12098.74,326.37,2.77 -AcmeSaaS Inc.,Engineering,2023,7,2023-07,Software & Tools,9564.12,9107.22,-456.9,-4.78 -AcmeSaaS Inc.,Engineering,2023,7,2023-07,Travel,6081.08,6151.04,69.96,1.15 -AcmeSaaS Inc.,Engineering,2023,7,2023-07,Marketing Spend,12145.69,10689.39,-1456.3,-11.99 -AcmeSaaS Inc.,Engineering,2023,7,2023-07,Cloud Infrastructure,22901.3,21730.11,-1171.19,-5.11 -AcmeSaaS Inc.,Engineering,2023,7,2023-07,Contractors,17459.63,17165.84,-293.79,-1.68 -AcmeSaaS Inc.,Engineering,2023,7,2023-07,Office & Facilities,22124.33,22549.04,424.71,1.92 -AcmeSaaS Inc.,Sales,2023,7,2023-07,Salaries,12516.47,11524.24,-992.23,-7.93 -AcmeSaaS Inc.,Sales,2023,7,2023-07,Software & Tools,9625.46,8666.29,-959.17,-9.96 -AcmeSaaS Inc.,Sales,2023,7,2023-07,Travel,9277.59,9312.0,34.41,0.37 -AcmeSaaS Inc.,Sales,2023,7,2023-07,Marketing Spend,5796.23,5981.16,184.93,3.19 -AcmeSaaS Inc.,Sales,2023,7,2023-07,Cloud Infrastructure,9750.39,9364.72,-385.67,-3.96 -AcmeSaaS Inc.,Sales,2023,7,2023-07,Contractors,16272.38,17515.94,1243.56,7.64 -AcmeSaaS Inc.,Sales,2023,7,2023-07,Office & Facilities,14669.96,15554.16,884.2,6.03 -AcmeSaaS Inc.,Marketing,2023,7,2023-07,Salaries,13087.85,11746.07,-1341.78,-10.25 -AcmeSaaS Inc.,Marketing,2023,7,2023-07,Software & Tools,6100.78,5975.51,-125.27,-2.05 -AcmeSaaS Inc.,Marketing,2023,7,2023-07,Travel,5703.04,5880.65,177.61,3.11 -AcmeSaaS Inc.,Marketing,2023,7,2023-07,Marketing Spend,2979.27,2760.78,-218.49,-7.33 -AcmeSaaS Inc.,Marketing,2023,7,2023-07,Cloud Infrastructure,6415.74,6718.08,302.34,4.71 -AcmeSaaS Inc.,Marketing,2023,7,2023-07,Contractors,10006.19,9992.69,-13.5,-0.13 -AcmeSaaS Inc.,Marketing,2023,7,2023-07,Office & Facilities,15846.51,14872.84,-973.67,-6.14 -AcmeSaaS Inc.,Operations,2023,7,2023-07,Salaries,8509.09,9444.34,935.25,10.99 -AcmeSaaS Inc.,Operations,2023,7,2023-07,Software & Tools,1781.11,1788.79,7.68,0.43 -AcmeSaaS Inc.,Operations,2023,7,2023-07,Travel,9490.67,8466.18,-1024.49,-10.79 -AcmeSaaS Inc.,Operations,2023,7,2023-07,Marketing Spend,9688.03,9104.88,-583.15,-6.02 -AcmeSaaS Inc.,Operations,2023,7,2023-07,Cloud Infrastructure,2826.15,3062.42,236.27,8.36 -AcmeSaaS Inc.,Operations,2023,7,2023-07,Contractors,6120.87,6056.91,-63.96,-1.04 -AcmeSaaS Inc.,Operations,2023,7,2023-07,Office & Facilities,3542.89,3799.18,256.29,7.23 -AcmeSaaS Inc.,Engineering,2023,8,2023-08,Salaries,13071.65,13086.64,14.99,0.11 -AcmeSaaS Inc.,Engineering,2023,8,2023-08,Software & Tools,18090.61,19525.86,1435.25,7.93 -AcmeSaaS Inc.,Engineering,2023,8,2023-08,Travel,11941.53,12078.73,137.2,1.15 -AcmeSaaS Inc.,Engineering,2023,8,2023-08,Marketing Spend,17497.5,19165.54,1668.04,9.53 -AcmeSaaS Inc.,Engineering,2023,8,2023-08,Cloud Infrastructure,16579.09,17548.59,969.5,5.85 -AcmeSaaS Inc.,Engineering,2023,8,2023-08,Contractors,12211.03,12136.81,-74.22,-0.61 -AcmeSaaS Inc.,Engineering,2023,8,2023-08,Office & Facilities,13881.67,13079.39,-802.28,-5.78 -AcmeSaaS Inc.,Sales,2023,8,2023-08,Salaries,7603.28,7212.28,-391.0,-5.14 -AcmeSaaS Inc.,Sales,2023,8,2023-08,Software & Tools,14775.16,13965.65,-809.51,-5.48 -AcmeSaaS Inc.,Sales,2023,8,2023-08,Travel,17129.27,16388.09,-741.18,-4.33 -AcmeSaaS Inc.,Sales,2023,8,2023-08,Marketing Spend,12637.65,12759.43,121.78,0.96 -AcmeSaaS Inc.,Sales,2023,8,2023-08,Cloud Infrastructure,14574.7,13309.76,-1264.94,-8.68 -AcmeSaaS Inc.,Sales,2023,8,2023-08,Contractors,8105.83,7583.03,-522.8,-6.45 -AcmeSaaS Inc.,Sales,2023,8,2023-08,Office & Facilities,4484.93,4693.69,208.76,4.65 -AcmeSaaS Inc.,Marketing,2023,8,2023-08,Salaries,13559.97,14877.47,1317.5,9.72 -AcmeSaaS Inc.,Marketing,2023,8,2023-08,Software & Tools,3586.06,3658.42,72.36,2.02 -AcmeSaaS Inc.,Marketing,2023,8,2023-08,Travel,8918.97,9337.5,418.53,4.69 -AcmeSaaS Inc.,Marketing,2023,8,2023-08,Marketing Spend,11015.85,11958.98,943.13,8.56 -AcmeSaaS Inc.,Marketing,2023,8,2023-08,Cloud Infrastructure,9045.94,9622.55,576.61,6.37 -AcmeSaaS Inc.,Marketing,2023,8,2023-08,Contractors,5800.88,5634.35,-166.53,-2.87 -AcmeSaaS Inc.,Marketing,2023,8,2023-08,Office & Facilities,9113.8,8033.04,-1080.76,-11.86 -AcmeSaaS Inc.,Operations,2023,8,2023-08,Salaries,3786.02,3879.84,93.82,2.48 -AcmeSaaS Inc.,Operations,2023,8,2023-08,Software & Tools,6719.72,6269.02,-450.7,-6.71 -AcmeSaaS Inc.,Operations,2023,8,2023-08,Travel,7449.82,6948.16,-501.66,-6.73 -AcmeSaaS Inc.,Operations,2023,8,2023-08,Marketing Spend,8179.98,8054.01,-125.97,-1.54 -AcmeSaaS Inc.,Operations,2023,8,2023-08,Cloud Infrastructure,4277.23,3793.76,-483.47,-11.3 -AcmeSaaS Inc.,Operations,2023,8,2023-08,Contractors,6676.2,6413.63,-262.57,-3.93 -AcmeSaaS Inc.,Operations,2023,8,2023-08,Office & Facilities,5205.51,5429.32,223.81,4.3 -AcmeSaaS Inc.,Engineering,2023,9,2023-09,Salaries,17685.15,17958.46,273.31,1.55 -AcmeSaaS Inc.,Engineering,2023,9,2023-09,Software & Tools,10274.15,9108.08,-1166.07,-11.35 -AcmeSaaS Inc.,Engineering,2023,9,2023-09,Travel,19638.74,20311.56,672.82,3.43 -AcmeSaaS Inc.,Engineering,2023,9,2023-09,Marketing Spend,9115.23,8318.27,-796.96,-8.74 -AcmeSaaS Inc.,Engineering,2023,9,2023-09,Cloud Infrastructure,24435.45,24210.83,-224.62,-0.92 -AcmeSaaS Inc.,Engineering,2023,9,2023-09,Contractors,5997.43,5350.12,-647.31,-10.79 -AcmeSaaS Inc.,Engineering,2023,9,2023-09,Office & Facilities,17366.24,16862.36,-503.88,-2.9 -AcmeSaaS Inc.,Sales,2023,9,2023-09,Salaries,6524.4,5869.73,-654.67,-10.03 -AcmeSaaS Inc.,Sales,2023,9,2023-09,Software & Tools,8510.82,7529.11,-981.71,-11.53 -AcmeSaaS Inc.,Sales,2023,9,2023-09,Travel,16001.95,16153.34,151.39,0.95 -AcmeSaaS Inc.,Sales,2023,9,2023-09,Marketing Spend,9412.42,10541.7,1129.28,12.0 -AcmeSaaS Inc.,Sales,2023,9,2023-09,Cloud Infrastructure,15842.95,15272.45,-570.5,-3.6 -AcmeSaaS Inc.,Sales,2023,9,2023-09,Contractors,17221.11,17841.67,620.56,3.6 -AcmeSaaS Inc.,Sales,2023,9,2023-09,Office & Facilities,7224.76,7712.4,487.64,6.75 -AcmeSaaS Inc.,Marketing,2023,9,2023-09,Salaries,12611.77,13124.69,512.92,4.07 -AcmeSaaS Inc.,Marketing,2023,9,2023-09,Software & Tools,14190.95,14408.82,217.87,1.54 -AcmeSaaS Inc.,Marketing,2023,9,2023-09,Travel,17201.71,16037.35,-1164.36,-6.77 -AcmeSaaS Inc.,Marketing,2023,9,2023-09,Marketing Spend,5640.43,5910.45,270.02,4.79 -AcmeSaaS Inc.,Marketing,2023,9,2023-09,Cloud Infrastructure,2882.37,3067.0,184.63,6.41 -AcmeSaaS Inc.,Marketing,2023,9,2023-09,Contractors,4916.5,4524.5,-392.0,-7.97 -AcmeSaaS Inc.,Marketing,2023,9,2023-09,Office & Facilities,4513.36,4629.53,116.17,2.57 -AcmeSaaS Inc.,Operations,2023,9,2023-09,Salaries,9155.38,9545.06,389.68,4.26 -AcmeSaaS Inc.,Operations,2023,9,2023-09,Software & Tools,2814.9,3124.43,309.53,11.0 -AcmeSaaS Inc.,Operations,2023,9,2023-09,Travel,9869.87,9625.07,-244.8,-2.48 -AcmeSaaS Inc.,Operations,2023,9,2023-09,Marketing Spend,11325.57,11910.01,584.44,5.16 -AcmeSaaS Inc.,Operations,2023,9,2023-09,Cloud Infrastructure,2750.49,2470.6,-279.89,-10.18 -AcmeSaaS Inc.,Operations,2023,9,2023-09,Contractors,1925.44,2013.52,88.08,4.57 -AcmeSaaS Inc.,Operations,2023,9,2023-09,Office & Facilities,4791.19,4937.5,146.31,3.05 -AcmeSaaS Inc.,Engineering,2023,10,2023-10,Salaries,5280.53,5086.89,-193.64,-3.67 -AcmeSaaS Inc.,Engineering,2023,10,2023-10,Software & Tools,18465.34,18147.93,-317.41,-1.72 -AcmeSaaS Inc.,Engineering,2023,10,2023-10,Travel,19995.27,19374.16,-621.11,-3.11 -AcmeSaaS Inc.,Engineering,2023,10,2023-10,Marketing Spend,15082.14,15103.72,21.58,0.14 -AcmeSaaS Inc.,Engineering,2023,10,2023-10,Cloud Infrastructure,5657.13,5441.57,-215.56,-3.81 -AcmeSaaS Inc.,Engineering,2023,10,2023-10,Contractors,22621.12,24518.99,1897.87,8.39 -AcmeSaaS Inc.,Engineering,2023,10,2023-10,Office & Facilities,18664.99,20108.9,1443.91,7.74 -AcmeSaaS Inc.,Sales,2023,10,2023-10,Salaries,4013.91,4462.32,448.41,11.17 -AcmeSaaS Inc.,Sales,2023,10,2023-10,Software & Tools,16625.3,15707.91,-917.39,-5.52 -AcmeSaaS Inc.,Sales,2023,10,2023-10,Travel,11829.91,12704.94,875.03,7.4 -AcmeSaaS Inc.,Sales,2023,10,2023-10,Marketing Spend,14677.67,14812.14,134.47,0.92 -AcmeSaaS Inc.,Sales,2023,10,2023-10,Cloud Infrastructure,12887.53,12836.49,-51.04,-0.4 -AcmeSaaS Inc.,Sales,2023,10,2023-10,Contractors,8879.29,8742.0,-137.29,-1.55 -AcmeSaaS Inc.,Sales,2023,10,2023-10,Office & Facilities,13278.1,14014.32,736.22,5.54 -AcmeSaaS Inc.,Marketing,2023,10,2023-10,Salaries,5706.59,5857.7,151.11,2.65 -AcmeSaaS Inc.,Marketing,2023,10,2023-10,Software & Tools,13357.8,12969.86,-387.94,-2.9 -AcmeSaaS Inc.,Marketing,2023,10,2023-10,Travel,13082.59,11602.79,-1479.8,-11.31 -AcmeSaaS Inc.,Marketing,2023,10,2023-10,Marketing Spend,3322.85,3602.73,279.88,8.42 -AcmeSaaS Inc.,Marketing,2023,10,2023-10,Cloud Infrastructure,13750.23,12700.28,-1049.95,-7.64 -AcmeSaaS Inc.,Marketing,2023,10,2023-10,Contractors,5384.81,5012.77,-372.04,-6.91 -AcmeSaaS Inc.,Marketing,2023,10,2023-10,Office & Facilities,8281.57,8873.53,591.96,7.15 -AcmeSaaS Inc.,Operations,2023,10,2023-10,Salaries,4942.15,5203.18,261.03,5.28 -AcmeSaaS Inc.,Operations,2023,10,2023-10,Software & Tools,10205.75,10177.77,-27.98,-0.27 -AcmeSaaS Inc.,Operations,2023,10,2023-10,Travel,8459.57,8983.72,524.15,6.2 -AcmeSaaS Inc.,Operations,2023,10,2023-10,Marketing Spend,4317.61,4515.12,197.51,4.57 -AcmeSaaS Inc.,Operations,2023,10,2023-10,Cloud Infrastructure,1723.57,1783.92,60.35,3.5 -AcmeSaaS Inc.,Operations,2023,10,2023-10,Contractors,10866.08,10842.14,-23.94,-0.22 -AcmeSaaS Inc.,Operations,2023,10,2023-10,Office & Facilities,2459.15,2632.04,172.89,7.03 -AcmeSaaS Inc.,Engineering,2023,11,2023-11,Salaries,6838.62,6716.35,-122.27,-1.79 -AcmeSaaS Inc.,Engineering,2023,11,2023-11,Software & Tools,10223.25,10826.67,603.42,5.9 -AcmeSaaS Inc.,Engineering,2023,11,2023-11,Travel,22603.73,21685.79,-917.94,-4.06 -AcmeSaaS Inc.,Engineering,2023,11,2023-11,Marketing Spend,12451.09,13057.27,606.18,4.87 -AcmeSaaS Inc.,Engineering,2023,11,2023-11,Cloud Infrastructure,19701.25,18618.07,-1083.18,-5.5 -AcmeSaaS Inc.,Engineering,2023,11,2023-11,Contractors,16849.76,15844.45,-1005.31,-5.97 -AcmeSaaS Inc.,Engineering,2023,11,2023-11,Office & Facilities,18368.03,16695.76,-1672.27,-9.1 -AcmeSaaS Inc.,Sales,2023,11,2023-11,Salaries,8207.01,8649.37,442.36,5.39 -AcmeSaaS Inc.,Sales,2023,11,2023-11,Software & Tools,6538.67,7286.6,747.93,11.44 -AcmeSaaS Inc.,Sales,2023,11,2023-11,Travel,16049.17,16144.07,94.9,0.59 -AcmeSaaS Inc.,Sales,2023,11,2023-11,Marketing Spend,21219.53,20114.41,-1105.12,-5.21 -AcmeSaaS Inc.,Sales,2023,11,2023-11,Cloud Infrastructure,8037.18,7266.63,-770.55,-9.59 -AcmeSaaS Inc.,Sales,2023,11,2023-11,Contractors,8750.73,8108.32,-642.41,-7.34 -AcmeSaaS Inc.,Sales,2023,11,2023-11,Office & Facilities,14868.88,13896.4,-972.48,-6.54 -AcmeSaaS Inc.,Marketing,2023,11,2023-11,Salaries,5028.02,4577.04,-450.98,-8.97 -AcmeSaaS Inc.,Marketing,2023,11,2023-11,Software & Tools,2626.76,2859.05,232.29,8.84 -AcmeSaaS Inc.,Marketing,2023,11,2023-11,Travel,10180.77,10158.48,-22.29,-0.22 -AcmeSaaS Inc.,Marketing,2023,11,2023-11,Marketing Spend,6406.23,6979.28,573.05,8.95 -AcmeSaaS Inc.,Marketing,2023,11,2023-11,Cloud Infrastructure,16575.11,16869.74,294.63,1.78 -AcmeSaaS Inc.,Marketing,2023,11,2023-11,Contractors,10460.04,10383.21,-76.83,-0.73 -AcmeSaaS Inc.,Marketing,2023,11,2023-11,Office & Facilities,12552.82,12373.47,-179.35,-1.43 -AcmeSaaS Inc.,Operations,2023,11,2023-11,Salaries,3692.43,3807.14,114.71,3.11 -AcmeSaaS Inc.,Operations,2023,11,2023-11,Software & Tools,2293.56,2047.84,-245.72,-10.71 -AcmeSaaS Inc.,Operations,2023,11,2023-11,Travel,11652.04,10671.02,-981.02,-8.42 -AcmeSaaS Inc.,Operations,2023,11,2023-11,Marketing Spend,6778.3,6880.53,102.23,1.51 -AcmeSaaS Inc.,Operations,2023,11,2023-11,Cloud Infrastructure,10400.84,9911.17,-489.67,-4.71 -AcmeSaaS Inc.,Operations,2023,11,2023-11,Contractors,5968.12,6675.58,707.46,11.85 -AcmeSaaS Inc.,Operations,2023,11,2023-11,Office & Facilities,2532.4,2300.5,-231.9,-9.16 -AcmeSaaS Inc.,Engineering,2023,12,2023-12,Salaries,20294.73,22049.01,1754.28,8.64 -AcmeSaaS Inc.,Engineering,2023,12,2023-12,Software & Tools,16848.18,18829.65,1981.47,11.76 -AcmeSaaS Inc.,Engineering,2023,12,2023-12,Travel,20867.92,19893.21,-974.71,-4.67 -AcmeSaaS Inc.,Engineering,2023,12,2023-12,Marketing Spend,8551.85,8800.25,248.4,2.9 -AcmeSaaS Inc.,Engineering,2023,12,2023-12,Cloud Infrastructure,15022.85,15418.12,395.27,2.63 -AcmeSaaS Inc.,Engineering,2023,12,2023-12,Contractors,13452.25,14227.39,775.14,5.76 -AcmeSaaS Inc.,Engineering,2023,12,2023-12,Office & Facilities,13282.38,14709.2,1426.82,10.74 -AcmeSaaS Inc.,Sales,2023,12,2023-12,Salaries,12015.36,11872.63,-142.73,-1.19 -AcmeSaaS Inc.,Sales,2023,12,2023-12,Software & Tools,12119.24,12392.1,272.86,2.25 -AcmeSaaS Inc.,Sales,2023,12,2023-12,Travel,26539.52,25209.95,-1329.57,-5.01 -AcmeSaaS Inc.,Sales,2023,12,2023-12,Marketing Spend,10387.53,9718.1,-669.43,-6.44 -AcmeSaaS Inc.,Sales,2023,12,2023-12,Cloud Infrastructure,10925.21,11467.86,542.65,4.97 -AcmeSaaS Inc.,Sales,2023,12,2023-12,Contractors,7756.59,8134.47,377.88,4.87 -AcmeSaaS Inc.,Sales,2023,12,2023-12,Office & Facilities,5433.8,5373.85,-59.95,-1.1 -AcmeSaaS Inc.,Marketing,2023,12,2023-12,Salaries,8908.88,9004.16,95.28,1.07 -AcmeSaaS Inc.,Marketing,2023,12,2023-12,Software & Tools,11376.15,11779.23,403.08,3.54 -AcmeSaaS Inc.,Marketing,2023,12,2023-12,Travel,9956.63,10932.57,975.94,9.8 -AcmeSaaS Inc.,Marketing,2023,12,2023-12,Marketing Spend,8258.73,8906.14,647.41,7.84 -AcmeSaaS Inc.,Marketing,2023,12,2023-12,Cloud Infrastructure,8635.56,7747.29,-888.27,-10.29 -AcmeSaaS Inc.,Marketing,2023,12,2023-12,Contractors,11477.94,10557.66,-920.28,-8.02 -AcmeSaaS Inc.,Marketing,2023,12,2023-12,Office & Facilities,6173.31,5888.27,-285.04,-4.62 -AcmeSaaS Inc.,Operations,2023,12,2023-12,Salaries,7646.0,7646.87,0.87,0.01 -AcmeSaaS Inc.,Operations,2023,12,2023-12,Software & Tools,6144.98,6135.83,-9.15,-0.15 -AcmeSaaS Inc.,Operations,2023,12,2023-12,Travel,3801.83,3419.01,-382.82,-10.07 -AcmeSaaS Inc.,Operations,2023,12,2023-12,Marketing Spend,2430.19,2161.82,-268.37,-11.04 -AcmeSaaS Inc.,Operations,2023,12,2023-12,Cloud Infrastructure,7142.63,7026.11,-116.52,-1.63 -AcmeSaaS Inc.,Operations,2023,12,2023-12,Contractors,7234.95,6926.43,-308.52,-4.26 -AcmeSaaS Inc.,Operations,2023,12,2023-12,Office & Facilities,9263.66,8708.66,-555.0,-5.99 -AcmeSaaS Inc.,Engineering,2024,1,2024-01,Salaries,4522.32,4272.16,-250.16,-5.53 -AcmeSaaS Inc.,Engineering,2024,1,2024-01,Software & Tools,19782.63,17599.73,-2182.9,-11.03 -AcmeSaaS Inc.,Engineering,2024,1,2024-01,Travel,17574.83,18655.76,1080.93,6.15 -AcmeSaaS Inc.,Engineering,2024,1,2024-01,Marketing Spend,13004.03,12911.96,-92.07,-0.71 -AcmeSaaS Inc.,Engineering,2024,1,2024-01,Cloud Infrastructure,19587.62,20299.87,712.25,3.64 -AcmeSaaS Inc.,Engineering,2024,1,2024-01,Contractors,20442.79,22484.16,2041.37,9.99 -AcmeSaaS Inc.,Engineering,2024,1,2024-01,Office & Facilities,14705.77,13581.62,-1124.15,-7.64 -AcmeSaaS Inc.,Sales,2024,1,2024-01,Salaries,15089.94,16385.5,1295.56,8.59 -AcmeSaaS Inc.,Sales,2024,1,2024-01,Software & Tools,16082.33,15425.41,-656.92,-4.08 -AcmeSaaS Inc.,Sales,2024,1,2024-01,Travel,13211.64,13825.74,614.1,4.65 -AcmeSaaS Inc.,Sales,2024,1,2024-01,Marketing Spend,5175.33,4912.28,-263.05,-5.08 -AcmeSaaS Inc.,Sales,2024,1,2024-01,Cloud Infrastructure,10326.78,11430.16,1103.38,10.68 -AcmeSaaS Inc.,Sales,2024,1,2024-01,Contractors,10032.75,10787.77,755.02,7.53 -AcmeSaaS Inc.,Sales,2024,1,2024-01,Office & Facilities,16791.66,16993.55,201.89,1.2 -AcmeSaaS Inc.,Marketing,2024,1,2024-01,Salaries,7956.75,8257.82,301.07,3.78 -AcmeSaaS Inc.,Marketing,2024,1,2024-01,Software & Tools,6160.43,6223.4,62.97,1.02 -AcmeSaaS Inc.,Marketing,2024,1,2024-01,Travel,6272.54,6141.94,-130.6,-2.08 -AcmeSaaS Inc.,Marketing,2024,1,2024-01,Marketing Spend,14554.71,13463.4,-1091.31,-7.5 -AcmeSaaS Inc.,Marketing,2024,1,2024-01,Cloud Infrastructure,7308.29,7065.85,-242.44,-3.32 -AcmeSaaS Inc.,Marketing,2024,1,2024-01,Contractors,8721.97,9258.78,536.81,6.15 -AcmeSaaS Inc.,Marketing,2024,1,2024-01,Office & Facilities,14784.32,15229.3,444.98,3.01 -AcmeSaaS Inc.,Operations,2024,1,2024-01,Salaries,8383.62,9335.62,952.0,11.36 -AcmeSaaS Inc.,Operations,2024,1,2024-01,Software & Tools,3349.49,3437.01,87.52,2.61 -AcmeSaaS Inc.,Operations,2024,1,2024-01,Travel,6476.74,6071.5,-405.24,-6.26 -AcmeSaaS Inc.,Operations,2024,1,2024-01,Marketing Spend,9900.67,9088.92,-811.75,-8.2 -AcmeSaaS Inc.,Operations,2024,1,2024-01,Cloud Infrastructure,5471.57,5538.33,66.76,1.22 -AcmeSaaS Inc.,Operations,2024,1,2024-01,Contractors,7825.04,7923.17,98.13,1.25 -AcmeSaaS Inc.,Operations,2024,1,2024-01,Office & Facilities,2606.42,2351.96,-254.46,-9.76 -AcmeSaaS Inc.,Engineering,2024,2,2024-02,Salaries,22563.74,22611.78,48.04,0.21 -AcmeSaaS Inc.,Engineering,2024,2,2024-02,Software & Tools,21019.27,19876.28,-1142.99,-5.44 -AcmeSaaS Inc.,Engineering,2024,2,2024-02,Travel,12229.12,13211.53,982.41,8.03 -AcmeSaaS Inc.,Engineering,2024,2,2024-02,Marketing Spend,5531.94,6169.54,637.6,11.53 -AcmeSaaS Inc.,Engineering,2024,2,2024-02,Cloud Infrastructure,19446.39,18250.35,-1196.04,-6.15 -AcmeSaaS Inc.,Engineering,2024,2,2024-02,Contractors,12948.08,13107.39,159.31,1.23 -AcmeSaaS Inc.,Engineering,2024,2,2024-02,Office & Facilities,17196.88,16716.41,-480.47,-2.79 -AcmeSaaS Inc.,Sales,2024,2,2024-02,Salaries,16505.92,18226.16,1720.24,10.42 -AcmeSaaS Inc.,Sales,2024,2,2024-02,Software & Tools,10233.91,10252.91,19.0,0.19 -AcmeSaaS Inc.,Sales,2024,2,2024-02,Travel,15860.84,17081.04,1220.2,7.69 -AcmeSaaS Inc.,Sales,2024,2,2024-02,Marketing Spend,15628.85,14814.3,-814.55,-5.21 -AcmeSaaS Inc.,Sales,2024,2,2024-02,Cloud Infrastructure,6716.09,6391.39,-324.7,-4.83 -AcmeSaaS Inc.,Sales,2024,2,2024-02,Contractors,14506.44,14809.12,302.68,2.09 -AcmeSaaS Inc.,Sales,2024,2,2024-02,Office & Facilities,8819.19,9875.17,1055.98,11.97 -AcmeSaaS Inc.,Marketing,2024,2,2024-02,Salaries,10333.19,9891.2,-441.99,-4.28 -AcmeSaaS Inc.,Marketing,2024,2,2024-02,Software & Tools,4963.62,4592.72,-370.9,-7.47 -AcmeSaaS Inc.,Marketing,2024,2,2024-02,Travel,11103.72,11630.03,526.31,4.74 -AcmeSaaS Inc.,Marketing,2024,2,2024-02,Marketing Spend,8057.86,8196.71,138.85,1.72 -AcmeSaaS Inc.,Marketing,2024,2,2024-02,Cloud Infrastructure,11313.7,10590.25,-723.45,-6.39 -AcmeSaaS Inc.,Marketing,2024,2,2024-02,Contractors,11180.07,11919.42,739.35,6.61 -AcmeSaaS Inc.,Marketing,2024,2,2024-02,Office & Facilities,9793.22,8720.62,-1072.6,-10.95 -AcmeSaaS Inc.,Operations,2024,2,2024-02,Salaries,6439.15,6431.93,-7.22,-0.11 -AcmeSaaS Inc.,Operations,2024,2,2024-02,Software & Tools,6160.23,5475.73,-684.5,-11.11 -AcmeSaaS Inc.,Operations,2024,2,2024-02,Travel,6910.43,6914.23,3.8,0.05 -AcmeSaaS Inc.,Operations,2024,2,2024-02,Marketing Spend,3905.33,3989.85,84.52,2.16 -AcmeSaaS Inc.,Operations,2024,2,2024-02,Cloud Infrastructure,5866.74,6387.28,520.54,8.87 -AcmeSaaS Inc.,Operations,2024,2,2024-02,Contractors,6976.41,7602.93,626.52,8.98 -AcmeSaaS Inc.,Operations,2024,2,2024-02,Office & Facilities,8107.36,7991.21,-116.15,-1.43 -AcmeSaaS Inc.,Engineering,2024,3,2024-03,Salaries,17049.92,18969.89,1919.97,11.26 -AcmeSaaS Inc.,Engineering,2024,3,2024-03,Software & Tools,15350.8,14756.03,-594.77,-3.87 -AcmeSaaS Inc.,Engineering,2024,3,2024-03,Travel,21886.91,22899.16,1012.25,4.62 -AcmeSaaS Inc.,Engineering,2024,3,2024-03,Marketing Spend,14195.07,14705.54,510.47,3.6 -AcmeSaaS Inc.,Engineering,2024,3,2024-03,Cloud Infrastructure,20221.28,21928.43,1707.15,8.44 -AcmeSaaS Inc.,Engineering,2024,3,2024-03,Contractors,7902.63,8570.89,668.26,8.46 -AcmeSaaS Inc.,Engineering,2024,3,2024-03,Office & Facilities,15660.05,17010.61,1350.56,8.62 -AcmeSaaS Inc.,Sales,2024,3,2024-03,Salaries,11376.23,11734.34,358.11,3.15 -AcmeSaaS Inc.,Sales,2024,3,2024-03,Software & Tools,10057.96,11074.05,1016.09,10.1 -AcmeSaaS Inc.,Sales,2024,3,2024-03,Travel,18424.68,20624.26,2199.58,11.94 -AcmeSaaS Inc.,Sales,2024,3,2024-03,Marketing Spend,19271.32,20412.64,1141.32,5.92 -AcmeSaaS Inc.,Sales,2024,3,2024-03,Cloud Infrastructure,21622.43,21279.78,-342.65,-1.58 -AcmeSaaS Inc.,Sales,2024,3,2024-03,Contractors,4215.36,3809.11,-406.25,-9.64 -AcmeSaaS Inc.,Sales,2024,3,2024-03,Office & Facilities,4892.13,5049.16,157.03,3.21 -AcmeSaaS Inc.,Marketing,2024,3,2024-03,Salaries,13498.34,13092.87,-405.47,-3.0 -AcmeSaaS Inc.,Marketing,2024,3,2024-03,Software & Tools,7927.52,7253.18,-674.34,-8.51 -AcmeSaaS Inc.,Marketing,2024,3,2024-03,Travel,11178.86,11262.48,83.62,0.75 -AcmeSaaS Inc.,Marketing,2024,3,2024-03,Marketing Spend,13898.97,14118.89,219.92,1.58 -AcmeSaaS Inc.,Marketing,2024,3,2024-03,Cloud Infrastructure,2762.12,2956.03,193.91,7.02 -AcmeSaaS Inc.,Marketing,2024,3,2024-03,Contractors,12505.55,11515.06,-990.49,-7.92 -AcmeSaaS Inc.,Marketing,2024,3,2024-03,Office & Facilities,5975.2,5371.42,-603.78,-10.1 -AcmeSaaS Inc.,Operations,2024,3,2024-03,Salaries,9937.18,9353.65,-583.53,-5.87 -AcmeSaaS Inc.,Operations,2024,3,2024-03,Software & Tools,7531.88,6645.04,-886.84,-11.77 -AcmeSaaS Inc.,Operations,2024,3,2024-03,Travel,3902.98,4188.33,285.35,7.31 -AcmeSaaS Inc.,Operations,2024,3,2024-03,Marketing Spend,10339.35,11334.93,995.58,9.63 -AcmeSaaS Inc.,Operations,2024,3,2024-03,Cloud Infrastructure,2967.1,3093.58,126.48,4.26 -AcmeSaaS Inc.,Operations,2024,3,2024-03,Contractors,6013.19,5519.59,-493.6,-8.21 -AcmeSaaS Inc.,Operations,2024,3,2024-03,Office & Facilities,4028.91,3972.57,-56.34,-1.4 -AcmeSaaS Inc.,Engineering,2024,4,2024-04,Salaries,13055.4,12694.11,-361.29,-2.77 -AcmeSaaS Inc.,Engineering,2024,4,2024-04,Software & Tools,19223.48,19146.01,-77.47,-0.4 -AcmeSaaS Inc.,Engineering,2024,4,2024-04,Travel,20532.68,19237.67,-1295.01,-6.31 -AcmeSaaS Inc.,Engineering,2024,4,2024-04,Marketing Spend,15062.35,15322.35,260.0,1.73 -AcmeSaaS Inc.,Engineering,2024,4,2024-04,Cloud Infrastructure,10622.2,10812.92,190.72,1.8 -AcmeSaaS Inc.,Engineering,2024,4,2024-04,Contractors,25792.37,28842.22,3049.85,11.82 -AcmeSaaS Inc.,Engineering,2024,4,2024-04,Office & Facilities,9325.38,8867.09,-458.29,-4.91 -AcmeSaaS Inc.,Sales,2024,4,2024-04,Salaries,20439.41,20961.38,521.97,2.55 -AcmeSaaS Inc.,Sales,2024,4,2024-04,Software & Tools,14730.24,14718.67,-11.57,-0.08 -AcmeSaaS Inc.,Sales,2024,4,2024-04,Travel,7877.6,8641.71,764.11,9.7 -AcmeSaaS Inc.,Sales,2024,4,2024-04,Marketing Spend,13082.02,12410.74,-671.28,-5.13 -AcmeSaaS Inc.,Sales,2024,4,2024-04,Cloud Infrastructure,15222.56,16314.42,1091.86,7.17 -AcmeSaaS Inc.,Sales,2024,4,2024-04,Contractors,16273.79,16691.95,418.16,2.57 -AcmeSaaS Inc.,Sales,2024,4,2024-04,Office & Facilities,3851.97,3715.44,-136.53,-3.54 -AcmeSaaS Inc.,Marketing,2024,4,2024-04,Salaries,9285.5,10184.49,898.99,9.68 -AcmeSaaS Inc.,Marketing,2024,4,2024-04,Software & Tools,9103.71,9423.45,319.74,3.51 -AcmeSaaS Inc.,Marketing,2024,4,2024-04,Travel,9761.13,9313.52,-447.61,-4.59 -AcmeSaaS Inc.,Marketing,2024,4,2024-04,Marketing Spend,10260.08,10114.36,-145.72,-1.42 -AcmeSaaS Inc.,Marketing,2024,4,2024-04,Cloud Infrastructure,9546.32,9728.63,182.31,1.91 -AcmeSaaS Inc.,Marketing,2024,4,2024-04,Contractors,11617.26,12265.11,647.85,5.58 -AcmeSaaS Inc.,Marketing,2024,4,2024-04,Office & Facilities,9188.75,8284.87,-903.88,-9.84 -AcmeSaaS Inc.,Operations,2024,4,2024-04,Salaries,4566.14,5019.27,453.13,9.92 -AcmeSaaS Inc.,Operations,2024,4,2024-04,Software & Tools,9039.26,9756.19,716.93,7.93 -AcmeSaaS Inc.,Operations,2024,4,2024-04,Travel,3384.79,3187.36,-197.43,-5.83 -AcmeSaaS Inc.,Operations,2024,4,2024-04,Marketing Spend,2954.85,3185.11,230.26,7.79 -AcmeSaaS Inc.,Operations,2024,4,2024-04,Cloud Infrastructure,6981.8,6951.38,-30.42,-0.44 -AcmeSaaS Inc.,Operations,2024,4,2024-04,Contractors,11254.3,12082.14,827.84,7.36 -AcmeSaaS Inc.,Operations,2024,4,2024-04,Office & Facilities,6897.2,7305.34,408.14,5.92 -AcmeSaaS Inc.,Engineering,2024,5,2024-05,Salaries,11015.5,12284.33,1268.83,11.52 -AcmeSaaS Inc.,Engineering,2024,5,2024-05,Software & Tools,6143.01,6831.92,688.91,11.21 -AcmeSaaS Inc.,Engineering,2024,5,2024-05,Travel,24620.32,26420.09,1799.77,7.31 -AcmeSaaS Inc.,Engineering,2024,5,2024-05,Marketing Spend,6700.72,6484.86,-215.86,-3.22 -AcmeSaaS Inc.,Engineering,2024,5,2024-05,Cloud Infrastructure,24698.94,26422.03,1723.09,6.98 -AcmeSaaS Inc.,Engineering,2024,5,2024-05,Contractors,22380.69,19769.77,-2610.92,-11.67 -AcmeSaaS Inc.,Engineering,2024,5,2024-05,Office & Facilities,19418.04,19588.48,170.44,0.88 -AcmeSaaS Inc.,Sales,2024,5,2024-05,Salaries,10673.14,9991.31,-681.83,-6.39 -AcmeSaaS Inc.,Sales,2024,5,2024-05,Software & Tools,14417.9,12774.34,-1643.56,-11.4 -AcmeSaaS Inc.,Sales,2024,5,2024-05,Travel,14409.53,15738.32,1328.79,9.22 -AcmeSaaS Inc.,Sales,2024,5,2024-05,Marketing Spend,12901.94,13092.09,190.15,1.47 -AcmeSaaS Inc.,Sales,2024,5,2024-05,Cloud Infrastructure,16987.02,18679.97,1692.95,9.97 -AcmeSaaS Inc.,Sales,2024,5,2024-05,Contractors,19011.45,17740.12,-1271.33,-6.69 -AcmeSaaS Inc.,Sales,2024,5,2024-05,Office & Facilities,4723.21,4228.09,-495.12,-10.48 -AcmeSaaS Inc.,Marketing,2024,5,2024-05,Salaries,13824.33,13799.86,-24.47,-0.18 -AcmeSaaS Inc.,Marketing,2024,5,2024-05,Software & Tools,15018.07,13566.21,-1451.86,-9.67 -AcmeSaaS Inc.,Marketing,2024,5,2024-05,Travel,6543.65,7151.83,608.18,9.29 -AcmeSaaS Inc.,Marketing,2024,5,2024-05,Marketing Spend,8024.53,7322.86,-701.67,-8.74 -AcmeSaaS Inc.,Marketing,2024,5,2024-05,Cloud Infrastructure,4276.92,4229.34,-47.58,-1.11 -AcmeSaaS Inc.,Marketing,2024,5,2024-05,Contractors,15532.71,16168.26,635.55,4.09 -AcmeSaaS Inc.,Marketing,2024,5,2024-05,Office & Facilities,6574.0,6957.62,383.62,5.84 -AcmeSaaS Inc.,Operations,2024,5,2024-05,Salaries,11407.8,11156.23,-251.57,-2.21 -AcmeSaaS Inc.,Operations,2024,5,2024-05,Software & Tools,6006.09,6656.94,650.85,10.84 -AcmeSaaS Inc.,Operations,2024,5,2024-05,Travel,9319.24,8274.11,-1045.13,-11.21 -AcmeSaaS Inc.,Operations,2024,5,2024-05,Marketing Spend,3293.13,3190.8,-102.33,-3.11 -AcmeSaaS Inc.,Operations,2024,5,2024-05,Cloud Infrastructure,5962.59,5881.57,-81.02,-1.36 -AcmeSaaS Inc.,Operations,2024,5,2024-05,Contractors,2724.08,3018.64,294.56,10.81 -AcmeSaaS Inc.,Operations,2024,5,2024-05,Office & Facilities,6726.05,7299.84,573.79,8.53 -AcmeSaaS Inc.,Engineering,2024,6,2024-06,Salaries,7002.04,6367.08,-634.96,-9.07 -AcmeSaaS Inc.,Engineering,2024,6,2024-06,Software & Tools,22434.91,24308.85,1873.94,8.35 -AcmeSaaS Inc.,Engineering,2024,6,2024-06,Travel,18717.96,18514.54,-203.42,-1.09 -AcmeSaaS Inc.,Engineering,2024,6,2024-06,Marketing Spend,30125.01,31301.83,1176.82,3.91 -AcmeSaaS Inc.,Engineering,2024,6,2024-06,Cloud Infrastructure,13827.66,14297.93,470.27,3.4 -AcmeSaaS Inc.,Engineering,2024,6,2024-06,Contractors,14866.46,15213.07,346.61,2.33 -AcmeSaaS Inc.,Engineering,2024,6,2024-06,Office & Facilities,9382.9,8305.05,-1077.85,-11.49 -AcmeSaaS Inc.,Sales,2024,6,2024-06,Salaries,23008.66,21440.12,-1568.54,-6.82 -AcmeSaaS Inc.,Sales,2024,6,2024-06,Software & Tools,9899.69,10778.06,878.37,8.87 -AcmeSaaS Inc.,Sales,2024,6,2024-06,Travel,7060.71,6770.19,-290.52,-4.11 -AcmeSaaS Inc.,Sales,2024,6,2024-06,Marketing Spend,17646.19,16153.55,-1492.64,-8.46 -AcmeSaaS Inc.,Sales,2024,6,2024-06,Cloud Infrastructure,5677.63,6223.41,545.78,9.61 -AcmeSaaS Inc.,Sales,2024,6,2024-06,Contractors,22486.52,19803.44,-2683.08,-11.93 -AcmeSaaS Inc.,Sales,2024,6,2024-06,Office & Facilities,9021.02,9796.99,775.97,8.6 -AcmeSaaS Inc.,Marketing,2024,6,2024-06,Salaries,8588.41,9186.13,597.72,6.96 -AcmeSaaS Inc.,Marketing,2024,6,2024-06,Software & Tools,8183.04,7668.36,-514.68,-6.29 -AcmeSaaS Inc.,Marketing,2024,6,2024-06,Travel,11511.38,11024.51,-486.87,-4.23 -AcmeSaaS Inc.,Marketing,2024,6,2024-06,Marketing Spend,9410.66,8674.93,-735.73,-7.82 -AcmeSaaS Inc.,Marketing,2024,6,2024-06,Cloud Infrastructure,22831.95,20379.25,-2452.7,-10.74 -AcmeSaaS Inc.,Marketing,2024,6,2024-06,Contractors,5308.45,5616.41,307.96,5.8 -AcmeSaaS Inc.,Marketing,2024,6,2024-06,Office & Facilities,5007.24,5038.59,31.35,0.63 -AcmeSaaS Inc.,Operations,2024,6,2024-06,Salaries,7977.46,7103.19,-874.27,-10.96 -AcmeSaaS Inc.,Operations,2024,6,2024-06,Software & Tools,5621.65,6003.78,382.13,6.8 -AcmeSaaS Inc.,Operations,2024,6,2024-06,Travel,8260.35,8987.88,727.53,8.81 -AcmeSaaS Inc.,Operations,2024,6,2024-06,Marketing Spend,5945.11,5975.72,30.61,0.51 -AcmeSaaS Inc.,Operations,2024,6,2024-06,Cloud Infrastructure,2410.91,2386.63,-24.28,-1.01 -AcmeSaaS Inc.,Operations,2024,6,2024-06,Contractors,5862.92,6515.85,652.93,11.14 -AcmeSaaS Inc.,Operations,2024,6,2024-06,Office & Facilities,9724.09,8699.15,-1024.94,-10.54 -AcmeSaaS Inc.,Engineering,2024,7,2024-07,Salaries,17731.82,18192.69,460.87,2.6 -AcmeSaaS Inc.,Engineering,2024,7,2024-07,Software & Tools,15607.11,13980.28,-1626.83,-10.42 -AcmeSaaS Inc.,Engineering,2024,7,2024-07,Travel,23419.96,22155.37,-1264.59,-5.4 -AcmeSaaS Inc.,Engineering,2024,7,2024-07,Marketing Spend,18041.8,18618.03,576.23,3.19 -AcmeSaaS Inc.,Engineering,2024,7,2024-07,Cloud Infrastructure,29560.9,29903.97,343.07,1.16 -AcmeSaaS Inc.,Engineering,2024,7,2024-07,Contractors,6595.58,6318.86,-276.72,-4.2 -AcmeSaaS Inc.,Engineering,2024,7,2024-07,Office & Facilities,6796.06,7602.82,806.76,11.87 -AcmeSaaS Inc.,Sales,2024,7,2024-07,Salaries,13295.08,14153.29,858.21,6.46 -AcmeSaaS Inc.,Sales,2024,7,2024-07,Software & Tools,11829.82,12456.82,627.0,5.3 -AcmeSaaS Inc.,Sales,2024,7,2024-07,Travel,14722.74,13715.79,-1006.95,-6.84 -AcmeSaaS Inc.,Sales,2024,7,2024-07,Marketing Spend,5069.3,5010.36,-58.94,-1.16 -AcmeSaaS Inc.,Sales,2024,7,2024-07,Cloud Infrastructure,16560.05,15480.97,-1079.08,-6.52 -AcmeSaaS Inc.,Sales,2024,7,2024-07,Contractors,19439.67,18688.2,-751.47,-3.87 -AcmeSaaS Inc.,Sales,2024,7,2024-07,Office & Facilities,15590.17,15416.18,-173.99,-1.12 -AcmeSaaS Inc.,Marketing,2024,7,2024-07,Salaries,9928.85,8897.36,-1031.49,-10.39 -AcmeSaaS Inc.,Marketing,2024,7,2024-07,Software & Tools,4460.44,4815.6,355.16,7.96 -AcmeSaaS Inc.,Marketing,2024,7,2024-07,Travel,10112.45,9125.22,-987.23,-9.76 -AcmeSaaS Inc.,Marketing,2024,7,2024-07,Marketing Spend,14173.99,12801.6,-1372.39,-9.68 -AcmeSaaS Inc.,Marketing,2024,7,2024-07,Cloud Infrastructure,9218.45,9746.77,528.32,5.73 -AcmeSaaS Inc.,Marketing,2024,7,2024-07,Contractors,5441.18,5848.31,407.13,7.48 -AcmeSaaS Inc.,Marketing,2024,7,2024-07,Office & Facilities,18568.39,18819.6,251.21,1.35 -AcmeSaaS Inc.,Operations,2024,7,2024-07,Salaries,7665.93,8343.15,677.22,8.83 -AcmeSaaS Inc.,Operations,2024,7,2024-07,Software & Tools,7412.7,7805.98,393.28,5.31 -AcmeSaaS Inc.,Operations,2024,7,2024-07,Travel,5051.84,5619.75,567.91,11.24 -AcmeSaaS Inc.,Operations,2024,7,2024-07,Marketing Spend,2940.62,3011.48,70.86,2.41 -AcmeSaaS Inc.,Operations,2024,7,2024-07,Cloud Infrastructure,5295.64,5107.09,-188.55,-3.56 -AcmeSaaS Inc.,Operations,2024,7,2024-07,Contractors,8468.78,8627.15,158.37,1.87 -AcmeSaaS Inc.,Operations,2024,7,2024-07,Office & Facilities,9333.4,8689.93,-643.47,-6.89 -AcmeSaaS Inc.,Engineering,2024,8,2024-08,Salaries,20852.56,22390.08,1537.52,7.37 -AcmeSaaS Inc.,Engineering,2024,8,2024-08,Software & Tools,9899.78,10719.85,820.07,8.28 -AcmeSaaS Inc.,Engineering,2024,8,2024-08,Travel,6961.42,7754.26,792.84,11.39 -AcmeSaaS Inc.,Engineering,2024,8,2024-08,Marketing Spend,25629.77,27588.46,1958.69,7.64 -AcmeSaaS Inc.,Engineering,2024,8,2024-08,Cloud Infrastructure,13529.24,13898.01,368.77,2.73 -AcmeSaaS Inc.,Engineering,2024,8,2024-08,Contractors,23533.69,24339.67,805.98,3.42 -AcmeSaaS Inc.,Engineering,2024,8,2024-08,Office & Facilities,18759.81,16626.84,-2132.97,-11.37 -AcmeSaaS Inc.,Sales,2024,8,2024-08,Salaries,22785.27,20084.43,-2700.84,-11.85 -AcmeSaaS Inc.,Sales,2024,8,2024-08,Software & Tools,20713.68,22552.37,1838.69,8.88 -AcmeSaaS Inc.,Sales,2024,8,2024-08,Travel,9027.06,9170.74,143.68,1.59 -AcmeSaaS Inc.,Sales,2024,8,2024-08,Marketing Spend,7217.31,7045.45,-171.86,-2.38 -AcmeSaaS Inc.,Sales,2024,8,2024-08,Cloud Infrastructure,18077.76,16523.98,-1553.78,-8.59 -AcmeSaaS Inc.,Sales,2024,8,2024-08,Contractors,9890.79,10206.91,316.12,3.2 -AcmeSaaS Inc.,Sales,2024,8,2024-08,Office & Facilities,10532.08,9345.72,-1186.36,-11.26 -AcmeSaaS Inc.,Marketing,2024,8,2024-08,Salaries,14003.56,14230.73,227.17,1.62 -AcmeSaaS Inc.,Marketing,2024,8,2024-08,Software & Tools,5857.45,5273.99,-583.46,-9.96 -AcmeSaaS Inc.,Marketing,2024,8,2024-08,Travel,8997.88,8031.74,-966.14,-10.74 -AcmeSaaS Inc.,Marketing,2024,8,2024-08,Marketing Spend,7786.86,7146.61,-640.25,-8.22 -AcmeSaaS Inc.,Marketing,2024,8,2024-08,Cloud Infrastructure,8234.18,8467.05,232.87,2.83 -AcmeSaaS Inc.,Marketing,2024,8,2024-08,Contractors,13627.44,14196.42,568.98,4.18 -AcmeSaaS Inc.,Marketing,2024,8,2024-08,Office & Facilities,14474.91,13683.2,-791.71,-5.47 -AcmeSaaS Inc.,Operations,2024,8,2024-08,Salaries,8909.54,8446.06,-463.48,-5.2 -AcmeSaaS Inc.,Operations,2024,8,2024-08,Software & Tools,7014.13,7314.59,300.46,4.28 -AcmeSaaS Inc.,Operations,2024,8,2024-08,Travel,6545.13,6524.13,-21.0,-0.32 -AcmeSaaS Inc.,Operations,2024,8,2024-08,Marketing Spend,4729.29,4918.99,189.7,4.01 -AcmeSaaS Inc.,Operations,2024,8,2024-08,Cloud Infrastructure,9909.57,8828.44,-1081.13,-10.91 -AcmeSaaS Inc.,Operations,2024,8,2024-08,Contractors,3015.89,2940.08,-75.81,-2.51 -AcmeSaaS Inc.,Operations,2024,8,2024-08,Office & Facilities,6414.7,6567.61,152.91,2.38 -AcmeSaaS Inc.,Engineering,2024,9,2024-09,Salaries,8704.38,9220.41,516.03,5.93 -AcmeSaaS Inc.,Engineering,2024,9,2024-09,Software & Tools,23368.57,21549.72,-1818.85,-7.78 -AcmeSaaS Inc.,Engineering,2024,9,2024-09,Travel,18866.18,18323.77,-542.41,-2.88 -AcmeSaaS Inc.,Engineering,2024,9,2024-09,Marketing Spend,15171.87,15913.49,741.62,4.89 -AcmeSaaS Inc.,Engineering,2024,9,2024-09,Cloud Infrastructure,21077.08,21078.41,1.33,0.01 -AcmeSaaS Inc.,Engineering,2024,9,2024-09,Contractors,24701.68,26677.94,1976.26,8.0 -AcmeSaaS Inc.,Engineering,2024,9,2024-09,Office & Facilities,8706.51,9346.33,639.82,7.35 -AcmeSaaS Inc.,Sales,2024,9,2024-09,Salaries,5281.88,5374.93,93.05,1.76 -AcmeSaaS Inc.,Sales,2024,9,2024-09,Software & Tools,22752.78,23896.78,1144.0,5.03 -AcmeSaaS Inc.,Sales,2024,9,2024-09,Travel,4623.18,4531.86,-91.32,-1.98 -AcmeSaaS Inc.,Sales,2024,9,2024-09,Marketing Spend,4101.93,3723.08,-378.85,-9.24 -AcmeSaaS Inc.,Sales,2024,9,2024-09,Cloud Infrastructure,24066.89,21299.33,-2767.56,-11.5 -AcmeSaaS Inc.,Sales,2024,9,2024-09,Contractors,22760.42,21803.22,-957.2,-4.21 -AcmeSaaS Inc.,Sales,2024,9,2024-09,Office & Facilities,16425.26,17613.09,1187.83,7.23 -AcmeSaaS Inc.,Marketing,2024,9,2024-09,Salaries,10966.0,11029.07,63.07,0.58 -AcmeSaaS Inc.,Marketing,2024,9,2024-09,Software & Tools,13954.86,13605.77,-349.09,-2.5 -AcmeSaaS Inc.,Marketing,2024,9,2024-09,Travel,15180.92,14489.67,-691.25,-4.55 -AcmeSaaS Inc.,Marketing,2024,9,2024-09,Marketing Spend,3560.31,3423.18,-137.13,-3.85 -AcmeSaaS Inc.,Marketing,2024,9,2024-09,Cloud Infrastructure,14128.95,13562.89,-566.06,-4.01 -AcmeSaaS Inc.,Marketing,2024,9,2024-09,Contractors,5728.75,5272.47,-456.28,-7.96 -AcmeSaaS Inc.,Marketing,2024,9,2024-09,Office & Facilities,10557.23,10583.79,26.56,0.25 -AcmeSaaS Inc.,Operations,2024,9,2024-09,Salaries,2435.13,2281.0,-154.13,-6.33 -AcmeSaaS Inc.,Operations,2024,9,2024-09,Software & Tools,5869.94,5371.86,-498.08,-8.49 -AcmeSaaS Inc.,Operations,2024,9,2024-09,Travel,9305.14,8629.08,-676.06,-7.27 -AcmeSaaS Inc.,Operations,2024,9,2024-09,Marketing Spend,4476.87,4586.89,110.02,2.46 -AcmeSaaS Inc.,Operations,2024,9,2024-09,Cloud Infrastructure,7756.2,8240.59,484.39,6.25 -AcmeSaaS Inc.,Operations,2024,9,2024-09,Contractors,8550.61,8869.74,319.13,3.73 -AcmeSaaS Inc.,Operations,2024,9,2024-09,Office & Facilities,8516.67,7856.76,-659.91,-7.75 -AcmeSaaS Inc.,Engineering,2024,10,2024-10,Salaries,19482.03,20114.64,632.61,3.25 -AcmeSaaS Inc.,Engineering,2024,10,2024-10,Software & Tools,13702.19,14111.68,409.49,2.99 -AcmeSaaS Inc.,Engineering,2024,10,2024-10,Travel,19100.43,20770.18,1669.75,8.74 -AcmeSaaS Inc.,Engineering,2024,10,2024-10,Marketing Spend,19213.06,19799.68,586.62,3.05 -AcmeSaaS Inc.,Engineering,2024,10,2024-10,Cloud Infrastructure,12764.66,11695.36,-1069.3,-8.38 -AcmeSaaS Inc.,Engineering,2024,10,2024-10,Contractors,22619.55,20275.91,-2343.64,-10.36 -AcmeSaaS Inc.,Engineering,2024,10,2024-10,Office & Facilities,15161.5,14951.21,-210.29,-1.39 -AcmeSaaS Inc.,Sales,2024,10,2024-10,Salaries,15286.7,16503.63,1216.93,7.96 -AcmeSaaS Inc.,Sales,2024,10,2024-10,Software & Tools,14370.23,12910.44,-1459.79,-10.16 -AcmeSaaS Inc.,Sales,2024,10,2024-10,Travel,7255.72,7890.02,634.3,8.74 -AcmeSaaS Inc.,Sales,2024,10,2024-10,Marketing Spend,21945.84,23817.17,1871.33,8.53 -AcmeSaaS Inc.,Sales,2024,10,2024-10,Cloud Infrastructure,15533.74,15962.5,428.76,2.76 -AcmeSaaS Inc.,Sales,2024,10,2024-10,Contractors,20141.24,20175.41,34.17,0.17 -AcmeSaaS Inc.,Sales,2024,10,2024-10,Office & Facilities,7279.1,7213.96,-65.14,-0.89 -AcmeSaaS Inc.,Marketing,2024,10,2024-10,Salaries,9608.88,9552.68,-56.2,-0.58 -AcmeSaaS Inc.,Marketing,2024,10,2024-10,Software & Tools,12774.18,11703.79,-1070.39,-8.38 -AcmeSaaS Inc.,Marketing,2024,10,2024-10,Travel,14161.02,12671.98,-1489.04,-10.52 -AcmeSaaS Inc.,Marketing,2024,10,2024-10,Marketing Spend,8215.06,7433.32,-781.74,-9.52 -AcmeSaaS Inc.,Marketing,2024,10,2024-10,Cloud Infrastructure,13014.05,14260.67,1246.62,9.58 -AcmeSaaS Inc.,Marketing,2024,10,2024-10,Contractors,10908.59,10498.7,-409.89,-3.76 -AcmeSaaS Inc.,Marketing,2024,10,2024-10,Office & Facilities,6506.39,6841.05,334.66,5.14 -AcmeSaaS Inc.,Operations,2024,10,2024-10,Salaries,8695.16,8429.82,-265.34,-3.05 -AcmeSaaS Inc.,Operations,2024,10,2024-10,Software & Tools,4394.44,4165.47,-228.97,-5.21 -AcmeSaaS Inc.,Operations,2024,10,2024-10,Travel,5368.41,5250.87,-117.54,-2.19 -AcmeSaaS Inc.,Operations,2024,10,2024-10,Marketing Spend,7829.93,7526.19,-303.74,-3.88 -AcmeSaaS Inc.,Operations,2024,10,2024-10,Cloud Infrastructure,7851.48,8035.93,184.45,2.35 -AcmeSaaS Inc.,Operations,2024,10,2024-10,Contractors,8930.91,9550.84,619.93,6.94 -AcmeSaaS Inc.,Operations,2024,10,2024-10,Office & Facilities,4215.51,4364.54,149.03,3.54 -AcmeSaaS Inc.,Engineering,2024,11,2024-11,Salaries,6277.04,6839.38,562.34,8.96 -AcmeSaaS Inc.,Engineering,2024,11,2024-11,Software & Tools,7048.76,6766.86,-281.9,-4.0 -AcmeSaaS Inc.,Engineering,2024,11,2024-11,Travel,22806.85,23259.74,452.89,1.99 -AcmeSaaS Inc.,Engineering,2024,11,2024-11,Marketing Spend,12166.96,11119.91,-1047.05,-8.61 -AcmeSaaS Inc.,Engineering,2024,11,2024-11,Cloud Infrastructure,24030.92,23164.77,-866.15,-3.6 -AcmeSaaS Inc.,Engineering,2024,11,2024-11,Contractors,22218.08,24712.0,2493.92,11.22 -AcmeSaaS Inc.,Engineering,2024,11,2024-11,Office & Facilities,28959.33,30338.81,1379.48,4.76 -AcmeSaaS Inc.,Sales,2024,11,2024-11,Salaries,10756.3,11195.46,439.16,4.08 -AcmeSaaS Inc.,Sales,2024,11,2024-11,Software & Tools,14666.66,15824.6,1157.94,7.9 -AcmeSaaS Inc.,Sales,2024,11,2024-11,Travel,21270.37,22489.29,1218.92,5.73 -AcmeSaaS Inc.,Sales,2024,11,2024-11,Marketing Spend,9170.15,9578.22,408.07,4.45 -AcmeSaaS Inc.,Sales,2024,11,2024-11,Cloud Infrastructure,10462.11,10528.38,66.27,0.63 -AcmeSaaS Inc.,Sales,2024,11,2024-11,Contractors,18452.59,19099.28,646.69,3.5 -AcmeSaaS Inc.,Sales,2024,11,2024-11,Office & Facilities,18867.01,18520.19,-346.82,-1.84 -AcmeSaaS Inc.,Marketing,2024,11,2024-11,Salaries,10221.06,9331.99,-889.07,-8.7 -AcmeSaaS Inc.,Marketing,2024,11,2024-11,Software & Tools,10235.94,9197.19,-1038.75,-10.15 -AcmeSaaS Inc.,Marketing,2024,11,2024-11,Travel,6709.6,7264.23,554.63,8.27 -AcmeSaaS Inc.,Marketing,2024,11,2024-11,Marketing Spend,7365.8,6660.7,-705.1,-9.57 -AcmeSaaS Inc.,Marketing,2024,11,2024-11,Cloud Infrastructure,21551.18,22952.22,1401.04,6.5 -AcmeSaaS Inc.,Marketing,2024,11,2024-11,Contractors,12627.78,13643.42,1015.64,8.04 -AcmeSaaS Inc.,Marketing,2024,11,2024-11,Office & Facilities,7604.65,8304.91,700.26,9.21 -AcmeSaaS Inc.,Operations,2024,11,2024-11,Salaries,2558.05,2724.49,166.44,6.51 -AcmeSaaS Inc.,Operations,2024,11,2024-11,Software & Tools,6299.96,6767.23,467.27,7.42 -AcmeSaaS Inc.,Operations,2024,11,2024-11,Travel,11675.29,10738.11,-937.18,-8.03 -AcmeSaaS Inc.,Operations,2024,11,2024-11,Marketing Spend,3725.63,3669.9,-55.73,-1.5 -AcmeSaaS Inc.,Operations,2024,11,2024-11,Cloud Infrastructure,6799.97,6654.49,-145.48,-2.14 -AcmeSaaS Inc.,Operations,2024,11,2024-11,Contractors,4116.05,4290.27,174.22,4.23 -AcmeSaaS Inc.,Operations,2024,11,2024-11,Office & Facilities,12489.18,11702.45,-786.73,-6.3 -AcmeSaaS Inc.,Engineering,2024,12,2024-12,Salaries,16089.84,15970.19,-119.65,-0.74 -AcmeSaaS Inc.,Engineering,2024,12,2024-12,Software & Tools,11894.74,12851.4,956.66,8.04 -AcmeSaaS Inc.,Engineering,2024,12,2024-12,Travel,24105.92,23341.32,-764.6,-3.17 -AcmeSaaS Inc.,Engineering,2024,12,2024-12,Marketing Spend,16214.4,17954.39,1739.99,10.73 -AcmeSaaS Inc.,Engineering,2024,12,2024-12,Cloud Infrastructure,18455.45,20601.18,2145.73,11.63 -AcmeSaaS Inc.,Engineering,2024,12,2024-12,Contractors,12541.11,12425.77,-115.34,-0.92 -AcmeSaaS Inc.,Engineering,2024,12,2024-12,Office & Facilities,25688.58,24343.15,-1345.43,-5.24 -AcmeSaaS Inc.,Sales,2024,12,2024-12,Salaries,11463.43,11851.85,388.42,3.39 -AcmeSaaS Inc.,Sales,2024,12,2024-12,Software & Tools,14505.93,15808.39,1302.46,8.98 -AcmeSaaS Inc.,Sales,2024,12,2024-12,Travel,23676.2,23986.12,309.92,1.31 -AcmeSaaS Inc.,Sales,2024,12,2024-12,Marketing Spend,20554.51,18594.05,-1960.46,-9.54 -AcmeSaaS Inc.,Sales,2024,12,2024-12,Cloud Infrastructure,20227.83,21907.03,1679.2,8.3 -AcmeSaaS Inc.,Sales,2024,12,2024-12,Contractors,6375.29,6912.6,537.31,8.43 -AcmeSaaS Inc.,Sales,2024,12,2024-12,Office & Facilities,8707.61,8258.43,-449.18,-5.16 -AcmeSaaS Inc.,Marketing,2024,12,2024-12,Salaries,14815.77,14641.99,-173.78,-1.17 -AcmeSaaS Inc.,Marketing,2024,12,2024-12,Software & Tools,7002.61,6749.82,-252.79,-3.61 -AcmeSaaS Inc.,Marketing,2024,12,2024-12,Travel,17081.5,15141.06,-1940.44,-11.36 -AcmeSaaS Inc.,Marketing,2024,12,2024-12,Marketing Spend,5003.72,4467.23,-536.49,-10.72 -AcmeSaaS Inc.,Marketing,2024,12,2024-12,Cloud Infrastructure,9626.74,9631.38,4.64,0.05 -AcmeSaaS Inc.,Marketing,2024,12,2024-12,Contractors,17736.53,16611.8,-1124.73,-6.34 -AcmeSaaS Inc.,Marketing,2024,12,2024-12,Office & Facilities,6193.87,6929.0,735.13,11.87 -AcmeSaaS Inc.,Operations,2024,12,2024-12,Salaries,5290.09,5019.51,-270.58,-5.11 -AcmeSaaS Inc.,Operations,2024,12,2024-12,Software & Tools,1903.32,2053.95,150.63,7.91 -AcmeSaaS Inc.,Operations,2024,12,2024-12,Travel,10720.19,11224.65,504.46,4.71 -AcmeSaaS Inc.,Operations,2024,12,2024-12,Marketing Spend,9824.97,8973.25,-851.72,-8.67 -AcmeSaaS Inc.,Operations,2024,12,2024-12,Cloud Infrastructure,7976.73,8370.21,393.48,4.93 -AcmeSaaS Inc.,Operations,2024,12,2024-12,Contractors,9358.11,9242.67,-115.44,-1.23 -AcmeSaaS Inc.,Operations,2024,12,2024-12,Office & Facilities,2972.04,2619.14,-352.9,-11.87 diff --git a/testing/data/csv/pl_income_statement.csv b/testing/data/csv/pl_income_statement.csv deleted file mode 100644 index 0076b2c..0000000 --- a/testing/data/csv/pl_income_statement.csv +++ /dev/null @@ -1,25 +0,0 @@ -company,year,month,period,product_revenue,service_revenue,total_revenue,cogs_product,cogs_service,total_cogs,gross_profit,gross_margin_pct,total_opex,ebitda,ebitda_margin_pct,net_income -AcmeSaaS Inc.,2023,1,2023-01,167881.7,71338.86,239220.56,43649.24,32102.49,75751.73,163468.83,68.33,258734.13,-95265.3,-39.82,-71448.98 -AcmeSaaS Inc.,2023,2,2023-02,178629.98,69239.49,247869.47,44657.5,32542.56,77200.06,170669.41,68.85,262477.89,-91808.48,-37.04,-68856.36 -AcmeSaaS Inc.,2023,3,2023-03,174480.8,77504.48,251985.28,41875.39,33326.93,75202.32,176782.96,70.16,263811.87,-87028.91,-34.54,-65271.68 -AcmeSaaS Inc.,2023,4,2023-04,198938.2,71926.16,270864.36,51723.93,30928.25,82652.18,188212.18,69.49,277471.78,-89259.6,-32.95,-66944.7 -AcmeSaaS Inc.,2023,5,2023-05,195203.87,79126.8,274330.67,48800.97,37980.86,86781.83,187548.84,68.37,264394.63,-76845.79,-28.01,-57634.34 -AcmeSaaS Inc.,2023,6,2023-06,211243.76,82868.84,294112.6,54923.38,38119.67,93043.05,201069.55,68.36,271048.89,-69979.34,-23.79,-52484.5 -AcmeSaaS Inc.,2023,7,2023-07,197893.06,86586.64,284479.7,49473.26,39829.85,89303.11,195176.59,68.61,286660.29,-91483.7,-32.16,-68612.77 -AcmeSaaS Inc.,2023,8,2023-08,212514.63,79799.67,292314.3,51003.51,34313.86,85317.37,206996.93,70.81,287657.57,-80660.64,-27.59,-60495.48 -AcmeSaaS Inc.,2023,9,2023-09,220822.47,80318.59,301141.06,57413.84,34536.99,91950.83,209190.23,69.47,294485.72,-85295.49,-28.32,-63971.62 -AcmeSaaS Inc.,2023,10,2023-10,211288.73,83490.66,294779.39,54935.07,38405.7,93340.77,201438.62,68.34,306228.63,-104790.01,-35.55,-78592.51 -AcmeSaaS Inc.,2023,11,2023-11,230323.91,94427.01,324750.92,57580.98,44380.69,101961.67,222789.25,68.6,299769.68,-76980.43,-23.7,-57735.32 -AcmeSaaS Inc.,2023,12,2023-12,237893.99,85187.11,323081.1,57094.56,39186.07,96280.63,226800.47,70.2,300204.79,-73404.32,-22.72,-55053.24 -AcmeSaaS Inc.,2024,1,2024-01,236329.99,81463.84,317793.83,61445.8,35844.09,97289.89,220503.94,69.39,323370.74,-102866.8,-32.37,-77150.1 -AcmeSaaS Inc.,2024,2,2024-02,231260.65,93519.61,324780.26,57815.16,43954.22,101769.38,223010.88,68.67,318477.89,-95467.01,-29.39,-71600.26 -AcmeSaaS Inc.,2024,3,2024-03,265782.9,83786.47,349569.37,63787.9,39379.64,103167.54,246401.83,70.49,311338.28,-64936.45,-18.58,-48702.34 -AcmeSaaS Inc.,2024,4,2024-04,268162.53,102184.41,370346.94,67040.63,47004.83,114045.46,256301.48,69.21,321097.34,-64795.86,-17.5,-48596.9 -AcmeSaaS Inc.,2024,5,2024-05,256505.97,87815.99,344321.96,66691.55,39517.2,106208.75,238113.21,69.15,316380.7,-78267.49,-22.73,-58700.62 -AcmeSaaS Inc.,2024,6,2024-06,271536.83,106191.45,377728.28,65168.84,49909.98,115078.82,262649.46,69.53,325000.96,-62351.5,-16.51,-46763.62 -AcmeSaaS Inc.,2024,7,2024-07,298646.5,96974.17,395620.67,71675.16,44608.12,116283.28,279337.39,70.61,330776.36,-51438.97,-13.0,-38579.23 -AcmeSaaS Inc.,2024,8,2024-08,289698.65,107310.47,397009.12,75321.65,49362.82,124684.47,272324.65,68.59,354189.36,-81864.71,-20.62,-61398.53 -AcmeSaaS Inc.,2024,9,2024-09,277082.24,94973.08,372055.32,66499.74,44637.35,111137.09,260918.23,70.13,332157.93,-71239.7,-19.15,-53429.77 -AcmeSaaS Inc.,2024,10,2024-10,304823.0,104824.94,409647.94,73157.52,46122.97,119280.49,290367.45,70.88,365913.14,-75545.69,-18.44,-56659.27 -AcmeSaaS Inc.,2024,11,2024-11,325914.43,95195.56,421109.99,84737.75,43789.96,128527.71,292582.28,69.48,354860.87,-62278.59,-14.79,-46708.94 -AcmeSaaS Inc.,2024,12,2024-12,331873.11,108581.53,440454.64,79649.55,52119.13,131768.68,308685.96,70.08,352490.46,-43804.5,-9.95,-32853.38 diff --git a/testing/data/csv/revenue_budget_vs_actuals.csv b/testing/data/csv/revenue_budget_vs_actuals.csv deleted file mode 100644 index 8c99619..0000000 --- a/testing/data/csv/revenue_budget_vs_actuals.csv +++ /dev/null @@ -1,49 +0,0 @@ -company,year,month,period,revenue_type,budget_amount,actual_amount,variance,variance_pct -AcmeSaaS Inc.,2023,1,2023-01,Product,180000,185019.36,5019.36,2.79 -AcmeSaaS Inc.,2023,1,2023-01,Service,75000,67875.16,-7124.84,-9.5 -AcmeSaaS Inc.,2023,2,2023-02,Product,184500.0,176198.58,-8301.42,-4.5 -AcmeSaaS Inc.,2023,2,2023-02,Service,76125.0,71910.88,-4214.12,-5.54 -AcmeSaaS Inc.,2023,3,2023-03,Product,189112.5,198056.43,8943.93,4.73 -AcmeSaaS Inc.,2023,3,2023-03,Service,77266.87,79997.47,2730.6,3.53 -AcmeSaaS Inc.,2023,4,2023-04,Product,193840.31,209044.35,15204.04,7.84 -AcmeSaaS Inc.,2023,4,2023-04,Service,78425.88,71946.94,-6478.94,-8.26 -AcmeSaaS Inc.,2023,5,2023-05,Product,198686.32,195583.71,-3102.61,-1.56 -AcmeSaaS Inc.,2023,5,2023-05,Service,79602.27,72116.43,-7485.84,-9.4 -AcmeSaaS Inc.,2023,6,2023-06,Product,203653.48,192193.41,-11460.07,-5.63 -AcmeSaaS Inc.,2023,6,2023-06,Service,80796.3,80882.84,86.54,0.11 -AcmeSaaS Inc.,2023,7,2023-07,Product,208744.82,188978.19,-19766.63,-9.47 -AcmeSaaS Inc.,2023,7,2023-07,Service,82008.24,77068.68,-4939.56,-6.02 -AcmeSaaS Inc.,2023,8,2023-08,Product,213963.44,220377.4,6413.96,3.0 -AcmeSaaS Inc.,2023,8,2023-08,Service,83238.37,83986.54,748.17,0.9 -AcmeSaaS Inc.,2023,9,2023-09,Product,219312.52,207050.35,-12262.17,-5.59 -AcmeSaaS Inc.,2023,9,2023-09,Service,84486.94,85995.3,1508.36,1.79 -AcmeSaaS Inc.,2023,10,2023-10,Product,224795.33,238707.03,13911.7,6.19 -AcmeSaaS Inc.,2023,10,2023-10,Service,85754.25,77290.28,-8463.97,-9.87 -AcmeSaaS Inc.,2023,11,2023-11,Product,230415.22,244508.3,14093.08,6.12 -AcmeSaaS Inc.,2023,11,2023-11,Service,87040.56,90489.79,3449.23,3.96 -AcmeSaaS Inc.,2023,12,2023-12,Product,236175.6,228629.81,-7545.79,-3.19 -AcmeSaaS Inc.,2023,12,2023-12,Service,88346.17,82258.76,-6087.41,-6.89 -AcmeSaaS Inc.,2024,1,2024-01,Product,242079.99,264216.42,22136.43,9.14 -AcmeSaaS Inc.,2024,1,2024-01,Service,89671.36,86740.8,-2930.56,-3.27 -AcmeSaaS Inc.,2024,2,2024-02,Product,248131.99,227921.43,-20210.56,-8.15 -AcmeSaaS Inc.,2024,2,2024-02,Service,91016.43,83675.34,-7341.09,-8.07 -AcmeSaaS Inc.,2024,3,2024-03,Product,254335.29,272011.31,17676.02,6.95 -AcmeSaaS Inc.,2024,3,2024-03,Service,92381.68,94298.16,1916.48,2.07 -AcmeSaaS Inc.,2024,4,2024-04,Product,260693.67,276706.95,16013.28,6.14 -AcmeSaaS Inc.,2024,4,2024-04,Service,93767.4,98075.67,4308.27,4.59 -AcmeSaaS Inc.,2024,5,2024-05,Product,267211.01,269147.12,1936.11,0.72 -AcmeSaaS Inc.,2024,5,2024-05,Service,95173.92,104179.58,9005.66,9.46 -AcmeSaaS Inc.,2024,6,2024-06,Product,273891.29,267237.61,-6653.68,-2.43 -AcmeSaaS Inc.,2024,6,2024-06,Service,96601.52,97606.96,1005.44,1.04 -AcmeSaaS Inc.,2024,7,2024-07,Product,280738.57,299233.89,18495.32,6.59 -AcmeSaaS Inc.,2024,7,2024-07,Service,98050.55,100374.74,2324.19,2.37 -AcmeSaaS Inc.,2024,8,2024-08,Product,287757.03,308573.77,20816.74,7.23 -AcmeSaaS Inc.,2024,8,2024-08,Service,99521.31,101060.95,1539.64,1.55 -AcmeSaaS Inc.,2024,9,2024-09,Product,294950.96,307018.69,12067.73,4.09 -AcmeSaaS Inc.,2024,9,2024-09,Service,101014.13,91838.5,-9175.63,-9.08 -AcmeSaaS Inc.,2024,10,2024-10,Product,302324.73,285872.11,-16452.62,-5.44 -AcmeSaaS Inc.,2024,10,2024-10,Service,102529.34,98210.56,-4318.78,-4.21 -AcmeSaaS Inc.,2024,11,2024-11,Product,309882.85,283839.8,-26043.05,-8.4 -AcmeSaaS Inc.,2024,11,2024-11,Service,104067.28,98505.73,-5561.55,-5.34 -AcmeSaaS Inc.,2024,12,2024-12,Product,317629.92,292283.14,-25346.78,-7.98 -AcmeSaaS Inc.,2024,12,2024-12,Service,105628.29,100937.84,-4690.45,-4.44 diff --git a/testing/generators/generate_data.py b/testing/generators/generate_data.py deleted file mode 100644 index 0bdf7a9..0000000 --- a/testing/generators/generate_data.py +++ /dev/null @@ -1,382 +0,0 @@ -""" -FP&A Test Data Generator -Generates realistic CSV data for: Budget vs Actuals, Cash Flow, P&L, Headcount -Covers 2 years (2023–2024), 4 departments, product & service revenue mix. -""" - -import csv -import random -import os -from datetime import date, timedelta -from dataclasses import dataclass, fields, asdict -from typing import List - -random.seed(42) # reproducible data - -OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "..", "data", "csv") -os.makedirs(OUTPUT_DIR, exist_ok=True) - -# ── Company config ──────────────────────────────────────────────────────────── -COMPANY = "AcmeSaaS Inc." -DEPARTMENTS = ["Engineering", "Sales", "Marketing", "Operations"] -YEARS = [2023, 2024] - -# Revenue split: product (SaaS subscriptions) vs service (consulting/support) -PRODUCT_REVENUE_MIX = 0.70 # 70% product (recurring SaaS) -SERVICE_REVENUE_MIX = 0.30 # 30% services - -# Monthly growth rates (realistic SaaS-style) -PRODUCT_MONTHLY_GROWTH = 0.025 # 2.5% MoM -SERVICE_MONTHLY_GROWTH = 0.015 # 1.5% MoM - -# Base monthly revenue ($) -BASE_PRODUCT_REVENUE = 180_000 -BASE_SERVICE_REVENUE = 75_000 - -# Variance helpers — actuals deviate from budget by ±% -def vary(value: float, pct: float = 0.08) -> float: - """Apply random variance to simulate actuals vs budget.""" - return round(value * (1 + random.uniform(-pct, pct)), 2) - -def months_range(years: List[int]): - for year in years: - for month in range(1, 13): - yield year, month - -# ── 1. Revenue (Budget vs Actuals) ─────────────────────────────────────────── -@dataclass -class RevenueRow: - company: str - year: int - month: int - period: str # e.g. "2023-01" - revenue_type: str # "Product" | "Service" - budget_amount: float - actual_amount: float - variance: float # actual - budget - variance_pct: float # variance / budget * 100 - -def generate_revenue(): - rows = [] - prod_base = BASE_PRODUCT_REVENUE - svc_base = BASE_SERVICE_REVENUE - - for year, month in months_range(YEARS): - period = f"{year}-{month:02d}" - - for rev_type, base in [("Product", prod_base), ("Service", svc_base)]: - budget = round(base, 2) - actual = vary(budget, pct=0.10) - variance = round(actual - budget, 2) - vpct = round((variance / budget) * 100, 2) if budget else 0 - rows.append(RevenueRow(COMPANY, year, month, period, rev_type, - budget, actual, variance, vpct)) - - # grow base each month - prod_base *= (1 + PRODUCT_MONTHLY_GROWTH) - svc_base *= (1 + SERVICE_MONTHLY_GROWTH) - - write_csv("revenue_budget_vs_actuals.csv", rows) - print(f" ✓ revenue_budget_vs_actuals.csv ({len(rows)} rows)") - -# ── 2. Department Opex (Budget vs Actuals) ─────────────────────────────────── -DEPT_BUDGETS = { - # (base_monthly_opex, growth_rate) - "Engineering": (95_000, 0.012), - "Sales": (70_000, 0.018), - "Marketing": (55_000, 0.015), - "Operations": (40_000, 0.008), -} - -OPEX_CATEGORIES = ["Salaries", "Software & Tools", "Travel", "Marketing Spend", - "Cloud Infrastructure", "Contractors", "Office & Facilities"] - -@dataclass -class OpexRow: - company: str - department: str - year: int - month: int - period: str - category: str - budget_amount: float - actual_amount: float - variance: float - variance_pct: float - -def generate_opex(): - rows = [] - dept_bases = {d: v[0] for d, v in DEPT_BUDGETS.items()} - - for year, month in months_range(YEARS): - period = f"{year}-{month:02d}" - for dept, (_, growth) in DEPT_BUDGETS.items(): - total_budget = dept_bases[dept] - # Split across categories with random weights - weights = [random.uniform(0.05, 0.35) for _ in OPEX_CATEGORIES] - total_w = sum(weights) - weights = [w / total_w for w in weights] - - for cat, w in zip(OPEX_CATEGORIES, weights): - budget = round(total_budget * w, 2) - actual = vary(budget, pct=0.12) - variance = round(actual - budget, 2) - vpct = round((variance / budget) * 100, 2) if budget else 0 - rows.append(OpexRow(COMPANY, dept, year, month, period, cat, - budget, actual, variance, vpct)) - - dept_bases[dept] *= (1 + growth) - - write_csv("opex_budget_vs_actuals.csv", rows) - print(f" ✓ opex_budget_vs_actuals.csv ({len(rows)} rows)") - -# ── 3. P&L / Income Statement ───────────────────────────────────────────────── -@dataclass -class PLRow: - company: str - year: int - month: int - period: str - product_revenue: float - service_revenue: float - total_revenue: float - cogs_product: float # ~25% of product rev - cogs_service: float # ~45% of service rev (labor-heavy) - total_cogs: float - gross_profit: float - gross_margin_pct: float - total_opex: float - ebitda: float - ebitda_margin_pct: float - net_income: float # after ~25% tax estimate - -def generate_pl(): - rows = [] - prod_base = BASE_PRODUCT_REVENUE - svc_base = BASE_SERVICE_REVENUE - dept_bases = {d: v[0] for d, v in DEPT_BUDGETS.items()} - - for year, month in months_range(YEARS): - period = f"{year}-{month:02d}" - - prod_rev = vary(prod_base, 0.08) - svc_rev = vary(svc_base, 0.10) - total_rev = round(prod_rev + svc_rev, 2) - - cogs_prod = round(prod_rev * vary(0.25, 0.05), 2) - cogs_svc = round(svc_rev * vary(0.45, 0.06), 2) - total_cogs = round(cogs_prod + cogs_svc, 2) - - gross_profit = round(total_rev - total_cogs, 2) - gm_pct = round((gross_profit / total_rev) * 100, 2) if total_rev else 0 - - total_opex = round(sum( - vary(dept_bases[d], 0.10) for d in DEPARTMENTS - ), 2) - - ebitda = round(gross_profit - total_opex, 2) - ebitda_pct = round((ebitda / total_rev) * 100, 2) if total_rev else 0 - net_income = round(ebitda * 0.75, 2) # rough 25% tax - - rows.append(PLRow( - COMPANY, year, month, period, - round(prod_rev, 2), round(svc_rev, 2), total_rev, - cogs_prod, cogs_svc, total_cogs, - gross_profit, gm_pct, - total_opex, ebitda, ebitda_pct, net_income - )) - - prod_base *= (1 + PRODUCT_MONTHLY_GROWTH) - svc_base *= (1 + SERVICE_MONTHLY_GROWTH) - for d, (_, g) in DEPT_BUDGETS.items(): - dept_bases[d] *= (1 + g) - - write_csv("pl_income_statement.csv", rows) - print(f" ✓ pl_income_statement.csv ({len(rows)} rows)") - -# ── 4. Cash Flow ────────────────────────────────────────────────────────────── -@dataclass -class CashFlowRow: - company: str - year: int - month: int - period: str - # Operating - cash_collected_product: float # product ARR collections (may lag revenue) - cash_collected_service: float - cash_paid_opex: float - cash_paid_cogs: float - net_operating_cash_flow: float - # Investing - capex: float # infra / hardware - net_investing_cash_flow: float - # Financing - loan_repayment: float - equity_raised: float - net_financing_cash_flow: float - # Summary - net_change_in_cash: float - closing_cash_balance: float - -def generate_cashflow(): - rows = [] - cash_balance = 1_200_000.0 # starting cash (seed round runway) - prod_base = BASE_PRODUCT_REVENUE - svc_base = BASE_SERVICE_REVENUE - dept_bases = {d: v[0] for d, v in DEPT_BUDGETS.items()} - - for year, month in months_range(YEARS): - period = f"{year}-{month:02d}" - - # Collections slightly lag invoicing (DSO ~30 days effect) - cash_prod = vary(prod_base * 0.95, 0.06) - cash_svc = vary(svc_base * 0.90, 0.08) # services collect slower - - opex_paid = sum(vary(dept_bases[d], 0.08) for d in DEPARTMENTS) - cogs_paid = vary((prod_base * 0.25) + (svc_base * 0.45), 0.07) - - net_op = round(cash_prod + cash_svc - opex_paid - cogs_paid, 2) - - # Investing — occasional capex spikes - capex = vary(8_000, 0.40) if random.random() > 0.4 else 0.0 - net_inv = round(-capex, 2) - - # Financing — occasional loan repayment - loan = vary(5_000, 0.20) if month % 3 == 0 else 0.0 - equity = 0.0 - if year == 2023 and month == 6: - equity = 500_000.0 # Series A mid-2023 - net_fin = round(equity - loan, 2) - - net_change = round(net_op + net_inv + net_fin, 2) - cash_balance = round(cash_balance + net_change, 2) - - rows.append(CashFlowRow( - COMPANY, year, month, period, - round(cash_prod, 2), round(cash_svc, 2), - round(opex_paid, 2), round(cogs_paid, 2), net_op, - round(capex, 2), net_inv, - round(loan, 2), round(equity, 2), net_fin, - net_change, cash_balance - )) - - prod_base *= (1 + PRODUCT_MONTHLY_GROWTH) - svc_base *= (1 + SERVICE_MONTHLY_GROWTH) - for d, (_, g) in DEPT_BUDGETS.items(): - dept_bases[d] *= (1 + g) - - write_csv("cash_flow.csv", rows) - print(f" ✓ cash_flow.csv ({len(rows)} rows)") - -# ── 5. Headcount & Workforce ────────────────────────────────────────────────── -ROLES = { - "Engineering": [("Software Engineer", 120_000), ("Senior Engineer", 160_000), - ("Engineering Manager", 180_000), ("DevOps Engineer", 130_000)], - "Sales": [("Account Executive", 90_000), ("Sales Manager", 140_000), - ("SDR", 65_000)], - "Marketing": [("Marketing Manager", 110_000), ("Content Strategist", 80_000), - ("Growth Analyst", 95_000)], - "Operations": [("Operations Manager", 115_000), ("Customer Success", 75_000), - ("Finance Analyst", 95_000)], -} - -@dataclass -class HeadcountRow: - company: str - employee_id: str - department: str - role: str - hire_date: str - termination_date: str # empty if active - status: str # Active | Terminated - annual_salary_budget: float - actual_salary_paid_ytd: float # YTD for the given year - year: int - month: int - period: str - headcount_fte: float # 1.0 full time, 0.5 contractor etc. - -def generate_headcount(): - rows = [] - emp_id = 1000 - employees = [] - - # Seed initial employees at start of 2023 - for dept, role_list in ROLES.items(): - # Start with 2-4 per department - count = random.randint(2, 4) - for _ in range(count): - role, salary = random.choice(role_list) - hire_date = date(2022, random.randint(1, 12), random.randint(1, 28)) - employees.append({ - "id": f"EMP{emp_id}", - "dept": dept, "role": role, "salary": salary, - "hire_date": hire_date, "term_date": None, - "fte": 1.0, - }) - emp_id += 1 - - for year, month in months_range(YEARS): - period = f"{year}-{month:02d}" - current = date(year, month, 1) - - # Random hiring each month - if random.random() > 0.55: - dept = random.choice(DEPARTMENTS) - role, salary = random.choice(ROLES[dept]) - employees.append({ - "id": f"EMP{emp_id}", - "dept": dept, "role": role, "salary": salary, - "hire_date": current, "term_date": None, - "fte": random.choice([1.0, 1.0, 1.0, 0.5]), # mostly FT - }) - emp_id += 1 - - # Occasional attrition - active = [e for e in employees if e["term_date"] is None] - if len(active) > 6 and random.random() > 0.85: - leaver = random.choice(active) - leaver["term_date"] = current - - # Snapshot each employee for this month - for emp in employees: - if emp["hire_date"] > current: - continue # not hired yet - status = "Active" if emp["term_date"] is None or emp["term_date"] > current else "Terminated" - months_in_year = month if emp["hire_date"].year < year else ( - month - emp["hire_date"].month + 1 - ) - months_in_year = max(0, min(months_in_year, month)) - ytd_paid = round((emp["salary"] / 12) * months_in_year * vary(1.0, 0.02), 2) - rows.append(HeadcountRow( - COMPANY, emp["id"], emp["dept"], emp["role"], - str(emp["hire_date"]), - str(emp["term_date"]) if emp["term_date"] else "", - status, emp["salary"], ytd_paid, - year, month, period, emp["fte"] - )) - - write_csv("headcount_workforce.csv", rows) - print(f" ✓ headcount_workforce.csv ({len(rows)} rows)") - -# ── CSV writer ──────────────────────────────────────────────────────────────── -def write_csv(filename: str, rows: list): - if not rows: - return - path = os.path.join(OUTPUT_DIR, filename) - with open(path, "w", newline="") as f: - writer = csv.DictWriter(f, fieldnames=[field.name for field in fields(rows[0])]) - writer.writeheader() - writer.writerows([asdict(r) for r in rows]) - -# ── Entry point ─────────────────────────────────────────────────────────────── -if __name__ == "__main__": - print(f"\n🏗 Generating FP&A test data for {COMPANY}") - print(f" Periods : {YEARS[0]}-01 → {YEARS[-1]}-12 (24 months)") - print(f" Depts : {', '.join(DEPARTMENTS)}\n") - generate_revenue() - generate_opex() - generate_pl() - generate_cashflow() - generate_headcount() - print(f"\n✅ All CSV files written to: {os.path.abspath(OUTPUT_DIR)}\n") diff --git a/testing/requirements.txt b/testing/requirements.txt deleted file mode 100644 index 0e3d8c0..0000000 --- a/testing/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -# FP&A Test Platform dependencies - -# HTTP client — used by loaders/api_loader.py to POST to the Go API -requests>=2.31.0,<3.0.0 - -# Needed by requests on some systems (usually installed transitively) -certifi>=2023.7.22 -urllib3>=1.26.0,<3.0.0 - -# Note: tests/test_fpa.py uses stdlib only (csv, urllib, json) — no extra deps needed \ No newline at end of file diff --git a/testing/tests/test_fpa.py b/testing/tests/test_fpa.py deleted file mode 100644 index 34809d0..0000000 --- a/testing/tests/test_fpa.py +++ /dev/null @@ -1,477 +0,0 @@ -""" -FP&A Test Suite -Run: python tests/test_fpa.py - python tests/test_fpa.py --url http://localhost:9000 -""" - -import csv, json, os, sys, traceback -from dataclasses import dataclass -from typing import Callable, List, Optional -import requests - -DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data", "csv") -API_BASE = os.getenv("FPA_API_URL", "http://localhost:8080") - -# ── Runner ──────────────────────────────────────────────────────────────────── - -@dataclass -class Result: - name: str - passed: bool - skipped: bool = False - message: str = "" - -class SkipTest(Exception): - pass - -def skip(reason: str): - raise SkipTest(reason) - -class Suite: - def __init__(self, name: str): - self.name = name - self.results: List[Result] = [] - - def run(self, label: str, fn: Callable): - try: - fn() - self.results.append(Result(label, passed=True)) - except SkipTest as e: - self.results.append(Result(label, passed=False, skipped=True, message=str(e))) - except AssertionError as e: - self.results.append(Result(label, passed=False, message=str(e))) - except Exception as e: - self.results.append(Result(label, passed=False, - message=f"{type(e).__name__}: {e}\n{traceback.format_exc()}")) - -# ── Assertions ──────────────────────────────────────────────────────────────── - -def ok(condition, msg=""): - if not condition: - raise AssertionError(msg or "Expected True") - -def eq(actual, expected, msg=""): - if actual != expected: - raise AssertionError(msg or f"Expected {expected!r}, got {actual!r}") - -def near(actual, expected, tol=1.0, msg=""): - if abs(actual - expected) > tol: - raise AssertionError(msg or f"Expected ~{expected:.2f}, got {actual:.2f} (tol={tol})") - -# ── HTTP ────────────────────────────────────────────────────────────────────── - -session = requests.Session() -session.headers.update({"Content-Type": "application/json"}) - -def get(path: str, **params): - r = session.get(f"{API_BASE}{path}", params=params, timeout=5) - return r.status_code, r.json() if r.content else {} - -def post(path: str, body: dict): - r = session.post(f"{API_BASE}{path}", json=body, timeout=10) - return r.status_code, r.json() if r.content else {} - -def put(path: str, body: dict): - r = session.put(f"{API_BASE}{path}", json=body, timeout=10) - return r.status_code, r.json() if r.content else {} - -def api_up() -> bool: - try: - status, _ = get("/api/v1/health") - return status == 200 - except Exception: - return False - -# ── CSV ─────────────────────────────────────────────────────────────────────── - -def read_csv(filename: str) -> List[dict]: - path = os.path.join(DATA_DIR, filename) - if not os.path.exists(path): - skip(f"{filename} not found — run generators/generate_data.py first") - with open(path, newline="") as f: - return list(csv.DictReader(f)) - -# ── Reference data ──────────────────────────────────────────────────────────── -# Seeded once via the API; IDs stored at runtime. - -DEPT_IDS: dict[str, int] = {} # name → id -GL_IDS: dict[str, int] = {} # code → id - -DEPARTMENTS = [ - {"code": "REV", "name": "Revenue", "cost_center": "CC-100"}, - {"code": "ENG", "name": "Engineering", "cost_center": "CC-200"}, - {"code": "SAL", "name": "Sales", "cost_center": "CC-300"}, - {"code": "MKT", "name": "Marketing", "cost_center": "CC-400"}, - {"code": "OPS", "name": "Operations", "cost_center": "CC-500"}, - {"code": "FIN", "name": "Finance", "cost_center": "CC-600"}, -] - -GL_ACCOUNTS = [ - {"code": "4000", "description": "SaaS Subscription Revenue", "type": "revenue", "favour_high": True}, - {"code": "4100", "description": "Professional Services Revenue", "type": "revenue", "favour_high": True}, - {"code": "5000", "description": "Cost of Goods — Product", "type": "cogs", "favour_high": False}, - {"code": "5100", "description": "Cost of Goods — Service", "type": "cogs", "favour_high": False}, - {"code": "6000", "description": "Salaries and Wages", "type": "opex", "favour_high": False}, - {"code": "6100", "description": "Software and SaaS Tools", "type": "opex", "favour_high": False}, - {"code": "6200", "description": "Travel and Expenses", "type": "opex", "favour_high": False}, - {"code": "6300", "description": "Marketing and Paid Media", "type": "opex", "favour_high": False}, - {"code": "6400", "description": "Cloud Infrastructure", "type": "opex", "favour_high": False}, - {"code": "6500", "description": "Contractors and Freelancers", "type": "opex", "favour_high": False}, - {"code": "6600", "description": "Office and Facilities", "type": "opex", "favour_high": False}, - {"code": "7000", "description": "Capital Expenditure", "type": "capex", "favour_high": False}, - {"code": "9200", "description": "Headcount Cost", "type": "headcount", "favour_high": False}, -] - -# CSV category name → GL code -CSV_TO_GL = { - "Product": "4000", - "Service": "4100", - "Salaries": "6000", - "Software & Tools": "6100", - "Travel": "6200", - "Marketing Spend": "6300", - "Cloud Infrastructure": "6400", - "Contractors": "6500", - "Office & Facilities": "6600", - "Headcount": "9200", -} - -def gl_id(category: str) -> int: - code = CSV_TO_GL.get(category) - assert code, f"No GL mapping for '{category}'" - assert code in GL_IDS, f"GL '{code}' not seeded — call seed() first" - return GL_IDS[code] - -def gl_code(category: str) -> str: - code = CSV_TO_GL.get(category) - assert code, f"No GL mapping for '{category}'" - return code - -def fiscal(period: str): - y, m = period.split("-") - return int(y), int(m) - -def seed(): - """POST departments and GL accounts; cache returned IDs as int. Idempotent.""" - for d in DEPARTMENTS: - status, body = post("/api/v1/departments", d) - ok(status in (200, 201), f"POST /departments '{d['name']}' → {status}: {body}") - ok("id" in body, f"departments response missing 'id': {body}") - DEPT_IDS[d["name"]] = int(body["id"]) # explicit int — guards against "id": "3" - - for g in GL_ACCOUNTS: - status, body = post("/api/v1/gl-accounts", g) - ok(status in (200, 201), f"POST /gl-accounts '{g['code']}' → {status}: {body}") - ok("id" in body, f"gl-accounts response missing 'id': {body}") - GL_IDS[g["code"]] = int(body["id"]) - -# ── suite_revenue ───────────────────────────────────────────────────────────── - -def suite_revenue() -> Suite: - s = Suite("Revenue — CSV + API round-trip") - - if not api_up(): - s.run("API reachable", lambda: skip(f"Go API not running at {API_BASE}")) - return s - - rows = [] - - # 1. CSV validation - s.run("CSV loads", lambda: rows.extend(read_csv("revenue_budget_vs_actuals.csv"))) - s.run("48 rows (24 months × 2 types)", lambda: eq(len(rows), 48)) - s.run("only Product and Service types", lambda: - eq({r["revenue_type"] for r in rows}, {"Product", "Service"})) - s.run("variance = actual − budget", lambda: [ - near(float(r["actual_amount"]) - float(r["budget_amount"]), - float(r["variance"]), tol=0.01, msg=f"variance wrong in {r['period']}") - for r in rows - ]) - - # 2. Seed FK parents - s.run("seed departments + GL accounts", seed) - - # 3. POST budgets - budget_ids: dict[str, int] = {} - - def post_budget(rev_type: str): - row = next(r for r in rows if r["revenue_type"] == rev_type and r["period"] == "2023-01") - fy, fp = fiscal(row["period"]) # fiscal() already returns (int, int) - status, body = post("/api/v1/budgets", { - "fiscal_year": int(fy), - "fiscal_period": int(fp), - "version": "original", - "department_id": int(DEPT_IDS["Revenue"]), - "gl_account_id": int(gl_id(rev_type)), - "amount": float(row["budget_amount"]), - "currency": "DKK", - "notes": f"{rev_type} budget — csv import", - "created_by": "test_suite", - }) - # 201 = created, 200 = upserted (row already existed — idempotent re-run) - ok(status in (200, 201), f"POST /budgets {rev_type} → {status}: {body}") - ok(body.get("id"), f"response missing 'id': {body}") - budget_ids[rev_type] = int(body["id"]) - - s.run("POST budget — Product 2023-01", lambda: post_budget("Product")) - s.run("POST budget — Service 2023-01", lambda: post_budget("Service")) - - # 4. POST actuals - def post_actuals(): - for r in [r for r in rows if r["period"] == "2023-01"]: - fy, fp = fiscal(r["period"]) - status, body = post("/api/v1/actuals/ingest", { - "fiscal_year": int(fy), - "fiscal_period": int(fp), - "dept_code": "REV", - "gl_code": gl_code(r["revenue_type"]), - "amount": float(r["actual_amount"]), - "currency": "DKK", - "source": "test_suite", - }) - ok(status in (200, 201), - f"POST /actuals/ingest {r['revenue_type']} → {status}: {body}") - - s.run("POST actuals — Product + Service 2023-01", post_actuals) - - # 5. GET variance — verify VarianceReport shape and values - report: dict = {} - - def fetch_report(): - status, body = get("/api/v1/variance", - fiscal_year=2023, fiscal_period=1, dept_code="REV", version="original") - ok(status == 200, f"GET /variance → {status}: {body}") - ok(isinstance(body, dict), "expected VarianceReport object") - ok("lines" in body, f"missing 'lines', got keys: {list(body.keys())}") - ok(len(body["lines"]) > 0, "lines is empty") - report.update(body) - - s.run("GET /variance returns VarianceReport", fetch_report) - - def verify_report(): - eq(report["fiscal_year"], 2023, "fiscal_year") - eq(report["fiscal_period"], 1, "fiscal_period") - eq(report["version"], "original", "version") - for field in ("department", "currency", "total_budget", "total_actual"): - ok(field in report, f"VarianceReport missing '{field}'") - - for line in report["lines"]: - for f in ("gl_account_id", "gl_description", "gl_type", - "budget", "actual", "variance_abs", "status"): - ok(f in line, f"VarianceLine missing '{f}'") - ok(line["status"] in ("favourable", "unfavourable", "on_budget"), - f"bad status: {line['status']}") - - s.run("VarianceReport shape valid", verify_report) - - def verify_product_line(): - csv_row = next(r for r in rows - if r["revenue_type"] == "Product" and r["period"] == "2023-01") - line = next((l for l in report["lines"] - if l["gl_account_id"] == "4000"), None) - ok(line, f"no line for gl_account_id=4000, got: {[l['gl_account_id'] for l in report['lines']]}") - near(float(line["budget"]), float(csv_row["budget_amount"]), msg="budget") - near(float(line["actual"]), float(csv_row["actual_amount"]), msg="actual") - near(float(line["variance_abs"]), abs(float(csv_row["variance"])), msg="variance_abs") - if float(csv_row["actual_amount"]) >= float(csv_row["budget_amount"]): - eq(line["status"], "favourable", "revenue over-budget should be favourable") - - s.run("Product line values match CSV", verify_product_line) - - s.run("totals = sum of lines", lambda: ( - near(float(report["total_budget"]), - sum(float(l["budget"]) for l in report["lines"]), msg="total_budget"), - near(float(report["total_actual"]), - sum(float(l["actual"]) for l in report["lines"]), msg="total_actual"), - )) - - # 6. Alerts shape check - def check_alerts(): - status, body = get("/api/v1/variance/alerts", fiscal_year=2023, fiscal_period=1) - ok(status == 200, f"GET /variance/alerts → {status}: {body}") - alerts = body if isinstance(body, list) else body.get("alerts", []) - for a in alerts: - for f in ("gl_code", "description", "budget", "actual", "variance_pct", "status", "department"): - ok(f in a, f"AlertThreshold missing '{f}'") - ok(a["status"] in ("favourable", "unfavourable", "on_budget"), - f"bad alert status: {a['status']}") - - s.run("GET /variance/alerts shape valid", check_alerts) - - # 7. Update to forecast_1 and verify variance shifts - def update_forecast(): - bid = budget_ids.get("Product") - if not bid: - skip("no Product budget id — skipping") - - csv_row = next(r for r in rows - if r["revenue_type"] == "Product" and r["period"] == "2023-01") - revised = float(csv_row["budget_amount"]) * 1.10 - - fy, fp = fiscal("2023-01") - status, body = put(f"/api/v1/budgets/{bid}", { - "fiscal_year": int(fy), - "fiscal_period": int(fp), - "version": "forecast_1", - "department_id": int(DEPT_IDS["Revenue"]), - "gl_account_id": int(gl_id("Product")), - "amount": float(revised), - "currency": "DKK", - "notes": "Q1 reforecast +10%", - "created_by": "test_suite", - }) - ok(status in (200, 204), f"PUT /budgets/{bid} → {status}: {body}") - - status2, body2 = get("/api/v1/variance", - fiscal_year=2023, fiscal_period=1, dept_code="REV", version="forecast_1") - ok(status2 == 200, f"GET forecast_1 variance → {status2}") - lines = body2.get("lines", []) if isinstance(body2, dict) else [] - line = next((l for l in lines if l.get("gl_account_id") == "4000"), None) - ok(line, "Product line missing in forecast_1 variance") - near(float(line["budget"]), revised, msg="forecast_1 budget not updated") - - s.run("PUT budget forecast_1 — variance reflects revision", update_forecast) - - return s - -# ── CSV-only suites ─────────────────────────────────────────────────────────── - -def suite_opex() -> Suite: - s = Suite("Opex CSV") - rows = [] - s.run("loads", lambda: rows.extend(read_csv("opex_budget_vs_actuals.csv"))) - s.run("has rows", lambda: ok(len(rows) > 0)) - s.run("all departments present", lambda: - ok({"Engineering","Sales","Marketing","Operations"}.issubset({r["department"] for r in rows}))) - s.run("no zero budgets", lambda: [ - ok(float(r["budget_amount"]) > 0, f"zero budget: {r['department']}/{r['category']}") - for r in rows - ]) - s.run("variance sign correct", lambda: [ - eq(1 if float(r["variance"]) > 0 else (-1 if float(r["variance"]) < 0 else 0), - 1 if float(r["actual_amount"]) > float(r["budget_amount"]) else - (-1 if float(r["actual_amount"]) < float(r["budget_amount"]) else 0), - f"sign wrong {r['period']} {r['category']}") - for r in rows - ]) - return s - - -def suite_pl() -> Suite: - s = Suite("P&L CSV") - rows = [] - s.run("loads", lambda: rows.extend(read_csv("pl_income_statement.csv"))) - s.run("24 rows", lambda: eq(len(rows), 24)) - s.run("total_revenue = product + service", lambda: [ - near(float(r["product_revenue"]) + float(r["service_revenue"]), - float(r["total_revenue"]), tol=0.05, msg=r["period"]) - for r in rows - ]) - s.run("gross_profit = revenue − cogs", lambda: [ - near(float(r["total_revenue"]) - float(r["total_cogs"]), - float(r["gross_profit"]), tol=0.05, msg=r["period"]) - for r in rows - ]) - s.run("gross margin 30–90%", lambda: [ - ok(30 <= float(r["gross_margin_pct"]) <= 90, f"{r['gross_margin_pct']}% in {r['period']}") - for r in rows - ]) - s.run("ebitda = gross_profit − opex", lambda: [ - near(float(r["gross_profit"]) - float(r["total_opex"]), - float(r["ebitda"]), tol=0.05, msg=r["period"]) - for r in rows - ]) - return s - - -def suite_cashflow() -> Suite: - s = Suite("Cash Flow CSV") - rows = [] - s.run("loads", lambda: rows.extend(read_csv("cash_flow.csv"))) - s.run("24 rows", lambda: eq(len(rows), 24)) - s.run("net_change = op + inv + fin", lambda: [ - near(float(r["net_operating_cash_flow"]) + - float(r["net_investing_cash_flow"]) + - float(r["net_financing_cash_flow"]), - float(r["net_change_in_cash"]), tol=0.05, msg=r["period"]) - for r in rows - ]) - s.run("positive cash pre Series A", lambda: [ - ok(float(r["closing_cash_balance"]) > 0, f"negative cash in {r['period']}") - for r in rows if r["period"] <= "2023-05" - ]) - s.run("Series A in 2023-06", lambda: ( - lambda june=next((r for r in rows if r["period"] == "2023-06"), None): ( - ok(june is not None, "2023-06 missing"), - ok(float(june["equity_raised"]) > 0, "Series A not recorded"), - ) - )()) - return s - - -def suite_headcount() -> Suite: - s = Suite("Headcount CSV") - rows = [] - s.run("loads", lambda: rows.extend(read_csv("headcount_workforce.csv"))) - s.run("has rows", lambda: ok(len(rows) > 0)) - s.run("valid statuses", lambda: - ok({r["status"] for r in rows}.issubset({"Active", "Terminated"}))) - s.run("FTE 0–1", lambda: [ - ok(0 < float(r["headcount_fte"]) <= 1.0, f"FTE={r['headcount_fte']} for {r['employee_id']}") - for r in rows - ]) - s.run("salaries positive", lambda: [ - ok(float(r["annual_salary_budget"]) > 0) for r in rows - ]) - s.run("headcount grows Jan→Dec", lambda: ok( - len([r for r in rows if r["period"] == "2024-12" and r["status"] == "Active"]) >= - len([r for r in rows if r["period"] == "2023-01" and r["status"] == "Active"]) - )) - return s - -# ── Reporter ────────────────────────────────────────────────────────────────── - -def run_suites(suites): - total = passed = failed = skipped = 0 - for suite in suites: - print(f"\n\033[1m{suite.name}\033[0m") - for r in suite.results: - total += 1 - if r.skipped: - skipped += 1 - print(f" \033[33m⊘\033[0m {r.name}") - print(f" {r.message}") - elif r.passed: - passed += 1 - print(f" \033[32m✓\033[0m {r.name}") - else: - failed += 1 - print(f" \033[31m✗\033[0m {r.name}") - for line in r.message.strip().splitlines(): - print(f" {line}") - - print(f"\n{'─'*48}") - color = "\033[32m" if failed == 0 else "\033[31m" - print(f"{color}\033[1m{passed}/{total} passed\033[0m" - + (f" \033[33m⊘\033[0m {skipped} skipped" if skipped else "") - + (f" \033[31m✗\033[0m {failed} failed" if failed else "")) - print() - return failed == 0 - -# ── Entry point ─────────────────────────────────────────────────────────────── - -if __name__ == "__main__": - import argparse - parser = argparse.ArgumentParser(description="FP&A test suite") - parser.add_argument("--url", default=None, help="API base URL") - args = parser.parse_args() - if args.url: - API_BASE = args.url - - ok = run_suites([ - suite_revenue(), - suite_opex(), - suite_pl(), - suite_cashflow(), - suite_headcount(), - ]) - sys.exit(0 if ok else 1) \ No newline at end of file diff --git a/tests/actual_test.go b/tests/actual_test.go new file mode 100644 index 0000000..c7235d6 --- /dev/null +++ b/tests/actual_test.go @@ -0,0 +1,268 @@ +package test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "Engine/internal/database" + "Engine/internal/handler" + "Engine/internal/service" + "Engine/tests/internal/testutil" +) + +// ── wire helpers ────────────────────────────────────────────────────────────── + +type testServer struct { + srv *httptest.Server + db interface{ Close() error } +} + +func newFullServer(t *testing.T) *httptest.Server { + t.Helper() + db := testutil.NewTestDB(t) + + actualsRepo := database.NewActualsRepo(db) + actualsH := handler.NewActualsHandler(actualsRepo) + + budgetRepo := database.NewBudgetRepo(db) + varianceH := handler.NewVarianceHandler(service.NewVarianceService(budgetRepo, actualsRepo)) + + mux := http.NewServeMux() + mux.HandleFunc("POST /api/v1/actuals/ingest", actualsH.Ingest) + mux.HandleFunc("GET /api/v1/variance", varianceH.Report) + mux.HandleFunc("GET /api/v1/variance/alerts", varianceH.Alerts) + + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + return srv +} + +func newActualsHandler(t *testing.T) *handler.ActualsHandler { + t.Helper() + return handler.NewActualsHandler(database.NewActualsRepo(testutil.NewTestDB(t))) +} + +// validActual returns one well-formed actual record. +func validActual() map[string]any { + return map[string]any{ + "department_id": 1, + "gl_account_id": 1, + "period": "2024-01", + "amount": 1234.56, + "source": "csv_import", + } +} + +// ── Actuals: Ingest ─────────────────────────────────────────────────────────── + +func TestIngestActuals_SingleRecord(t *testing.T) { + h := newActualsHandler(t) + + w := testutil.Do(t, http.HandlerFunc(h.Ingest), http.MethodPost, "/", + []any{validActual()}) + + testutil.AssertStatus(t, w, http.StatusCreated) +} + +func TestIngestActuals_MultipleRecords(t *testing.T) { + h := newActualsHandler(t) + + records := []any{ + map[string]any{"department_id": 1, "gl_account_id": 1, "period": "2024-01", "amount": 100.00, "source": "csv"}, + map[string]any{"department_id": 1, "gl_account_id": 2, "period": "2024-01", "amount": 200.00, "source": "csv"}, + map[string]any{"department_id": 2, "gl_account_id": 1, "period": "2024-01", "amount": 300.00, "source": "csv"}, + } + + w := testutil.Do(t, http.HandlerFunc(h.Ingest), http.MethodPost, "/", records) + testutil.AssertStatus(t, w, http.StatusCreated) +} + +func TestIngestActuals_EmptyList(t *testing.T) { + h := newActualsHandler(t) + + w := testutil.Do(t, http.HandlerFunc(h.Ingest), http.MethodPost, "/", []any{}) + // Depending on your handler: 201 with 0 rows ingested, or 400 + t.Logf("empty ingest: %d — verify against your handler", w.Code) +} + +func TestIngestActuals_InvalidJSON(t *testing.T) { + h := newActualsHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.Ingest), http.MethodPost, "/", nil) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestIngestActuals_MissingPeriod(t *testing.T) { + h := newActualsHandler(t) + record := validActual() + delete(record, "period") + w := testutil.Do(t, http.HandlerFunc(h.Ingest), http.MethodPost, "/", []any{record}) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestIngestActuals_NegativeAmount(t *testing.T) { + h := newActualsHandler(t) + record := validActual() + record["amount"] = -500.00 + w := testutil.Do(t, http.HandlerFunc(h.Ingest), http.MethodPost, "/", []any{record}) + // Adjust to match your validation rule + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestIngestActuals_Idempotent(t *testing.T) { + h := newActualsHandler(t) + fn := http.HandlerFunc(h.Ingest) + + testutil.Do(t, fn, http.MethodPost, "/", []any{validActual()}) + // Ingesting the same record twice should not panic or 500 + w := testutil.Do(t, fn, http.MethodPost, "/", []any{validActual()}) + if w.Code >= 500 { + t.Errorf("duplicate ingest returned %d, expected non-5xx", w.Code) + } +} + +// ── Variance: Report ────────────────────────────────────────────────────────── + +func TestVarianceReport_Empty(t *testing.T) { + srv := newFullServer(t) + + resp, err := srv.Client().Get(srv.URL + "/api/v1/variance?period=2024-01") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("variance report: got %d, want 200", resp.StatusCode) + } +} + +func TestVarianceReport_WithData(t *testing.T) { + srv := newFullServer(t) + client := srv.Client() + + // Seed a budget + budgetPayload := map[string]any{ + "department_id": 1, "gl_account_id": 1, "period": "2024-01", "amount": 10000.00, + } + resp, _ := client.Post(srv.URL+"/api/v1/budgets", "application/json", + mustJSON(t, budgetPayload)) + resp.Body.Close() + + // Ingest an actual — under budget + actualPayload := []any{ + map[string]any{"department_id": 1, "gl_account_id": 1, "period": "2024-01", "amount": 7500.00, "source": "test"}, + } + resp2, _ := client.Post(srv.URL+"/api/v1/actuals/ingest", "application/json", + mustJSON(t, actualPayload)) + resp2.Body.Close() + + // Get variance report + resp3, err := client.Get(srv.URL + "/api/v1/variance?period=2024-01") + if err != nil { + t.Fatal(err) + } + defer resp3.Body.Close() + + if resp3.StatusCode != http.StatusOK { + t.Errorf("variance report: got %d, want 200", resp3.StatusCode) + } +} + +func TestVarianceReport_MissingPeriodParam(t *testing.T) { + srv := newFullServer(t) + + resp, err := srv.Client().Get(srv.URL + "/api/v1/variance") + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + + // Should return 400 if period is required, or 200 with all periods + t.Logf("variance without period param: %d — verify against your handler", resp.StatusCode) +} + +// ── Variance: Alerts ────────────────────────────────────────────────────────── + +func TestVarianceAlerts_NoAlerts(t *testing.T) { + srv := newFullServer(t) + + resp, err := srv.Client().Get(srv.URL + "/api/v1/variance/alerts?period=2024-01") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("alerts: got %d, want 200", resp.StatusCode) + } +} + +func TestVarianceAlerts_OverBudgetTriggersAlert(t *testing.T) { + srv := newFullServer(t) + client := srv.Client() + + // Budget: 1000 + budgetPayload := map[string]any{ + "department_id": 1, "gl_account_id": 1, "period": "2024-02", "amount": 1000.00, + } + resp, _ := client.Post(srv.URL+"/api/v1/budgets", "application/json", + mustJSON(t, budgetPayload)) + resp.Body.Close() + + // Actual: 1500 — 50% over budget + actualPayload := []any{ + map[string]any{"department_id": 1, "gl_account_id": 1, "period": "2024-02", "amount": 1500.00, "source": "test"}, + } + resp2, _ := client.Post(srv.URL+"/api/v1/actuals/ingest", "application/json", + mustJSON(t, actualPayload)) + resp2.Body.Close() + + resp3, err := client.Get(srv.URL + "/api/v1/variance/alerts?period=2024-02") + if err != nil { + t.Fatal(err) + } + defer resp3.Body.Close() + + if resp3.StatusCode != http.StatusOK { + t.Errorf("alerts: got %d, want 200", resp3.StatusCode) + } + + // Decode whatever shape your alerts response is + var alerts any + if err := json.NewDecoder(resp3.Body).Decode(&alerts); err != nil { + t.Fatalf("decode alerts: %v", err) + } + t.Logf("alerts payload: %+v", alerts) +} + +func TestVarianceAlerts_UnderBudgetNoAlert(t *testing.T) { + srv := newFullServer(t) + client := srv.Client() + + // Budget: 5000, Actual: 1000 — well under, no alert expected + budgetPayload := map[string]any{ + "department_id": 2, "gl_account_id": 2, "period": "2024-03", "amount": 5000.00, + } + resp, _ := client.Post(srv.URL+"/api/v1/budgets", "application/json", + mustJSON(t, budgetPayload)) + resp.Body.Close() + + actualPayload := []any{ + map[string]any{"department_id": 2, "gl_account_id": 2, "period": "2024-03", "amount": 1000.00, "source": "test"}, + } + resp2, _ := client.Post(srv.URL+"/api/v1/actuals/ingest", "application/json", + mustJSON(t, actualPayload)) + resp2.Body.Close() + + resp3, err := client.Get(srv.URL + "/api/v1/variance/alerts?period=2024-03") + if err != nil { + t.Fatal(err) + } + defer resp3.Body.Close() + + if resp3.StatusCode != http.StatusOK { + t.Errorf("alerts: got %d, want 200", resp3.StatusCode) + } +} diff --git a/tests/budget_test.go b/tests/budget_test.go new file mode 100644 index 0000000..b7b0e68 --- /dev/null +++ b/tests/budget_test.go @@ -0,0 +1,259 @@ +package test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "Engine/internal/database" + "Engine/internal/handler" + "Engine/internal/model" + "Engine/internal/service" + "Engine/tests/internal/testutil" +) + +// ── wire helpers ────────────────────────────────────────────────────────────── + +func newBudgetServer(t *testing.T) *httptest.Server { + t.Helper() + db := testutil.NewTestDB(t) + repo := database.NewBudgetRepo(db) + 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) + + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + return srv +} + +func newBudgetHandler(t *testing.T) *handler.BudgetHandler { + t.Helper() + return handler.NewBudgetHandler(service.NewBudgetService(database.NewBudgetRepo(testutil.NewTestDB(t)))) +} + +func validBudget() map[string]any { + return map[string]any{ + "fiscal_year": 2024, + "fiscal_period": 1, + "version": "original", // adjust to match your BudgetVersion values + "department_id": 1, + "gl_account_id": 1, + "amount": 5000.00, + "currency": "USD", + "notes": "", + "created_by": "test", + } +} + +// ── Create ──────────────────────────────────────────────────────────────────── + +func TestCreateBudget_OK(t *testing.T) { + h := newBudgetHandler(t) + + w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", validBudget()) + testutil.AssertStatus(t, w, http.StatusCreated) + + var got model.Budget + testutil.DecodeJSON(t, w, &got) + if got.ID == 0 { + t.Error("expected non-zero ID") + } + if got.Amount != 5000.00 { + t.Errorf("amount: got %v, want 5000.00", got.Amount) + } +} + +func TestCreateBudget_InvalidJSON(t *testing.T) { + h := newBudgetHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", nil) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateBudget_MissingPeriod(t *testing.T) { + h := newBudgetHandler(t) + body := validBudget() + delete(body, "fiscal_period") + w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", body) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateBudget_ZeroAmount(t *testing.T) { + h := newBudgetHandler(t) + body := validBudget() + body["amount"] = 0 + w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", body) + // Whether 0 is rejected or accepted depends on your business rule — adjust to match + t.Logf("zero amount response: %d — verify against your handler", w.Code) +} + +func TestCreateBudget_NegativeAmount(t *testing.T) { + h := newBudgetHandler(t) + body := validBudget() + body["amount"] = -100 + w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", body) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +// ── Update ──────────────────────────────────────────────────────────────────── + +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())) + if err != nil { + t.Fatal(err) + } + var created model.Budget + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + // Update amount + updated := validBudget() + updated["amount"] = 9999.99 + + req, _ := http.NewRequest(http.MethodPut, + fmt.Sprintf("%s/api/v1/budgets/%d", srv.URL, created.ID), + mustJSON(t, updated)) + 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 { + 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) + } + } +} + +func TestUpdateBudget_NotFound(t *testing.T) { + srv := newBudgetServer(t) + + req, _ := http.NewRequest(http.MethodPut, srv.URL+"/api/v1/budgets/9999", + mustJSON(t, validBudget())) + req.Header.Set("Content-Type", "application/json") + resp, err := srv.Client().Do(req) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + + if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNoContent { + t.Errorf("update non-existent: got %d, want 404 or 204", resp.StatusCode) + } +} + +func TestUpdateBudget_InvalidJSON(t *testing.T) { + srv := newBudgetServer(t) + + // Create one first so the ID exists + resp, _ := srv.Client().Post(srv.URL+"/api/v1/budgets", "application/json", + mustJSON(t, validBudget())) + var created model.Budget + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + req, _ := http.NewRequest(http.MethodPut, + fmt.Sprintf("%s/api/v1/budgets/%d", srv.URL, created.ID), + bytes.NewBufferString("not-json")) + req.Header.Set("Content-Type", "application/json") + resp2, err := srv.Client().Do(req) + if err != nil { + t.Fatal(err) + } + resp2.Body.Close() + if resp2.StatusCode != http.StatusBadRequest { + t.Errorf("invalid JSON update: got %d, want 400", resp2.StatusCode) + } +} + +// ── Delete ──────────────────────────────────────────────────────────────────── + +func TestDeleteBudget_OK(t *testing.T) { + srv := newBudgetServer(t) + client := srv.Client() + + resp, err := client.Post(srv.URL+"/api/v1/budgets", "application/json", + mustJSON(t, validBudget())) + if err != nil { + t.Fatal(err) + } + var created model.Budget + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + req, _ := http.NewRequest(http.MethodDelete, + srv.URL+"/api/v1/budgets/"+strconv.Itoa(created.ID), nil) + resp2, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + resp2.Body.Close() + + if resp2.StatusCode != http.StatusNoContent && resp2.StatusCode != http.StatusOK { + t.Errorf("delete: got %d, want 200 or 204", resp2.StatusCode) + } +} + +func TestDeleteBudget_NotFound(t *testing.T) { + srv := newBudgetServer(t) + + req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/budgets/9999", nil) + resp, err := srv.Client().Do(req) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + + if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNoContent { + t.Errorf("delete non-existent: got %d, want 404 or 204", resp.StatusCode) + } +} + +func TestDeleteBudget_DoubleDelete(t *testing.T) { + srv := newBudgetServer(t) + client := srv.Client() + + resp, _ := client.Post(srv.URL+"/api/v1/budgets", "application/json", + mustJSON(t, validBudget())) + var created model.Budget + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + url := srv.URL + "/api/v1/budgets/" + strconv.Itoa(created.ID) + + req1, _ := http.NewRequest(http.MethodDelete, url, nil) + resp1, _ := client.Do(req1) + resp1.Body.Close() + + // Second delete — should not panic, should return 404 or 204 + req2, _ := http.NewRequest(http.MethodDelete, url, nil) + resp2, err := client.Do(req2) + if err != nil { + t.Fatal(err) + } + resp2.Body.Close() + + if resp2.StatusCode != http.StatusNotFound && resp2.StatusCode != http.StatusNoContent { + t.Errorf("double delete: got %d, want 404 or 204", resp2.StatusCode) + } +} diff --git a/tests/internal/testutil/testutil.go b/tests/internal/testutil/testutil.go new file mode 100644 index 0000000..8782ae1 --- /dev/null +++ b/tests/internal/testutil/testutil.go @@ -0,0 +1,133 @@ +package testutil + +// Package testutil provides test helpers for handler and integration tests. +// It wires up an in-memory SQLite database, a lightweight HTTP request helper, +// and common assertion utilities. + +import ( + "Engine/internal/database" + "bytes" + "database/sql" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + _ "modernc.org/sqlite" // pure-Go SQLite driver, no CGO required +) + +// ── Database ────────────────────────────────────────────────────────────────── + +// NewTestDB opens a fresh in-memory SQLite database, runs your schema +// migrations, and registers a t.Cleanup to close it when the test ends. +// +// Each call gets its own isolated database — tests never share state. +func NewTestDB(t *testing.T) *sql.DB { + t.Helper() + + // "file::memory:?cache=shared" would share across connections; + // the plain ":memory:" gives a fully isolated DB per open call. + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + t.Fatalf("testutil.NewTestDB: open: %v", err) + } + + // SQLite in-memory works best with a single connection. + db.SetMaxOpenConns(1) + + if err := database.Migrate(db); err != nil { + db.Close() + t.Fatalf("testutil.NewTestDB: migrate: %v", err) + } + + t.Cleanup(func() { db.Close() }) + return db +} + +// ── HTTP helpers ────────────────────────────────────────────────────────────── + +// Do fires an HTTP request directly at handler h and returns the recorded +// response. body is marshalled to JSON; pass nil to send no body. +// +// Usage: +// +// w := testutil.Do(t, http.HandlerFunc(h.Ingest), http.MethodPost, "/", payload) +func Do(t *testing.T, h http.Handler, method, path string, body any) *httptest.ResponseRecorder { + t.Helper() + + var r io.Reader + if body != nil { + b, err := json.Marshal(body) + if err != nil { + t.Fatalf("testutil.Do: marshal body: %v", err) + } + r = bytes.NewReader(b) + } + + req := httptest.NewRequest(method, path, r) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + w := httptest.NewRecorder() + h.ServeHTTP(w, req) + return w +} + +// MustJSON marshals v to JSON and returns it as an io.Reader. +// Intended for use with http.Client.Post in full-server integration tests. +// +// Usage: +// +// resp, _ := client.Post(srv.URL+"/api/v1/budgets", "application/json", testutil.MustJSON(t, payload)) +func MustJSON(t *testing.T, v any) io.Reader { + t.Helper() + b, err := json.Marshal(v) + if err != nil { + t.Fatalf("testutil.MustJSON: %v", err) + } + return bytes.NewReader(b) +} + +// ── Assertions ──────────────────────────────────────────────────────────────── + +// AssertStatus fails the test if the recorder's status code does not match want. +func AssertStatus(t *testing.T, w *httptest.ResponseRecorder, want int) { + t.Helper() + if w.Code != want { + t.Errorf("status: got %d, want %d\n\tbody: %s", w.Code, want, w.Body.String()) + } +} + +// AssertJSONField decodes the recorder's body as a JSON object and checks that +// the top-level key equals the expected string value. Useful for quick smoke +// checks without defining a full response struct. +// +// Usage: +// +// testutil.AssertJSONField(t, w, "status", "ok") +func AssertJSONField(t *testing.T, w *httptest.ResponseRecorder, key, want string) { + t.Helper() + var m map[string]any + if err := json.NewDecoder(w.Body).Decode(&m); err != nil { + t.Fatalf("AssertJSONField: decode body: %v", err) + } + got, ok := m[key] + if !ok { + t.Errorf("AssertJSONField: key %q not found in response", key) + return + } + if got != want { + t.Errorf("AssertJSONField: %q = %q, want %q", key, got, want) + } +} + +// DecodeJSON decodes the recorder's body into dst. +// Fails the test immediately if decoding errors. +func DecodeJSON(t *testing.T, w *httptest.ResponseRecorder, dst any) { + t.Helper() + if err := json.NewDecoder(w.Body).Decode(dst); err != nil { + t.Fatalf("DecodeJSON: %v\n\tbody: %s", err, w.Body.String()) + } +} diff --git a/tests/refrence_test.go b/tests/refrence_test.go new file mode 100644 index 0000000..422ed87 --- /dev/null +++ b/tests/refrence_test.go @@ -0,0 +1,334 @@ +package test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "Engine/internal/database" + "Engine/internal/handler" + "Engine/internal/model" + "Engine/tests/internal/testutil" +) + +// ── wire helpers ────────────────────────────────────────────────────────────── + +func newReferenceHandler(t *testing.T) *handler.ReferenceHandler { + t.Helper() + return handler.NewReferenceHandler(database.NewReferenceRepo(testutil.NewTestDB(t))) +} + +// newReferenceServer spins up a real httptest.Server with the production +// mux routes. Use this for delete/path-param tests that need {id} routing. +func newReferenceServer(t *testing.T) *httptest.Server { + t.Helper() + h := newReferenceHandler(t) + + mux := http.NewServeMux() + mux.HandleFunc("POST /api/v1/departments", h.CreateDepartment) + mux.HandleFunc("GET /api/v1/departments", h.ListDepartments) + mux.HandleFunc("DELETE /api/v1/departments/{id}", h.DeleteDepartment) + mux.HandleFunc("POST /api/v1/gl-accounts", h.CreateGLAccount) + mux.HandleFunc("GET /api/v1/gl-accounts", h.ListGLAccounts) + mux.HandleFunc("DELETE /api/v1/gl-accounts/{id}", h.DeleteGLAccount) + + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + return srv +} + +// ── Department: Create ──────────────────────────────────────────────────────── + +func TestCreateDepartment_OK(t *testing.T) { + h := newReferenceHandler(t) + + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", + map[string]any{"code": "ENG", "name": "Engineering", "active": true}) + + testutil.AssertStatus(t, w, http.StatusCreated) + + var got database.Department + testutil.DecodeJSON(t, w, &got) + if got.Code != "ENG" { + t.Errorf("code: got %q, want %q", got.Code, "ENG") + } + if got.ID == 0 { + t.Error("expected non-zero ID in response") + } + if !got.Active { + t.Error("expected active=true") + } +} + +func TestCreateDepartment_DefaultsActiveTrue(t *testing.T) { + h := newReferenceHandler(t) + + // active field omitted — handler must default it to true + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", + map[string]any{"code": "MKT", "name": "Marketing"}) + + testutil.AssertStatus(t, w, http.StatusCreated) + + var got database.Department + testutil.DecodeJSON(t, w, &got) + if !got.Active { + t.Error("expected active to default to true when omitted") + } +} + +func TestCreateDepartment_MissingCode(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", + map[string]any{"name": "Engineering"}) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateDepartment_MissingName(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", + map[string]any{"code": "ENG"}) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateDepartment_WhitespaceOnly(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", + map[string]any{"code": " ", "name": " "}) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateDepartment_InvalidJSON(t *testing.T) { + h := newReferenceHandler(t) + // nil body → empty body → JSON decode fails → 400 + w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", nil) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateDepartment_Upsert(t *testing.T) { + h := newReferenceHandler(t) + fn := http.HandlerFunc(h.CreateDepartment) + + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "FIN", "name": "Finance"}) + + 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 + testutil.DecodeJSON(t, w, &got) + if got.Name != "Finance Updated" { + t.Errorf("upsert name: got %q, want %q", got.Name, "Finance Updated") + } +} + +// ── Department: List ────────────────────────────────────────────────────────── + +func TestListDepartments_Empty(t *testing.T) { + h := newReferenceHandler(t) + + w := testutil.Do(t, http.HandlerFunc(h.ListDepartments), http.MethodGet, "/", nil) + testutil.AssertStatus(t, w, http.StatusOK) + + var got []database.Department + testutil.DecodeJSON(t, w, &got) + if len(got) != 0 { + t.Errorf("expected empty list, got %d", len(got)) + } +} + +func TestListDepartments_ReturnAll(t *testing.T) { + h := newReferenceHandler(t) + fn := http.HandlerFunc(h.CreateDepartment) + + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "HR", "name": "Human Resources"}) + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "IT", "name": "Information Technology"}) + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "OPS", "name": "Operations"}) + + w := testutil.Do(t, http.HandlerFunc(h.ListDepartments), http.MethodGet, "/", nil) + testutil.AssertStatus(t, w, http.StatusOK) + + var got []database.Department + testutil.DecodeJSON(t, w, &got) + if len(got) != 3 { + t.Errorf("expected 3 departments, got %d", len(got)) + } +} + +// ── Department: Delete ──────────────────────────────────────────────────────── + +func TestDeleteDepartment_OK(t *testing.T) { + srv := newReferenceServer(t) + client := srv.Client() + + resp, err := client.Post(srv.URL+"/api/v1/departments", "application/json", + mustJSON(t, map[string]any{"code": "OPS", "name": "Operations"})) + if err != nil { + t.Fatal(err) + } + var created database.Department + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/departments/"+strconv.Itoa(created.ID), nil) + resp2, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + resp2.Body.Close() + + if resp2.StatusCode != http.StatusNoContent && resp2.StatusCode != http.StatusOK { + t.Errorf("delete: got %d, want 200 or 204", resp2.StatusCode) + } +} + +func TestDeleteDepartment_NotFound(t *testing.T) { + srv := newReferenceServer(t) + + req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/departments/9999", nil) + resp, err := srv.Client().Do(req) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + + // Accept 404 or 204 — adjust to match your handler's behaviour + if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNoContent { + t.Errorf("delete non-existent: got %d, want 404 or 204", resp.StatusCode) + } +} + +// ── GL Account: Create ──────────────────────────────────────────────────────── + +func TestCreateGLAccount_OK(t *testing.T) { + h := newReferenceHandler(t) + + w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/", + map[string]any{"code": "5001", "name": "Travel Expenses", "type": "expense"}) + + testutil.AssertStatus(t, w, http.StatusCreated) + + var got database.GLAccount + testutil.DecodeJSON(t, w, &got) + if got.Code != "5001" { + t.Errorf("code: got %q, want %q", got.Code, "5001") + } + if got.ID == 0 { + t.Error("expected non-zero ID") + } +} + +func TestCreateGLAccount_MissingCode(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/", + map[string]any{"name": "Travel Expenses"}) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateGLAccount_MissingName(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/", + map[string]any{"code": "5001"}) + testutil.AssertStatus(t, w, http.StatusBadRequest) +} + +func TestCreateGLAccount_Upsert(t *testing.T) { + h := newReferenceHandler(t) + fn := http.HandlerFunc(h.CreateGLAccount) + + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue"}) + + w := testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue Updated"}) + testutil.AssertStatus(t, w, http.StatusCreated) + + var got model.GLAccount + testutil.DecodeJSON(t, w, &got) + if got.Code != "Revenue Updated" { + t.Errorf("upsert name: got %q, want %q", got.Code, "Revenue Updated") + } +} + +// ── GL Account: List ────────────────────────────────────────────────────────── + +func TestListGLAccounts_Empty(t *testing.T) { + h := newReferenceHandler(t) + w := testutil.Do(t, http.HandlerFunc(h.ListGLAccounts), http.MethodGet, "/", nil) + testutil.AssertStatus(t, w, http.StatusOK) + + var got []database.GLAccount + testutil.DecodeJSON(t, w, &got) + if len(got) != 0 { + t.Errorf("expected empty list, got %d", len(got)) + } +} + +func TestListGLAccounts_ReturnAll(t *testing.T) { + h := newReferenceHandler(t) + fn := http.HandlerFunc(h.CreateGLAccount) + + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue"}) + testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "5001", "name": "COGS"}) + + w := testutil.Do(t, http.HandlerFunc(h.ListGLAccounts), http.MethodGet, "/", nil) + testutil.AssertStatus(t, w, http.StatusOK) + + var got []database.GLAccount + testutil.DecodeJSON(t, w, &got) + if len(got) != 2 { + t.Errorf("expected 2 GL accounts, got %d", len(got)) + } +} + +// ── GL Account: Delete ──────────────────────────────────────────────────────── + +func TestDeleteGLAccount_OK(t *testing.T) { + srv := newReferenceServer(t) + client := srv.Client() + + resp, err := client.Post(srv.URL+"/api/v1/gl-accounts", "application/json", + mustJSON(t, map[string]any{"code": "6001", "name": "Rent"})) + if err != nil { + t.Fatal(err) + } + var created database.GLAccount + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/gl-accounts/"+strconv.Itoa(created.ID), nil) + resp2, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + resp2.Body.Close() + + if resp2.StatusCode != http.StatusNoContent && resp2.StatusCode != http.StatusOK { + t.Errorf("delete: got %d, want 200 or 204", resp2.StatusCode) + } +} + +func TestDeleteGLAccount_NotFound(t *testing.T) { + srv := newReferenceServer(t) + + req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/gl-accounts/9999", nil) + resp, err := srv.Client().Do(req) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + + if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNoContent { + t.Errorf("delete non-existent: got %d, want 404 or 204", resp.StatusCode) + } +} + +// ── local helpers ───────────────────────────────────────────────────────────── + +func mustJSON(t *testing.T, v any) *bytes.Reader { + t.Helper() + b, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + return bytes.NewReader(b) +}