Skip to content

Commit

Permalink
Merge pull request #14385 from transcom/B-21353-Price-Escalations-Main
Browse files Browse the repository at this point in the history
B 21353 price escalations main
  • Loading branch information
WeatherfordAaron authored Dec 12, 2024
2 parents 4f88886 + 8c4dace commit 6320f57
Show file tree
Hide file tree
Showing 7 changed files with 385 additions and 8 deletions.
88 changes: 88 additions & 0 deletions pkg/models/re_contract_year.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package models

import (
"fmt"
"time"

"github.com/gobuffalo/pop/v6"
Expand All @@ -9,6 +10,29 @@ import (
"github.com/gofrs/uuid"
)

const (
BasePeriodYear1 string = "Base Period Year 1"
BasePeriodYear2 string = "Base Period Year 2"
BasePeriodYear3 string = "Base Period Year 3"
OptionPeriod1 string = "Option Period 1"
OptionPeriod2 string = "Option Period 2"
OptionPeriod3 string = "Option Period 3"
AwardTerm1 string = "Award Term 1"
AwardTerm2 string = "Award Term 2"
AwardTerm string = "Award Term"
OptionPeriod string = "Option Period"
BasePeriodYear string = "Base Period Year"
)

type ExpectedEscalationPriceContractsCount struct {
ExpectedAmountOfContractYearsForCalculation int
ExpectedAmountOfBasePeriodYearsForCalculation int
ExpectedAmountOfOptionPeriodYearsForCalculation int
ExpectedAmountOfAwardTermsForCalculation int
}

var ContractYears = []string{AwardTerm1, AwardTerm2, OptionPeriod1, OptionPeriod2, OptionPeriod3, BasePeriodYear2, BasePeriodYear3}

// ReContractYear represents a single "year" of a contract
type ReContractYear struct {
ID uuid.UUID `json:"id" db:"id"`
Expand Down Expand Up @@ -46,3 +70,67 @@ func (r *ReContractYear) Validate(_ *pop.Connection) (*validate.Errors, error) {
&Float64IsGreaterThan{Field: r.EscalationCompounded, Name: "EscalationCompounded", Compared: 0},
), nil
}

func GetExpectedEscalationPriceContractsCount(contractYearName string) (ExpectedEscalationPriceContractsCount, error) {
switch contractYearName {
case BasePeriodYear1:
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 1,
ExpectedAmountOfBasePeriodYearsForCalculation: 1,
ExpectedAmountOfOptionPeriodYearsForCalculation: 0,
ExpectedAmountOfAwardTermsForCalculation: 0,
}, nil
case BasePeriodYear2:
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 2,
ExpectedAmountOfBasePeriodYearsForCalculation: 2,
ExpectedAmountOfOptionPeriodYearsForCalculation: 0,
ExpectedAmountOfAwardTermsForCalculation: 0,
}, nil
case BasePeriodYear3:
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 3,
ExpectedAmountOfBasePeriodYearsForCalculation: 3,
ExpectedAmountOfOptionPeriodYearsForCalculation: 0,
ExpectedAmountOfAwardTermsForCalculation: 0,
}, nil
case OptionPeriod1:
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 4,
ExpectedAmountOfBasePeriodYearsForCalculation: 3,
ExpectedAmountOfOptionPeriodYearsForCalculation: 1,
ExpectedAmountOfAwardTermsForCalculation: 0,
}, nil
case OptionPeriod2:
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 5,
ExpectedAmountOfBasePeriodYearsForCalculation: 3,
ExpectedAmountOfOptionPeriodYearsForCalculation: 2,
ExpectedAmountOfAwardTermsForCalculation: 0,
}, nil
case AwardTerm1:
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 6,
ExpectedAmountOfBasePeriodYearsForCalculation: 3,
ExpectedAmountOfOptionPeriodYearsForCalculation: 2,
ExpectedAmountOfAwardTermsForCalculation: 1,
}, nil
case AwardTerm2:
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 7,
ExpectedAmountOfBasePeriodYearsForCalculation: 3,
ExpectedAmountOfOptionPeriodYearsForCalculation: 2,
ExpectedAmountOfAwardTermsForCalculation: 2,
}, nil
case OptionPeriod3:
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 8,
ExpectedAmountOfBasePeriodYearsForCalculation: 3,
ExpectedAmountOfOptionPeriodYearsForCalculation: 3,
ExpectedAmountOfAwardTermsForCalculation: 2,
}, nil
}

err := fmt.Errorf("unexpected contract year %s", contractYearName)
return ExpectedEscalationPriceContractsCount{}, err
}
8 changes: 4 additions & 4 deletions pkg/services/ghcrateengine/domestic_origin_pricer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticOriginWithServiceItemPa
suite.Equal(expectedCost, cost)

expectedParams := services.PricingDisplayParams{
{Key: models.ServiceItemParamNameContractYearName, Value: "Test Contract Year"},
{Key: models.ServiceItemParamNameContractYearName, Value: "Base Period Year 1"},
{Key: models.ServiceItemParamNameEscalationCompounded, Value: "1.04070"},
{Key: models.ServiceItemParamNameIsPeak, Value: "true"},
{Key: models.ServiceItemParamNamePriceRateOrFactor, Value: "1.46"},
Expand Down Expand Up @@ -126,7 +126,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticOrigin() {
suite.Equal(expectedCost, cost)

expectedParams := services.PricingDisplayParams{
{Key: models.ServiceItemParamNameContractYearName, Value: "Test Contract Year"},
{Key: models.ServiceItemParamNameContractYearName, Value: "Base Period Year 1"},
{Key: models.ServiceItemParamNameEscalationCompounded, Value: "1.04070"},
{Key: models.ServiceItemParamNameIsPeak, Value: "true"},
{Key: models.ServiceItemParamNamePriceRateOrFactor, Value: "1.46"},
Expand All @@ -153,7 +153,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticOrigin() {
suite.Equal(expectedCost, cost)

expectedParams := services.PricingDisplayParams{
{Key: models.ServiceItemParamNameContractYearName, Value: "Test Contract Year"},
{Key: models.ServiceItemParamNameContractYearName, Value: "Base Period Year 1"},
{Key: models.ServiceItemParamNameEscalationCompounded, Value: "1.04070"},
{Key: models.ServiceItemParamNameIsPeak, Value: "false"},
{Key: models.ServiceItemParamNamePriceRateOrFactor, Value: "1.27"},
Expand Down Expand Up @@ -286,7 +286,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticOrigin() {
suite.Equal(basePriceCents/5, fifthPriceCents)

expectedParams := services.PricingDisplayParams{
{Key: models.ServiceItemParamNameContractYearName, Value: "Test Contract Year"},
{Key: models.ServiceItemParamNameContractYearName, Value: "Base Period Year 1"},
{Key: models.ServiceItemParamNameEscalationCompounded, Value: "1.04070"},
{Key: models.ServiceItemParamNameIsPeak, Value: "true"},
{Key: models.ServiceItemParamNamePriceRateOrFactor, Value: "1.46"},
Expand Down
88 changes: 87 additions & 1 deletion pkg/services/ghcrateengine/pricer_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package ghcrateengine
import (
"fmt"
"math"
"slices"
"strconv"
"time"

"github.com/gofrs/uuid"
Expand Down Expand Up @@ -493,14 +495,98 @@ func escalatePriceForContractYear(appCtx appcontext.AppContext, contractID uuid.
}

escalatedPrice = roundToPrecision(escalatedPrice, precision)
escalatedPrice = escalatedPrice * contractYear.EscalationCompounded

if slices.Contains(models.ContractYears, contractYear.Name) {
escalatedPrice, err = compoundEscalationFactors(appCtx, contractID, contractYear, escalatedPrice)
if err != nil {
return 0, contractYear, err
}
} else {
escalatedPrice = escalatedPrice * contractYear.EscalationCompounded
}

escalatedPrice = roundToPrecision(escalatedPrice, precision)
return escalatedPrice, contractYear, nil
}

func compoundEscalationFactors(appCtx appcontext.AppContext, contractID uuid.UUID, contractYear models.ReContractYear, escalatedPrice float64) (float64, error) {
// Get all contracts based on contract Id
contractYearsFromDB, err := fetchContractsByContractId(appCtx, contractID)
if err != nil {
return escalatedPrice, fmt.Errorf("could not lookup contracts by Id: %w", err)
}

// A contract may have Option Year 3 but it is not guaranteed. Need to know if it does or not
contractsYearsFromDBMap := make(map[string]models.ReContractYear)
for _, contract := range contractYearsFromDB {
// Add re_contract_years record to map
contractsYearsFromDBMap[contract.Name] = contract
}

// Get expectations for price escalations calculations
expectations, err := models.GetExpectedEscalationPriceContractsCount(contractYear.Name)
if err != nil {
return escalatedPrice, err
}

// Adding contracts that are expected to be in the calculations based on the contract year to a map
contractYearsForCalculation := make(map[string]models.ReContractYear)
if expectations.ExpectedAmountOfAwardTermsForCalculation > 0 {
contractYearsForCalculation, err = addContractsForEscalationCalculation(contractYearsForCalculation, contractsYearsFromDBMap, expectations.ExpectedAmountOfAwardTermsForCalculation, models.AwardTerm)
if err != nil {
return escalatedPrice, err
}
}
if expectations.ExpectedAmountOfOptionPeriodYearsForCalculation > 0 {
contractYearsForCalculation, err = addContractsForEscalationCalculation(contractYearsForCalculation, contractsYearsFromDBMap, expectations.ExpectedAmountOfOptionPeriodYearsForCalculation, models.OptionPeriod)
if err != nil {
return escalatedPrice, err
}
}
if expectations.ExpectedAmountOfBasePeriodYearsForCalculation > 0 {
contractYearsForCalculation, err = addContractsForEscalationCalculation(contractYearsForCalculation, contractsYearsFromDBMap, expectations.ExpectedAmountOfBasePeriodYearsForCalculation, models.BasePeriodYear)
if err != nil {
return escalatedPrice, err
}
}

// Make sure the expected amount of contracts are being used in the escalated Price calculation
if expectations.ExpectedAmountOfContractYearsForCalculation > 0 && len(contractYearsForCalculation) != expectations.ExpectedAmountOfContractYearsForCalculation {
err := apperror.NewInternalServerError("Unexpected amount of contract years being used in escalated price calculation")
return escalatedPrice, err
}

// Multiply the escalated price by each re_contract_years record escalation factor. EscalatedPrice = EscalatedPrice * ContractEscalationFactor
var compoundedEscalatedPrice = escalatedPrice

if expectations.ExpectedAmountOfContractYearsForCalculation > 0 {
for _, contract := range contractYearsForCalculation {
compoundedEscalatedPrice = compoundedEscalatedPrice * contract.Escalation
}
}

return compoundedEscalatedPrice, nil
}

// roundToPrecision rounds a float64 value to the number of decimal points indicated by the precision.
// TODO: Future cleanup could involve moving this function to a math/utility package with some simple tests
func roundToPrecision(value float64, precision int) float64 {
ratio := math.Pow(10, float64(precision))
return math.Round(value*ratio) / ratio
}

func addContractsForEscalationCalculation(contractsMap map[string]models.ReContractYear, contractsMapDB map[string]models.ReContractYear, contractsAmount int, contractName string) (map[string]models.ReContractYear, error) {
if contractsAmount > 0 {
for i := contractsAmount; i != 0; i-- {
name := fmt.Sprintf("%s %s", contractName, strconv.FormatInt(int64(i), 10))
// If a contract that is expected to be used in the calculations is not found then return error
if _, exist := contractsMapDB[name]; exist {
contractsMap[contractsMapDB[name].Name] = contractsMapDB[name]
} else {
err := fmt.Errorf("expected contract %s not found", name)
return contractsMap, err
}
}
}
return contractsMap, nil
}
Loading

0 comments on commit 6320f57

Please sign in to comment.