Files
FPandA-Engine/internal/service/variance-service.go
2026-03-20 14:01:46 +01:00

117 lines
2.8 KiB
Go

package service
import (
"context"
"math"
"Engine/internal/database"
"Engine/internal/model"
)
type VarianceService struct {
budgets *database.BudgetRepo
actuals *database.ActualsRepo
}
func NewVarianceService(b *database.BudgetRepo, a *database.ActualsRepo) *VarianceService {
return &VarianceService{budgets: b, actuals: a}
}
type VarianceFilter struct {
FiscalYear int
FiscalPeriod int
DeptCode string
Version model.BudgetVersion
}
func (s *VarianceService) Report(ctx context.Context, f VarianceFilter) (*model.VarianceReport, error) {
budgets, err := s.budgets.List(ctx, f.FiscalYear, f.FiscalPeriod, f.DeptCode, f.Version)
if err != nil {
return nil, err
}
actualsByGL, err := s.actuals.ListByPeriod(ctx, f.FiscalYear, f.FiscalPeriod, f.DeptCode)
if err != nil {
return nil, err
}
report := &model.VarianceReport{
FiscalYear: f.FiscalYear,
FiscalPeriod: f.FiscalPeriod,
Version: f.Version,
Department: f.DeptCode,
Currency: "DKK",
}
for _, b := range budgets {
actual := actualsByGL[b.GLCode]
varAbs := actual - b.Amount
var varPct *float64
if b.Amount != 0 {
v := math.Round((varAbs/b.Amount)*10000) / 100
varPct = &v
}
report.Lines = append(report.Lines, model.VarianceLine{
GLCode: b.GLCode,
GLDescription: b.GLDescription,
GLType: b.GLType,
Budget: b.Amount,
Actual: actual,
VarianceAbs: varAbs,
VariancePct: varPct,
Status: computeStatus(varAbs, b.FavourHigh),
Currency: b.Currency,
})
report.TotalBudget += b.Amount
report.TotalActual += actual
}
report.TotalVariance = report.TotalActual - report.TotalBudget
if report.TotalBudget != 0 {
v := math.Round((report.TotalVariance/report.TotalBudget)*10000) / 100
report.VariancePct = &v
}
return report, nil
}
func (s *VarianceService) Alerts(ctx context.Context, f VarianceFilter, thresholdPct float64) ([]model.AlertThreshold, error) {
report, err := s.Report(ctx, f)
if err != nil {
return nil, err
}
var alerts []model.AlertThreshold
for _, line := range report.Lines {
if line.VariancePct == nil {
continue
}
if math.Abs(*line.VariancePct) >= thresholdPct {
alerts = append(alerts, model.AlertThreshold{
GLCode: line.GLCode,
Description: line.GLDescription,
Budget: line.Budget,
Actual: line.Actual,
VariancePct: *line.VariancePct,
Status: line.Status,
Department: report.Department,
})
}
}
return alerts, nil
}
func computeStatus(varAbs float64, favourHigh bool) model.VarianceStatus {
const epsilon = 0.01
if math.Abs(varAbs) < epsilon {
return model.StatusOnBudget
}
if (favourHigh && varAbs > 0) || (!favourHigh && varAbs < 0) {
return model.StatusFavourable
}
return model.StatusUnfavourable
}