-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathprice.go
130 lines (107 loc) · 3.57 KB
/
price.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package price
import (
"fmt"
"strings"
"github.com/pariz/gountries"
"github.com/lonnblad/shipment-service-backend/businesslogic/models"
)
// Calculate will return a price or an error if it didn't succeed in
// calculating a price.
//
// Note. the price is returned as an integer representing the real value,
// as the base prices are all multiplies of 10 and the specified region
// multiplier only use one decimal, this is fine since the result will
// always be an integer.
//
// In a real world scenario, where there might be a need for prices with
// decimals, the returned integer should compensate so that a value like
// 100.05 is returned as 10005, with a property stating the value 100 as
// the decimal multipler.
//
// This article talks about why floating points shouldn't be used for currency.
// https://husobee.github.io/money/float/2016/09/23/never-use-floats-for-currency.html
func Calculate(s models.Shipment) (_ int, err error) {
basePrice, err := findBasePrice(s.Package.Weight)
if err != nil {
return
}
multiplier, err := findRegionMultiplier(s.Sender.CountryCode)
if err != nil {
return
}
return basePrice * multiplier / regionMulitplierAdjustment, nil
}
type (
// WeightError will be returned by Calculate when there isn't
// a defined weight class for the provided weight.
WeightClassError struct{ Weight int }
// CountryCodeError will be returned by Calculate when there
// isn't a defined a country for the provided country code.
CountryCodeError struct{ CountryCode string }
)
func (we WeightClassError) Error() string {
return fmt.Sprintf("weight: %d don't have a defined price", we.Weight)
}
func (cce CountryCodeError) Error() string {
return fmt.Sprintf("countryCode: %s is not defined", cce.CountryCode)
}
const (
// - Small (0 - 10kg): 100sek
basePriceSmall = 100
weightLowerBoundSmall = 0
weightUpperBoundSmall = 10
// - Medium (10 - 25kg): 300sek
basePriceMedium = 300
weightUpperBoundMedium = 25
// - Large (26 - 50kg): 500sek
basePriceLarge = 500
weightUpperBoundLarge = 50
// - Huge (51 - 1000kg): 2000sek
basePriceHuge = 2000
weightUpperBoundHuge = 1000
// All the region multipliers are multiplied by 10
// to remove the need of using a floating pointer.
// - Nordic region (Sweden, Norway, Denmark, Finland), the price is multiplied by 1
// - EU region, the price is multiplied by 1.5
// - Outside the EU, the price is multiplied by 2.5
regionMulitplierAdjustment = 10
regionMultiplierNordic = 10
regionMultiplierEU = 15
regionMultiplierNonEU = 25
)
func findBasePrice(weight int) (_ int, err error) {
var price int
switch {
case weightLowerBoundSmall <= weight && weight <= weightUpperBoundSmall:
price = basePriceSmall
case weightUpperBoundSmall < weight && weight <= weightUpperBoundMedium:
price = basePriceMedium
case weightUpperBoundMedium < weight && weight <= weightUpperBoundLarge:
price = basePriceLarge
case weightUpperBoundLarge < weight && weight <= weightUpperBoundHuge:
price = basePriceHuge
default:
err = WeightClassError{Weight: weight}
return
}
return price, nil
}
var countries = gountries.New()
func findRegionMultiplier(countryCode string) (_ int, err error) {
country, err := countries.FindCountryByAlpha(countryCode)
if err != nil {
err = CountryCodeError{CountryCode: countryCode}
return
}
// Nordic Region
switch strings.ToLower(country.Alpha2) {
case "se", "no", "dk", "fi":
return regionMultiplierNordic, nil
}
// EU Region
if country.EuMember {
return regionMultiplierEU, nil
}
// Outside the EU
return regionMultiplierNonEU, nil
}