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 }