package model import "time" type GLAccountType string const ( GLRevenue GLAccountType = "revenue" GLCOGS GLAccountType = "cogs" GLOpex GLAccountType = "opex" GLCapex GLAccountType = "capex" GLHeadcount GLAccountType = "headcount" ) type BudgetVersion string const ( VersionOriginal BudgetVersion = "original" VersionForecast1 BudgetVersion = "forecast_1" VersionForecast2 BudgetVersion = "forecast_2" ) type Department struct { ID int `json:"id"` Code string `json:"code"` Name string `json:"name"` CostCenter string `json:"cost_center"` Active bool `json:"active"` CreatedAt string `json:"created_at"` } type CreateDepartmentRequest struct { Code string `json:"code"` Name string `json:"name"` CostCenter string `json:"cost_center"` Active bool `json:"active"` } type GetDepartmentRequest struct { Code *string `json:"code"` Name *string `json:"name"` CostCenter *string `json:"cost_center"` } type SetDepartmentActivity struct { Active bool `json:"active"` Code *string `json:"code"` Name *string `json:"name"` CostCenter *string `json:"cost_center"` } type DeleteDepartmentRequest struct { ID int `json:"id"` } type DeleteGLAccountRequest struct { ID int `json:"id"` } type GLAccount struct { ID int `json:"id"` Code string `json:"code"` Description string `json:"description"` Type string `json:"type"` // revenue | cogs | opex | capex | headcount FavourHigh bool `json:"favour_high"` // true = over-budget is good (revenue accounts) Active bool `json:"active"` } type GetDepartmentBudget struct { Code string `json:"code"` } type GetDepartmentActual struct { Code string `json:"code"` } type GetGLAccountBudget struct { Code string `json:"code"` } type GetGLAccountActual struct { Code string `json:"code"` } type Budget struct { ID int `json:"id"` FiscalYear int `json:"fiscal_year"` 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,omitempty"` CreatedBy string `json:"created_by"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type CreateBudgetRequest struct { 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"` 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 // required, not a pointer ChangedBy string // required, not a pointer FiscalYear *int FiscalPeriod *int Version *string DepartmentID *int GLAccountID *int Amount *float64 Currency *string Notes *string } type DeleteBudgetRequest struct { ID int `json:"id"` } func (uBudget *UpdateBudgetRequest) Valid() []string { var errs []string // Always required: identity + audit if uBudget.ID == 0 { errs = append(errs, "id is required") } if uBudget.ChangedBy == "" { errs = append(errs, "changed_by is required") } // Validate fields only if they were provided if uBudget.FiscalYear != nil { if *uBudget.FiscalYear < 1 || *uBudget.FiscalYear > 2200 { errs = append(errs, "fiscal_year must be between 1 and 2200") } } if uBudget.FiscalPeriod != nil { if *uBudget.FiscalPeriod < 1 || *uBudget.FiscalPeriod > 12 { errs = append(errs, "fiscal_period must be between 1 and 12") } } if uBudget.Amount != nil && *uBudget.Amount <= 0 { errs = append(errs, "amount must be greater than 0") } if uBudget.Version != nil && *uBudget.Version == "" { errs = append(errs, "version cannot be empty") } if uBudget.Currency != nil && *uBudget.Currency == "" { errs = append(errs, "currency cannot be empty") } // At least one field must be set — otherwise there's nothing to do if uBudget.FiscalYear == nil && uBudget.FiscalPeriod == nil && uBudget.Version == nil && uBudget.DepartmentID == nil && uBudget.GLAccountID == nil && uBudget.Amount == nil && uBudget.Currency == nil && uBudget.Notes == nil { errs = append(errs, "at least one field must be provided to update") } return errs } type Actual struct { ID int `json:"id"` FiscalYear int `json:"fiscal_year"` FiscalPeriod int `json:"fiscal_period"` DepartmentID int `json:"department_id"` GLAccountID int `json:"gl_account_id"` GLCode string `json:"gl_code"` Amount float64 `json:"amount"` Currency string `json:"currency"` Source string `json:"source"` IngestedAt time.Time `json:"ingested_at"` } type IngestActualsRequest struct { FiscalYear int `json:"fiscal_year"` FiscalPeriod int `json:"fiscal_period"` DeptCode string `json:"dept_code"` GLCode string `json:"gl_code"` Amount float64 `json:"amount"` Currency string `json:"currency"` 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 ( StatusFavourable VarianceStatus = "favourable" StatusUnfavourable VarianceStatus = "unfavourable" StatusOnBudget VarianceStatus = "on_budget" ) type VarianceLine struct { GLCode string `json:"gl_account_id"` GLDescription string `json:"gl_description"` GLType GLAccountType `json:"gl_type"` Budget float64 `json:"budget"` Actual float64 `json:"actual"` VarianceAbs float64 `json:"variance_abs"` VariancePct *float64 `json:"variance_pct"` Status VarianceStatus `json:"status"` Currency string `json:"currency"` } type VarianceReport struct { Department string `json:"department"` FiscalYear int `json:"fiscal_year"` FiscalPeriod int `json:"fiscal_period"` Version BudgetVersion `json:"version"` Currency string `json:"currency"` Lines []VarianceLine `json:"lines"` TotalBudget float64 `json:"total_budget"` TotalActual float64 `json:"total_actual"` TotalVariance float64 `json:"total_variance"` VariancePct *float64 `json:"total_variance_pct"` } type AlertThreshold struct { GLCode string `json:"gl_code"` Description string `json:"description"` Budget float64 `json:"budget"` Actual float64 `json:"actual"` VariancePct float64 `json:"variance_pct"` Status VarianceStatus `json:"status"` Department string `json:"department"` } // BudgetRow is used internally by the variance service type BudgetRow struct { GLCode string GLDescription string GLType GLAccountType FavourHigh bool Amount float64 Currency string } // GLAmountRow is a single GL line returned by the ReportRepo amount queries. // Used as the building block for PnLSection lines. type GLAmountRow struct { GLCode string `json:"gl_code"` Description string `json:"description"` Amount float64 `json:"amount"` Currency string `json:"currency"` } // RevenueAmounts holds the per-GL amounts for revenue accounts in a single period. // Used as input for P&L rollup and reforecast calculations. type RevenueAmounts struct { GLCode string Description string Amount float64 Currency string } // PnLSection is one block in the P&L (Revenue, COGS, Opex, etc.) // Lines are the individual GL rows; Total is their sum. type PnLSection struct { Lines []GLAmountRow `json:"lines"` Total float64 `json:"total"` } // PnLReport is the full P&L for a single period/dept/version. // GrossProfit = Revenue - COGS // EBIT = GrossProfit - Opex - Headcount // NetIncome = EBIT - Capex type PnLReport struct { Department string `json:"department"` FiscalYear int `json:"fiscal_year"` FiscalPeriod int `json:"fiscal_period"` Version BudgetVersion `json:"version"` Currency string `json:"currency"` Revenue PnLSection `json:"revenue"` COGS PnLSection `json:"cogs"` Opex PnLSection `json:"opex"` Headcount PnLSection `json:"headcount"` Capex PnLSection `json:"capex"` GrossProfit float64 `json:"gross_profit"` // Revenue - COGS EBIT float64 `json:"ebit"` // GrossProfit - Opex - Headcount NetIncome float64 `json:"net_income"` // EBIT - Capex } type PnLRequest struct { FiscalYears []int FiscalPeriods []int DeptCodes []string Version BudgetVersion }