333 lines
9.5 KiB
Go
333 lines
9.5 KiB
Go
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"`
|
|
}
|
|
|
|
type GetDepartmentBudget struct {
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
type GetDepartmentActual struct {
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
type GLAccount struct {
|
|
ID int `json:"id"`
|
|
Code string `json:"code"`
|
|
Description string `json:"description"`
|
|
Type GLAccountType `json:"type"`
|
|
FavourHigh bool `json:"favour_high"`
|
|
}
|
|
|
|
type GetGLAccountBudget struct {
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
type GetGLAccountActual struct {
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
type Budget struct {
|
|
ID int `json:"id"`
|
|
FiscalYear int `json:"fiscal_year"`
|
|
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
|
|
}
|