Skip to content

Commit

Permalink
Adding initial implementation and test fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
TevinAdams committed Oct 29, 2024
1 parent d2b1660 commit e280844
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 8 deletions.
105 changes: 105 additions & 0 deletions pkg/models/re_contract_year.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
package models

import (
"fmt"
"time"

"github.com/gobuffalo/pop/v6"
"github.com/gobuffalo/validate/v3"
"github.com/gobuffalo/validate/v3/validators"
"github.com/gofrs/uuid"
"github.com/transcom/mymove/pkg/apperror"
)

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
}

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

func GetExpectedEscalationPriceContractsCount(contractYearName string, hasOptionYear3 bool) (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 OptionPeriod3:
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 6,
ExpectedAmountOfBasePeriodYearsForCalculation: 3,
ExpectedAmountOfOptionPeriodYearsForCalculation: 3,
ExpectedAmountOfAwardTermsForCalculation: 0,
}, nil
case AwardTerm1:
if hasOptionYear3 {
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 7,
ExpectedAmountOfBasePeriodYearsForCalculation: 3,
ExpectedAmountOfOptionPeriodYearsForCalculation: 3,
ExpectedAmountOfAwardTermsForCalculation: 1,
}, nil
} else {
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 6,
ExpectedAmountOfBasePeriodYearsForCalculation: 3,
ExpectedAmountOfOptionPeriodYearsForCalculation: 2,
ExpectedAmountOfAwardTermsForCalculation: 1,
}, nil
}
case AwardTerm2:
if hasOptionYear3 {
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 8,
ExpectedAmountOfBasePeriodYearsForCalculation: 3,
ExpectedAmountOfOptionPeriodYearsForCalculation: 3,
ExpectedAmountOfAwardTermsForCalculation: 2,
}, nil
} else {
return ExpectedEscalationPriceContractsCount{
ExpectedAmountOfContractYearsForCalculation: 7,
ExpectedAmountOfBasePeriodYearsForCalculation: 3,
ExpectedAmountOfOptionPeriodYearsForCalculation: 2,
ExpectedAmountOfAwardTermsForCalculation: 2,
}, nil
}
default:
err := apperror.NewInternalServerError(fmt.Sprintf("Unexpected contract year name %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
90 changes: 89 additions & 1 deletion pkg/services/ghcrateengine/pricer_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ghcrateengine
import (
"fmt"
"math"
"strconv"
"time"

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

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

if contractYear.Name == models.BasePeriodYear1 {
escalatedPrice = escalatedPrice * contractYear.EscalationCompounded
} else {
escalatedPrice, err = compoundEscalationFactors(appCtx, contractID, contractYear, escalatedPrice)
return 0, contractYear, err
}

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)
hasOptionYear3 := false
for _, contract := range contractYearsFromDB {
if contract.Name == models.OptionPeriod3 {
hasOptionYear3 = true
}
// Add re_contract_years record to map
contractsYearsFromDBMap[contract.Name] = contract
}

// Get expectations for price escalations calculations
expectations, err := models.GetExpectedEscalationPriceContractsCount(contractYear.Name, hasOptionYear3)
if err != nil {
err := apperror.NewInternalServerError(fmt.Sprintf("Error getting expectations for escalated price calculations", err))
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 {
err := apperror.NewInternalServerError(fmt.Sprintf("Error collecting expected Award Term contracts for escalated price calculations", err))
return escalatedPrice, err
}
}
if expectations.ExpectedAmountOfOptionPeriodYearsForCalculation > 0 {
contractYearsForCalculation, err = addContractsForEscalationCalculation(contractYearsForCalculation, contractsYearsFromDBMap, expectations.ExpectedAmountOfOptionPeriodYearsForCalculation, models.OptionPeriod)
if err != nil {
err := apperror.NewInternalServerError(fmt.Sprintf("Error collecting expected Option Period contracts for escalated price calculations", err))
return escalatedPrice, err
}
}
if expectations.ExpectedAmountOfBasePeriodYearsForCalculation > 0 {
contractYearsForCalculation, err = addContractsForEscalationCalculation(contractYearsForCalculation, contractsYearsFromDBMap, expectations.ExpectedAmountOfBasePeriodYearsForCalculation, models.BasePeriodYear)
if err != nil {
err := apperror.NewInternalServerError(fmt.Sprintf("Error collecting expected Base Period Year contracts for escalated price calculations", err))
return escalatedPrice, err
}
}

// Make sure the expected amount of contracts are being used in the escalated Price calculation
if 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
for _, contract := range contractYearsForCalculation {
compoundedEscalatedPrice = compoundedEscalatedPrice * contract.Escalation
}

return escalatedPrice, 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 := apperror.NewInternalServerError(fmt.Sprintf("Expected %s contract not found", name))
return contractsMap, err
}
}
}
return contractsMap, nil
}
10 changes: 10 additions & 0 deletions pkg/services/ghcrateengine/pricer_query_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ func fetchContractYear(appCtx appcontext.AppContext, contractID uuid.UUID, date
return contractYear, nil
}

func fetchContractsByContractId(appCtx appcontext.AppContext, contractID uuid.UUID) (models.ReContractYears, error) {
var contracts models.ReContractYears
err := appCtx.DB().Where("contract_id = $1", contractID).All(&contracts)
if err != nil {
return models.ReContractYears{}, err
}

return contracts, nil
}

func fetchShipmentTypePrice(appCtx appcontext.AppContext, contractCode string, serviceCode models.ReServiceCode, market models.Market) (models.ReShipmentTypePrice, error) {
var shipmentTypePrice models.ReShipmentTypePrice
err := appCtx.DB().Q().
Expand Down
4 changes: 2 additions & 2 deletions pkg/services/ghcrateengine/pricer_query_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ func (suite *GHCRateEngineServiceSuite) Test_fetchDomServiceAreaPrice() {
testCents := unit.Cents(353)

suite.Run("golden path", func() {
suite.setupDomesticServiceAreaPrice(models.ReServiceCodeDOFSIT, testServiceArea, testIsPeakPeriod, testCents, "Test Contract Year", 1.125)
suite.setupDomesticServiceAreaPrice(models.ReServiceCodeDOFSIT, testServiceArea, testIsPeakPeriod, testCents, "Base Period Year 1", 1.125)

domServiceAreaPrice, err := fetchDomServiceAreaPrice(suite.AppContextForTest(), testdatagen.DefaultContractCode, models.ReServiceCodeDOFSIT, testServiceArea, testIsPeakPeriod)
suite.NoError(err)
suite.Equal(testCents, domServiceAreaPrice.PriceCents)
})

suite.Run("no records found", func() {
suite.setupDomesticServiceAreaPrice(models.ReServiceCodeDOFSIT, testServiceArea, testIsPeakPeriod, testCents, "Test Contract Year", 1.125)
suite.setupDomesticServiceAreaPrice(models.ReServiceCodeDOFSIT, testServiceArea, testIsPeakPeriod, testCents, "Base Period Year 1", 1.125)

// Look for service code DDFSIT that we haven't added
_, err := fetchDomServiceAreaPrice(suite.AppContextForTest(), testdatagen.DefaultContractCode, models.ReServiceCodeDDFSIT, testServiceArea, testIsPeakPeriod)
Expand Down
2 changes: 1 addition & 1 deletion pkg/testdatagen/make_re_contract_year.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func MakeReContractYear(db *pop.Connection, assertions Assertions) models.ReCont

reContractYear := models.ReContractYear{
ContractID: reContract.ID,
Name: "Test Contract Year",
Name: "Base Period Year 1",
StartDate: time.Date(TestYear, time.January, 1, 0, 0, 0, 0, time.UTC),
EndDate: time.Date(TestYear, time.December, 31, 0, 0, 0, 0, time.UTC),
Escalation: 1.0197,
Expand Down

0 comments on commit e280844

Please sign in to comment.