Files
FPandA-Engine/internal/model/model.go
2026-03-21 18:14:23 +01:00

364 lines
10 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"`
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
}