117 lines
2.8 KiB
Go
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
|
|
}
|