From 342611b55778c6ec1160c3396f717943312ae4a2 Mon Sep 17 00:00:00 2001 From: Jiefeng Xu Date: Sat, 26 Mar 2022 21:42:51 -0700 Subject: [PATCH] For issue #153 allow decimals for tax calculation --- calculator/calculator.go | 78 ++++++++++++------------ calculator/calculator_test.go | 108 +++++++++++++++++----------------- 2 files changed, 91 insertions(+), 95 deletions(-) diff --git a/calculator/calculator.go b/calculator/calculator.go index 4614cb6..6faf437 100644 --- a/calculator/calculator.go +++ b/calculator/calculator.go @@ -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 } @@ -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. @@ -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"` @@ -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. @@ -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 } @@ -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 @@ -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() @@ -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()) { @@ -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 } @@ -354,8 +354,4 @@ const ( bias = 1023 signMask = 1 << 63 fracMask = 1< 0 { return t.quantity } - return 1 + return 1.0 } type TestCoupon struct { @@ -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, }) }