Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow decimals for tax calculator #227

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 37 additions & 41 deletions calculator/calculator.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,30 @@ import (
// DiscountItem provides details about a discount that was applied
type DiscountItem struct {
Type DiscountType `json:"type"`
Percentage uint64 `json:"percentage"`
Fixed uint64 `json:"fixed"`
Percentage float64 `json:"percentage"`
Fixed float64 `json:"fixed"`
}

// Price represents the total price of all line items.
type Price struct {
Items []ItemPrice

Subtotal uint64
Discount uint64
NetTotal uint64
Taxes uint64
Total int64
Subtotal float64
Discount float64
NetTotal float64
Taxes float64
Total float64
}

// ItemPrice is the price of a single line item.
type ItemPrice struct {
Quantity uint64
Quantity float64

Subtotal uint64
Discount uint64
NetTotal uint64
Taxes uint64
Total int64
Subtotal float64
Discount float64
NetTotal float64
Taxes float64
Total float64

DiscountItems []DiscountItem
}
Expand Down Expand Up @@ -62,14 +62,14 @@ type Settings struct {

// Tax represents a tax, potentially specific to countries and product types.
type Tax struct {
Percentage uint64 `json:"percentage"`
Percentage float64 `json:"percentage"`
ProductTypes []string `json:"product_types"`
Countries []string `json:"countries"`
}

type taxAmount struct {
price uint64
percentage uint64
price float64
percentage float64
}

// FixedMemberDiscount represents a fixed discount given to members.
Expand All @@ -82,7 +82,7 @@ type FixedMemberDiscount struct {
// or a percentage.
type MemberDiscount struct {
Claims map[string]string `json:"claims"`
Percentage uint64 `json:"percentage"`
Percentage float64 `json:"percentage"`
FixedAmount []*FixedMemberDiscount `json:"fixed"`
ProductTypes []string `json:"product_types"`
Products []string `json:"products"`
Expand Down Expand Up @@ -125,34 +125,34 @@ func (d *MemberDiscount) ValidForProduct(productSku string) bool {
// Item is the interface for a single line item needed to do price calculation.
type Item interface {
ProductSku() string
PriceInLowestUnit() uint64
PriceInLowestUnit() float64
ProductType() string
FixedVAT() uint64
FixedVAT() float64
TaxableItems() []Item
GetQuantity() uint64
GetQuantity() float64
}

// Coupon is the interface for a coupon needed to do price calculation.
type Coupon interface {
ValidForType(string) bool
ValidForPrice(string, uint64) bool
ValidForPrice(string, float64) bool
ValidForProduct(string) bool
PercentageDiscount() uint64
FixedDiscount(string) uint64
PercentageDiscount() float64
FixedDiscount(string) float64
}

// FixedDiscount returns what the fixed discount amount is for a particular currency.
func (d *MemberDiscount) FixedDiscount(currency string) uint64 {
func (d *MemberDiscount) FixedDiscount(currency string) float64 {
if d.FixedAmount != nil {
for _, discount := range d.FixedAmount {
if discount.Currency == currency {
amount, _ := strconv.ParseFloat(discount.Amount, 64)
return rint(amount * 100)
return amount * 100
}
}
}

return 0
return 0.0
}

// AppliesTo determines if the tax applies to the country AND product type provided.
Expand Down Expand Up @@ -215,13 +215,13 @@ func calculateAmountsForSingleItem(settings *Settings, lineLogger logrus.FieldLo
}
}

discountedPrice := uint64(0)
discountedPrice := 0.0
if itemPrice.Discount < singlePrice {
discountedPrice = singlePrice - itemPrice.Discount
}

itemPrice.Taxes, itemPrice.NetTotal = calculateTaxes(discountedPrice, item, params, settings)
itemPrice.Total = int64(itemPrice.NetTotal + itemPrice.Taxes)
itemPrice.Total = itemPrice.NetTotal + itemPrice.Taxes

return itemPrice
}
Expand Down Expand Up @@ -280,10 +280,10 @@ func CalculatePrice(settings *Settings, jwtClaims map[string]interface{}, params
return price
}

func calculateDiscount(amountToDiscount, percentage, fixed uint64) uint64 {
var discount uint64
func calculateDiscount(amountToDiscount, percentage, fixed float64) float64 {
var discount float64
if percentage > 0 {
discount = rint(float64(amountToDiscount) * float64(percentage) / 100)
discount = amountToDiscount * percentage / 100
}
discount += fixed

Expand All @@ -293,7 +293,7 @@ func calculateDiscount(amountToDiscount, percentage, fixed uint64) uint64 {
return discount
}

func calculateTaxes(amountToTax uint64, item Item, params PriceParameters, settings *Settings) (taxes uint64, subtotal uint64) {
func calculateTaxes(amountToTax float64, item Item, params PriceParameters, settings *Settings) (taxes float64, subtotal float64) {
includeTaxes := settings != nil && settings.PricesIncludeTaxes
originalPrice := item.PriceInLowestUnit()

Expand All @@ -303,8 +303,8 @@ func calculateTaxes(amountToTax uint64, item Item, params PriceParameters, setti
} else if settings != nil && item.TaxableItems() != nil && len(item.TaxableItems()) > 0 {
for _, item := range item.TaxableItems() {
// because a discount may have been applied we need to determine the real price of this sub-item
priceShare := float64(item.PriceInLowestUnit()) / float64(originalPrice)
itemPrice := rint(float64(amountToTax) * priceShare)
priceShare := item.PriceInLowestUnit() / originalPrice
itemPrice := amountToTax * priceShare
amount := taxAmount{price: itemPrice}
for _, t := range settings.Taxes {
if t.AppliesTo(params.Country, item.ProductType()) {
Expand Down Expand Up @@ -332,11 +332,11 @@ func calculateTaxes(amountToTax uint64, item Item, params PriceParameters, setti
subtotal = 0
for _, tax := range taxAmounts {
if includeTaxes {
taxAmount := rint(float64(tax.price) / float64(100+tax.percentage) * 100 * (float64(tax.percentage) / 100))
taxAmount := tax.price / (100+tax.percentage) * 100 * tax.percentage / 100
tax.price -= taxAmount
taxes += taxAmount
} else {
taxes += rint(float64(tax.price) * float64(tax.percentage) / 100)
taxes += tax.price * tax.percentage / 100
}
subtotal += tax.price
}
Expand All @@ -354,8 +354,4 @@ const (
bias = 1023
signMask = 1 << 63
fracMask = 1<<shift - 1
)

func rint(x float64) uint64 {
return uint64(math.Round(x))
}
)
108 changes: 54 additions & 54 deletions calculator/calculator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,38 @@ var testLogger = logrus.NewEntry(logrus.StandardLogger())

type TestItem struct {
sku string
price uint64
price float64
itemType string
vat uint64
vat float64
items []Item
quantity uint64
quantity float64
}

func (t *TestItem) ProductSku() string {
return t.sku
}

func (t *TestItem) PriceInLowestUnit() uint64 {
func (t *TestItem) PriceInLowestUnit() float64 {
return t.price
}

func (t *TestItem) ProductType() string {
return t.itemType
}

func (t *TestItem) FixedVAT() uint64 {
func (t *TestItem) FixedVAT() float64 {
return t.vat
}

func (t *TestItem) TaxableItems() []Item {
return t.items
}

func (t *TestItem) GetQuantity() uint64 {
func (t *TestItem) GetQuantity() float64 {
if t.quantity > 0 {
return t.quantity
}
return 1
return 1.0
}

type TestCoupon struct {
Expand All @@ -65,111 +65,111 @@ func (c *TestCoupon) ValidForProduct(productSku string) bool {
return c.itemSku == productSku
}

func (c *TestCoupon) ValidForPrice(currency string, price uint64) bool {
func (c *TestCoupon) ValidForPrice(currency string, price float64) bool {
return c.moreThan == 0 || price > c.moreThan
}

func (c *TestCoupon) PercentageDiscount() uint64 {
func (c *TestCoupon) PercentageDiscount() float64 {
return c.percentage
}

func (c *TestCoupon) FixedDiscount(currency string) uint64 {
func (c *TestCoupon) FixedDiscount(currency string) float64 {
return c.fixed
}

func validatePrice(t *testing.T, actual Price, expected Price) {
assert.Equal(t, expected.Subtotal, actual.Subtotal, fmt.Sprintf("Expected subtotal to be %d, got %d", expected.Subtotal, actual.Subtotal))
assert.Equal(t, expected.Taxes, actual.Taxes, fmt.Sprintf("Expected taxes to be %d, got %d", expected.Taxes, actual.Taxes))
assert.Equal(t, expected.NetTotal, actual.NetTotal, fmt.Sprintf("Expected net total to be %d, got %d", expected.NetTotal, actual.NetTotal))
assert.Equal(t, expected.Discount, actual.Discount, fmt.Sprintf("Expected discount to be %d, got %d", expected.Discount, actual.Discount))
assert.Equal(t, expected.Total, actual.Total, fmt.Sprintf("Expected total to be %d, got %d", expected.Total, actual.Total))
assert.Equal(t, int64(expected.NetTotal+expected.Taxes), expected.Total, "Your expected nettotal and taxes should add up to the expected total. Check your test!")
assert.Equal(t, int64(actual.NetTotal+actual.Taxes), actual.Total, "Expected nettotal and taxes to add up to total")
assert.Equal(t, expected.Subtotal, actual.Subtotal, fmt.Sprintf("Expected subtotal to be %f, got %f", expected.Subtotal, actual.Subtotal))
assert.Equal(t, expected.Taxes, actual.Taxes, fmt.Sprintf("Expected taxes to be %f, got %f", expected.Taxes, actual.Taxes))
assert.Equal(t, expected.NetTotal, actual.NetTotal, fmt.Sprintf("Expected net total to be %f, got %f", expected.NetTotal, actual.NetTotal))
assert.Equal(t, expected.Discount, actual.Discount, fmt.Sprintf("Expected discount to be %f, got %f", expected.Discount, actual.Discount))
assert.Equal(t, expected.Total, actual.Total, fmt.Sprintf("Expected total to be %f, got %f", expected.Total, actual.Total))
assert.Equal(t, float64(expected.NetTotal+expected.Taxes), expected.Total, "Your expected nettotal and taxes should add up to the expected total. Check your test!")
assert.Equal(t, float64(actual.NetTotal+actual.Taxes), actual.Total, "Expected nettotal and taxes to add up to total")
}

func TestNoItems(t *testing.T) {
params := PriceParameters{"USA", "USD", nil, nil}
price := CalculatePrice(nil, nil, params, testLogger)
validatePrice(t, price, Price{
Subtotal: 0,
Discount: 0,
NetTotal: 0,
Taxes: 0,
Total: 0,
Subtotal: 0.0,
Discount: 0.0,
NetTotal: 0.0,
Taxes: 0.0,
Total: 0.0,
})
}

func TestNoTaxes(t *testing.T) {
params := PriceParameters{"USA", "USD", nil, []Item{&TestItem{price: 100, itemType: "test"}}}
params := PriceParameters{"USA", "USD", nil, []Item{&TestItem{price: 100.0, itemType: "test"}}}
price := CalculatePrice(nil, nil, params, testLogger)

validatePrice(t, price, Price{
Subtotal: 100,
Discount: 0,
NetTotal: 100,
Taxes: 0,
Total: 100,
Subtotal: 100.0,
Discount: 0.0,
NetTotal: 100.0,
Taxes: 0.0,
Total: 100.0,
})
}

func TestFixedVAT(t *testing.T) {
params := PriceParameters{"USA", "USD", nil, []Item{&TestItem{price: 100, itemType: "test", vat: 9}}}
params := PriceParameters{"USA", "USD", nil, []Item{&TestItem{price: 100.0, itemType: "test", vat: 9}}}
price := CalculatePrice(nil, nil, params, testLogger)

validatePrice(t, price, Price{
Subtotal: 100,
Discount: 0,
NetTotal: 100,
Taxes: 9,
Total: 109,
Subtotal: 100.0,
Discount: 0.0,
NetTotal: 100.0,
Taxes: 9.0,
Total: 109.0,
})
}

func TestFixedVATWhenPricesIncludeTaxes(t *testing.T) {
params := PriceParameters{"USA", "USD", nil, []Item{&TestItem{price: 100, itemType: "test", vat: 9}}}
params := PriceParameters{"USA", "USD", nil, []Item{&TestItem{price: 100.0, itemType: "test", vat: 9}}}
price := CalculatePrice(&Settings{PricesIncludeTaxes: true}, nil, params, testLogger)

validatePrice(t, price, Price{
Subtotal: 92,
Discount: 0,
NetTotal: 92,
Taxes: 8,
Total: 100,
Subtotal: 92.0,
Discount: 0.0,
NetTotal: 92.0,
Taxes: 8.0,
Total: 100.0,
})
}

func TestCountryBasedVAT(t *testing.T) {
settings := &Settings{
Taxes: []*Tax{&Tax{
Percentage: 21,
Percentage: 21.0,
ProductTypes: []string{"test"},
Countries: []string{"USA"},
}},
}

params := PriceParameters{"USA", "USD", nil, []Item{&TestItem{price: 100, itemType: "test"}}}
params := PriceParameters{"USA", "USD", nil, []Item{&TestItem{price: 100.0, itemType: "test"}}}
price := CalculatePrice(settings, nil, params, testLogger)

validatePrice(t, price, Price{
Subtotal: 100,
Discount: 0,
NetTotal: 100,
Taxes: 21,
Total: 121,
Subtotal: 100.0,
Discount: 0.0,
NetTotal: 100.0,
Taxes: 21.0,
Total: 121.0,
})
}

func TestCouponWithNoTaxes(t *testing.T) {
coupon := &TestCoupon{itemType: "test", percentage: 10}
params := PriceParameters{"USA", "USD", coupon, []Item{&TestItem{price: 100, itemType: "test"}}}
coupon := &TestCoupon{itemType: "test", percentage: 10.0}
params := PriceParameters{"USA", "USD", coupon, []Item{&TestItem{price: 100.0, itemType: "test"}}}
price := CalculatePrice(nil, nil, params, testLogger)

validatePrice(t, price, Price{
Subtotal: 100,
Discount: 10,
NetTotal: 90,
Taxes: 0,
Total: 90,
Subtotal: 100.0,
Discount: 10.0,
NetTotal: 90.0,
Taxes: 0.0,
Total: 90.0,
})
}

Expand Down