DEV Community

Cover image for What a number
JingIsCoding
JingIsCoding

Posted on

What a number

Here at chowbus.com, we are building Point of Sales solution for restaurant, one of the most essential part of the application logic to accurately calculate subtotal, tax, discounts, fees and totals for an order, While this might seem like a straightforward task initially, it entails some nuances.

As a standard practice, we employ big integers to represent monetary amounts. This helps us avoid floating-point precision errors that can occur during addition, subtraction, and multiplication between integers. Concretely, $100.00 is represented as the integer 10000 in our system.

Discount in our user case comes with either fixed amount or percentage form, and they can be applied on either order or dishes, Adding complexity, we must distribute the order discount proportionally among each dish when customers opt to pay separately for different items. The allocation of the discount respects the subtotal of each dish.

Consider the scenario where there is an order discount of $1.00 with two dishes priced at $4 and $6. In this case, dish 1 would carry a discount of $0.4, and dish 2 would carry a discount of $0.6. Handling cases where the discount is not evenly divisible by the dish subtotals requires us to distribute the remainders as evenly as possible. For instance, if the discount is $1 with three dishes priced at $3 each, each dish would carry a discount of $0.33, with a remainder of $0.01. This remainder is allocated to one of the dishes.

To demonstrate the point, let’s look at some code.

type DiscountType string

const (
 FixedAmount DiscountType = "fixed_amount"
 Percentage  DiscountType = "percentage"
)

type Discount struct {
  // other fields
  Amount int64 // precision to 4 decimal places, divide by 10000 when used as precentage
  DiscountType DiscountType
}

func (discount *Discount) Apply(subtotal int64) int64 {
  if (discount.DiscountType == FixedAmount) {
      // no easy way to get max or min, unless convert to float64 and use math
      if discount.Amount > subtotal {
        return subtotal
      }
      return discount.Amount
  } else {
      // have to switch between number types
      return int64(float64(subtotal) * float64(discount.Amount) / 10000)
  }
}
Enter fullscreen mode Exit fullscreen mode

And the code to allocate order discount to dishes.

var allocatedDiscount int64
for index, dish := range order.Dishes {
  weight := float64(dish.SubTotal) / float64(order.Subtotal())
  discount := int64(math.Floor(float64(orderDiscount.Amount) * weight))
  dishDeductible := dishItem.SubTotal - dishItem.PreTaxLineItemsDiscount - dishItem.PreTaxOrderDiscount
  // make sure each dish can not be deducted to negative subtotal
  if discount > dishDeductible {
    discount = dishDeductible
  }
  if discount > 0 {
    allocatedDiscount += discount
    order.Dishes[index].PreTaxOrderDiscount += discount
  }
}

// allocate the remainder
remainder := orderDiscount.Amount - allocatedDiscount
dishSize = len(order.Dishes)
for i := 0, i < remainder; i++ {
  dishIndex := i % int64(dishSize)
  item := cart.CartData.DishItems[dishIndex]
  if item.PreTaxOrderDiscount >= 0 && item.SubTotal-item.PreTaxLineItemsDiscount-item.PreTaxOrderDiscount > 0 {
    cart.CartData.DishItems[dishIndex].PreTaxOrderDiscount += 1
  }
}
Enter fullscreen mode Exit fullscreen mode

As we can see, there are lots of type casting between numbers, and what is more unpredictable in this code is that, if the order subtotal is 0, then weight is +/-Inf, and we learnt it the hard way.

Admittedly, it is true that here is ample room for code refactoring to enhance cleanliness, However, the points I am trying make is that we have to deal with this kind of logic through out the calculation logic, which makes the code tedious to write and hard to read for following reasons.

Have to constantly type casting between number types.
Need to shift the decimal digit for example from 10 to 0.01or vice versa.
Need a clear way to round up or down.
Need to make sure division is safely handle regardless the number types.
That is why we open source this library https://github.com/JingIsCoding/number from our code base to mitigate some of the issues. so we can make the code looks more like:

type DiscountType string

const (
 FixedAmount DiscountType = "fixed_amount"
 Percentage  DiscountType = "percentage"
)

type Discount struct {
  // other fields
  Amount number.Number
  DiscountType DiscountType
}

func (discount *Discount) Apply(subtotal int64) number.Number {
  if (discount.DiscountType == FixedAmount) {
      return discount.Amount.Min(subtotal)
  } else {
      return discount.Amount.ShiftDecimal(-4).Multiply(subtotal)
  }
}

Enter fullscreen mode Exit fullscreen mode
var allocatedDiscount number.Number
for index, dish := range order.Dishes {
  weight, err := number.Of(dish.SubTotal).Divide(order.Subtotal())
  // deal with divide by 0
  if err != nil {
    return
  }
  discount := weight.Multiply(orderDiscount.Amount).RoundDown()
  dishDeductible := dishItem.SubTotal - dishItem.PreTaxLineItemsDiscount - dishItem.PreTaxOrderDiscount
  // make sure each dish can not be deducted to negative subtotal
  discount = discount.Min(dishDeductible)
  if discount.IsGreaterThan(0) {
    allocatedDiscount = allocatedDiscount.Add(discount)
    order.Dishes[index].PreTaxOrderDiscount += discount.GetInt()
  }
}
Enter fullscreen mode Exit fullscreen mode

Hopefully you would find this library useful, and leave feedback to the repo if you like.

Top comments (0)