Skip to content

Commit eed49b6

Browse files
authored
Merge pull request #1377 from threefoldtech/development_fix_calculator
Calculator: fix get cost function
2 parents 1f4d661 + c0c3c2d commit eed49b6

File tree

3 files changed

+207
-79
lines changed

3 files changed

+207
-79
lines changed

grid-client/calculator/calculate.go

Lines changed: 71 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@ package calculator
33
import (
44
"math"
55

6+
"github.com/pkg/errors"
67
substrate "github.com/threefoldtech/tfchain/clients/tfchain-client-go"
78
"github.com/threefoldtech/tfgrid-sdk-go/grid-client/subi"
89
)
910

1011
const defaultPricingPolicyID = uint32(1)
1112

13+
// The price of TFT stored on the TFChain is expressed in mUSD per 1 TFT.
14+
// To convert this to USD, the formula is:
15+
// tft_price_units_usd = tft_price / 1000
16+
const mUSDToUSD = 1000
17+
18+
// UnitFactor represents the smallest unit conversion factor for both USD and TFT
19+
// 1 USD = 10,000,000 unit-USD
20+
// 1 TFT = 10,000,000 unit-TFT (TFT's Planck)
21+
const UnitFactor = 1e7
22+
1223
// Calculator struct for calculating the cost of resources
1324
type Calculator struct {
1425
substrateConn subi.SubstrateExt
@@ -22,10 +33,6 @@ func NewCalculator(substrateConn subi.SubstrateExt, identity substrate.Identity)
2233

2334
// CalculateCost calculates the cost in $ per month of the given resources without a discount
2435
func (c *Calculator) CalculateCost(cru, mru, hru, sru int64, publicIP, certified bool) (float64, error) {
25-
tftPrice, err := c.substrateConn.GetTFTPrice()
26-
if err != nil {
27-
return 0, err
28-
}
2936

3037
pricingPolicy, err := c.substrateConn.GetPricingPolicy(defaultPricingPolicyID)
3138
if err != nil {
@@ -44,97 +51,103 @@ func (c *Calculator) CalculateCost(cru, mru, hru, sru int64, publicIP, certified
4451
if certified {
4552
certifiedFactor = 1.25
4653
}
47-
54+
// cost per month in unit-USD
4855
costPerMonth := (cu*float64(pricingPolicy.CU.Value) + su*float64(pricingPolicy.SU.Value) + ipv4*float64(pricingPolicy.IPU.Value)) * certifiedFactor * 24 * 30
49-
return costPerMonth / float64(tftPrice) / 1000, nil
56+
// convert to USD
57+
return costPerMonth / UnitFactor, nil
5058
}
5159

52-
// CalculateDiscount calculates the discount of a given cost
53-
func (c *Calculator) CalculateDiscount(cost float64) (dedicatedPrice, sharedPrice float64, err error) {
54-
tftPrice, err := c.substrateConn.GetTFTPrice()
55-
if err != nil {
56-
return
57-
}
58-
60+
// CalculatePricesAfterDiscount calculates the prices after discount
61+
func (c *Calculator) CalculatePricesAfterDiscount(cost float64) (dedicatedPrice, sharedPrice float64, err error) {
5962
pricingPolicy, err := c.substrateConn.GetPricingPolicy(defaultPricingPolicyID)
6063
if err != nil {
6164
return
6265
}
6366

64-
// discount for shared Nodes
6567
sharedPrice = cost
66-
67-
// discount for Dedicated Nodes
6868
discount := float64(pricingPolicy.DedicatedNodesDiscount)
6969
dedicatedPrice = cost - cost*(discount/100)
7070

71-
// discount for Twin Balance in TFT
7271
accountBalance, err := c.substrateConn.GetBalance(c.identity)
7372
if err != nil {
7473
return
7574
}
76-
balance := float64(tftPrice) / 1000 * float64(accountBalance.Free.Int64()) * 10000000
77-
78-
discountPackages := map[string]map[string]float64{
79-
"none": {
80-
"duration": 0,
81-
"discount": 0,
82-
},
83-
"default": {
84-
"duration": 1.5,
85-
"discount": 20,
86-
},
87-
"bronze": {
88-
"duration": 3,
89-
"discount": 30,
90-
},
91-
"silver": {
92-
"duration": 6,
93-
"discount": 40,
94-
},
95-
"gold": {
96-
"duration": 18,
97-
"discount": 60,
98-
},
75+
76+
balanceTFT := float64(accountBalance.Free.Int64()) / UnitFactor
77+
78+
balanceUSD, err := c.TFTtoUSD(balanceTFT)
79+
if err != nil {
80+
return
9981
}
10082

101-
// check which package will be used according to the balance
102-
dedicatedPackage := "none"
103-
sharedPackage := "none"
104-
for pkg := range discountPackages {
105-
if balance > dedicatedPrice*discountPackages[pkg]["duration"] {
106-
dedicatedPackage = pkg
83+
sharedDiscount, dedicatedDiscount := getApplicableDiscount(balanceUSD, dedicatedPrice, sharedPrice)
84+
85+
dedicatedPrice = dedicatedPrice - dedicatedPrice*dedicatedDiscount
86+
sharedPrice = sharedPrice - sharedPrice*sharedDiscount
87+
88+
return
89+
}
90+
91+
func getApplicableDiscount(balance float64, dedicatedPrice float64, sharedPrice float64) (bestSharedDiscount, bestDedicatedDiscount float64) {
92+
packages := []struct {
93+
name string
94+
duration float64
95+
discount float64
96+
}{
97+
{name: "none", duration: 0, discount: 0},
98+
{name: "default", duration: 1.5, discount: 20},
99+
{name: "bronze", duration: 3, discount: 30},
100+
{name: "silver", duration: 6, discount: 40},
101+
{name: "gold", duration: 18, discount: 60},
102+
}
103+
104+
var bestSharedDiscountValue, bestDedicatedDiscountValue float64 = 0, 0
105+
106+
for _, pkg := range packages {
107+
sharedThreshold := sharedPrice * pkg.duration
108+
dedicatedThreshold := dedicatedPrice * pkg.duration
109+
110+
if balance > sharedThreshold {
111+
bestSharedDiscountValue = pkg.discount
107112
}
108-
if balance > sharedPrice*discountPackages[pkg]["duration"] {
109-
sharedPackage = pkg
113+
114+
if balance > dedicatedThreshold {
115+
bestDedicatedDiscountValue = pkg.discount
110116
}
111117
}
112118

113-
dedicatedPrice = (dedicatedPrice - dedicatedPrice*(discountPackages[dedicatedPackage]["discount"]/100)) / 1e7
114-
sharedPrice = (sharedPrice - sharedPrice*(discountPackages[sharedPackage]["discount"]/100)) / 1e7
115-
116-
return
119+
return bestSharedDiscountValue / 100, bestDedicatedDiscountValue / 100
117120
}
118121

119122
func calculateSU(hru, sru int64) float64 {
120-
return float64(hru/1200 + sru/200)
123+
return float64(hru)/1200 + float64(sru)/200
121124
}
122125

123126
func calculateCU(cru, mru int64) float64 {
124-
MruUsed1 := float64(mru / 4)
125-
CruUsed1 := float64(cru / 2)
127+
128+
MruUsed1 := float64(mru) / 4
129+
CruUsed1 := float64(cru) / 2
126130
cu1 := math.Max(MruUsed1, CruUsed1)
127131

128-
MruUsed2 := float64(mru / 8)
132+
MruUsed2 := float64(mru) / 8
129133
CruUsed2 := float64(cru)
130134
cu2 := math.Max(MruUsed2, CruUsed2)
131135

132-
MruUsed3 := float64(mru / 2)
133-
CruUsed3 := float64(cru / 4)
136+
MruUsed3 := float64(mru) / 2
137+
CruUsed3 := float64(cru) / 4
134138
cu3 := math.Max(MruUsed3, CruUsed3)
135139

136140
cu := math.Min(cu1, cu2)
137141
cu = math.Min(cu, cu3)
138142

139143
return cu
140144
}
145+
146+
// TFTtoUSD converts TFT amount to USD based on the current price
147+
func (c *Calculator) TFTtoUSD(tft float64) (float64, error) {
148+
tftPrice, err := c.substrateConn.GetTFTPrice()
149+
if err != nil {
150+
return 0, errors.Wrap(err, "failed to get TFT price")
151+
}
152+
return tft * (float64(tftPrice) / mUSDToUSD), nil
153+
}

grid-client/calculator/calculate_test.go

Lines changed: 135 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,31 @@ func TestCalculator(t *testing.T) {
2222

2323
calculator := NewCalculator(sub, identity)
2424

25-
sub.EXPECT().GetTFTPrice().Return(types.U32(1), nil).AnyTimes()
25+
sub.EXPECT().GetTFTPrice().Return(types.U32(5), nil).AnyTimes()
2626
sub.EXPECT().GetPricingPolicy(1).Return(substrate.PricingPolicy{
2727
ID: 1,
2828
SU: substrate.Policy{
29-
Value: 2,
29+
Value: 50000,
3030
},
3131
CU: substrate.Policy{
32-
Value: 2,
32+
Value: 100000,
3333
},
3434
IPU: substrate.Policy{
35-
Value: 2,
35+
Value: 40000,
3636
},
3737
}, nil).AnyTimes()
3838

3939
cost, err := calculator.CalculateCost(8, 32, 0, 50, true, true)
4040
assert.NoError(t, err)
41-
assert.Equal(t, cost, 16.2)
41+
assert.Equal(t, 76.725, cost)
4242

4343
sub.EXPECT().GetBalance(identity).Return(substrate.Balance{
4444
Free: types.U128{
4545
Int: big.NewInt(50000000),
4646
},
4747
}, nil)
4848

49-
dedicatedPrice, sharedPrice, err := calculator.CalculateDiscount(cost)
49+
dedicatedPrice, sharedPrice, err := calculator.CalculatePricesAfterDiscount(cost)
5050
assert.NoError(t, err)
5151
assert.Equal(t, dedicatedPrice, sharedPrice)
5252
}
@@ -61,33 +61,148 @@ func TestSubstrateErrors(t *testing.T) {
6161

6262
calculator := NewCalculator(sub, identity)
6363

64-
t.Run("test tft price error", func(t *testing.T) {
65-
sub.EXPECT().GetTFTPrice().Return(types.U32(1), errors.New("error")).AnyTimes()
66-
67-
_, err := calculator.CalculateCost(0, 0, 0, 0, false, false)
68-
assert.Error(t, err)
69-
70-
_, _, err = calculator.CalculateDiscount(200)
71-
assert.Error(t, err)
72-
})
73-
7464
t.Run("test tft pricing policy error", func(t *testing.T) {
75-
sub.EXPECT().GetTFTPrice().Return(types.U32(1), nil).AnyTimes()
7665
sub.EXPECT().GetPricingPolicy(1).Return(substrate.PricingPolicy{}, errors.New("error")).AnyTimes()
7766

7867
_, err := calculator.CalculateCost(0, 0, 0, 0, false, false)
7968
assert.Error(t, err)
8069

81-
_, _, err = calculator.CalculateDiscount(200)
70+
_, _, err = calculator.CalculatePricesAfterDiscount(200)
8271
assert.Error(t, err)
8372
})
8473

8574
t.Run("test tft balance error", func(t *testing.T) {
86-
sub.EXPECT().GetTFTPrice().Return(types.U32(1), nil).AnyTimes()
8775
sub.EXPECT().GetPricingPolicy(1).Return(substrate.PricingPolicy{}, nil).AnyTimes()
8876
sub.EXPECT().GetBalance(identity).Return(substrate.Balance{}, errors.New("error")).AnyTimes()
8977

90-
_, _, err = calculator.CalculateDiscount(0)
78+
_, _, err = calculator.CalculatePricesAfterDiscount(0)
79+
assert.Error(t, err)
80+
})
81+
}
82+
83+
func TestGetApplicableDiscount(t *testing.T) {
84+
testCases := []struct {
85+
name string
86+
balance float64
87+
dedicatedPrice float64
88+
sharedPrice float64
89+
expectedDedicatedDiscount float64
90+
expectedSharedDiscount float64
91+
}{
92+
{
93+
name: "No balance",
94+
balance: 0,
95+
dedicatedPrice: 100,
96+
sharedPrice: 80,
97+
expectedDedicatedDiscount: 0,
98+
expectedSharedDiscount: 0,
99+
},
100+
{
101+
name: "Insufficient balance for any package",
102+
balance: 50,
103+
dedicatedPrice: 100,
104+
sharedPrice: 80,
105+
expectedDedicatedDiscount: 0,
106+
expectedSharedDiscount: 0,
107+
},
108+
{
109+
name: "Balance enough for default package only for shared",
110+
balance: 130, // > 80 * 1.5 but < 100 * 1.5
111+
dedicatedPrice: 100,
112+
sharedPrice: 80,
113+
expectedDedicatedDiscount: 0,
114+
expectedSharedDiscount: 0.2, // Default package discount 20%
115+
},
116+
{
117+
name: "Balance enough for default package for both",
118+
balance: 160, // > 100 * 1.5 and > 80 * 1.5
119+
dedicatedPrice: 100,
120+
sharedPrice: 80,
121+
expectedDedicatedDiscount: 0.2, // Default package discount 20%
122+
expectedSharedDiscount: 0.2, // Default package discount 20%
123+
},
124+
{
125+
name: "Balance enough for bronze package for shared, default for dedicated",
126+
balance: 250, // > 80 * 3 and < 100 * 1.5
127+
dedicatedPrice: 100,
128+
sharedPrice: 80,
129+
expectedDedicatedDiscount: 0.2, // Default Package discount 20%
130+
expectedSharedDiscount: 0.3, // Bronze package discount 30%
131+
},
132+
{
133+
name: "Balance enough for bronze package for both",
134+
balance: 350, // > 100 * 3 and > 80 * 3
135+
dedicatedPrice: 100,
136+
sharedPrice: 80,
137+
expectedDedicatedDiscount: 0.3, // Bronze package discount 30%
138+
expectedSharedDiscount: 0.3, // Bronze package discount 30%
139+
},
140+
{
141+
name: "Balance enough for silver package for shared, and bronze for dedicated",
142+
balance: 500, // > 80 * 6 but < 100 * 6
143+
dedicatedPrice: 100,
144+
sharedPrice: 80,
145+
expectedDedicatedDiscount: 0.3, // Bronze package discount 30%
146+
expectedSharedDiscount: 0.4, // Silver package discount 40%
147+
},
148+
{
149+
name: "Balance enough for silver package for both",
150+
balance: 650, // > 100 * 6 and > 80 * 6
151+
dedicatedPrice: 100,
152+
sharedPrice: 80,
153+
expectedDedicatedDiscount: 0.4,
154+
expectedSharedDiscount: 0.4,
155+
},
156+
{
157+
name: "Balance enough for gold package for shared, and Silver for dedicated",
158+
balance: 1500, // > 80 * 18 but < 100 * 18
159+
dedicatedPrice: 100,
160+
sharedPrice: 80,
161+
expectedDedicatedDiscount: 0.4, // Silver package discount 40%
162+
expectedSharedDiscount: 0.6, // Gold package discount 60%
163+
},
164+
{
165+
name: "Balance enough for gold package for both",
166+
balance: 2000, // > 100 * 18 and > 80 * 18
167+
dedicatedPrice: 100,
168+
sharedPrice: 80,
169+
expectedDedicatedDiscount: 0.6,
170+
expectedSharedDiscount: 0.6,
171+
},
172+
}
173+
174+
for _, tc := range testCases {
175+
t.Run(tc.name, func(t *testing.T) {
176+
sharedDiscount, dedicatedDiscount := getApplicableDiscount(tc.balance, tc.dedicatedPrice, tc.sharedPrice)
177+
178+
assert.Equal(t, tc.expectedDedicatedDiscount, dedicatedDiscount, "Dedicated discount percentage mismatch")
179+
assert.Equal(t, tc.expectedSharedDiscount, sharedDiscount, "Shared discount percentage mismatch")
180+
})
181+
}
182+
}
183+
184+
func TestTFTtoUSD(t *testing.T) {
185+
ctrl := gomock.NewController(t)
186+
defer ctrl.Finish()
187+
188+
sub := mocks.NewMockSubstrateExt(ctrl)
189+
identity, err := substrate.NewIdentityFromSr25519Phrase("//Alice")
190+
assert.NoError(t, err)
191+
192+
calculator := NewCalculator(sub, identity)
193+
194+
t.Run("success case", func(t *testing.T) {
195+
sub.EXPECT().GetTFTPrice().Return(types.U32(5), nil)
196+
197+
result, err := calculator.TFTtoUSD(10)
198+
assert.NoError(t, err)
199+
assert.Equal(t, 0.05, result)
200+
})
201+
202+
t.Run("error case", func(t *testing.T) {
203+
sub.EXPECT().GetTFTPrice().Return(types.U32(0), errors.New("failed to get TFT price"))
204+
205+
_, err := calculator.TFTtoUSD(100)
91206
assert.Error(t, err)
92207
})
93208
}

0 commit comments

Comments
 (0)