/* * [CalculateCanadianTaxes.java] * * Summary: Calculate Canadian Sales taxes given a province, date and sale amount. * * Copyright: (c) 2011-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2011-03-24 initial version */ package com.mindprod.canadiantax; import com.mindprod.common18.BigDate; /** * Calculate Canadian Sales taxes given a province, date and sale amount. * * @author Roedy Green, Canadian Mind Products * @version 1.0 2011-03-24 initial version. * @since 2011-03-24 */ class CalculateCanadianTaxes { /** * does province cheat and compound pst on gst? */ private boolean taxOnTax; /** * gst amount or gst portion of hst */ private double gstAmount; /** * gstRate or gst portion of hst */ private double gstRate; /** * hst amount */ private double hstAmount; /** * hst rate */ private double hstRate; /** * pst amount or pst portion of HST */ private double pstAmount; /** * pst rate or pst part of hst */ private double pstRate; /** * sale amount plus hst or gst + pst */ private double totalPayableAmount; /** * hst or gst + pst */ private double totalTaxAmount; /** * Round dollar values to penny. * * @param dollars amount in fractional dollars * * @return dollars rounded to the nearest penny. */ private static double nickelRounded( double dollars ) { return ( ( double ) ( ( ( long ) ( dollars * 100.0d ) + 2 ) / 5 * 5 ) ) / 100.0d; } /** * Round dollar values to penny. * * @param dollars amount in fractional dollars * * @return dollars rounded to the nearest penny. */ private static double pennyRounded( double dollars ) { return Math.rint( dollars * 100.0d ) / 100d; } /** * guts of calulation to compute taxes. Presumes fields needed are set. * Can retrieve the results with * getGst, getHst, getPst, getTotalTaxAmount getTotalPayableAmount, isTaxOnTax * * @param saleAmount amount of sale. */ private void calculateCanadianTaxesGuts( final double saleAmount ) { gstAmount = saleAmount * gstRate / 100.0d; hstAmount = saleAmount * hstRate / 100.0d; final double pstBase = taxOnTax ? ( saleAmount + gstAmount ) : saleAmount; pstAmount = pstBase * pstRate / 100.d; totalTaxAmount = ( hstRate > 0 ) ? hstAmount : gstAmount + pstAmount; totalPayableAmount = saleAmount + totalTaxAmount; } /** * compute taxes on a sale. Can retrieve the results with * getGst, getHst, getPst, getTotalTaxAmount getTotalPayableAmount, isTaxOnTax * * @param province enum * @param date YYYY-MM-DD you want taxes calculated as of. * @param saleAmount amount of sale */ public void calculateCanadianTaxes( Province province, BigDate date, double saleAmount ) { final ProvincialTaxFact taxFact = province.findTaxFactForDate( date ); gstRate = taxFact.getGstRate(); hstRate = taxFact.getHstRate(); pstRate = taxFact.getPstRate(); taxOnTax = taxFact.isTaxOnTax(); calculateCanadianTaxesGuts( saleAmount ); } /** * compute original sale price to have it come out to given value after the taxes are added on. * Calculate the sale amount, WORKING BACKWARDS from the Total Payable. When you calculate forward again you may be * off a penny or two from the original total payable. This is unavoidable, nothing to do with the program. This * method gives you the closest possible value. * * @param province enum * @param date YYYY-MM-DD you want taxes calculated as of. * @param targetTotalPayable final amount of sale including taxes * * @return sale amount that after taxes would come closest to targetTotalPayable. */ public double calculateSaleAmount( Province province, BigDate date, double targetTotalPayable ) { final ProvincialTaxFact taxFact = province.findTaxFactForDate( date ); gstRate = taxFact.getGstRate(); hstRate = taxFact.getHstRate(); pstRate = taxFact.getPstRate(); taxOnTax = taxFact.isTaxOnTax(); double totalTaxRate = getTotalTaxRate(); double saleAmount = targetTotalPayable / ( 1. + totalTaxRate / 100 ); // see how good an estimate we have double bestDiff = 999999999.; double bestSaleAmount = saleAmount; while ( true ) { calculateCanadianTaxesGuts( saleAmount ); final double diff = this.totalPayableAmount - targetTotalPayable; if ( Math.abs( diff ) >= bestDiff ) { // we are doing worse that previously. We have overshot. Return previous best. return bestSaleAmount; } else { // this is the best so far. bestSaleAmount = saleAmount; bestDiff = Math.abs( diff ); } if ( diff == 0 ) { return bestSaleAmount; // got it bang on. } else if ( diff > 0 ) { // estimate for saleAmount was too large // try an estimate 1 penny smaller to see if it works better. saleAmount -= .01; } else { // estimate for saleAmount was too small // try an estimate 1 penny bigger to see if it works better. saleAmount += .01; } saleAmount = pennyRounded( saleAmount ); } } /** * get effective PST rate if province quoted the rate honestly as if there were not tax on tax. * * @return pst rate */ public double getEffectivePstRate() { return taxOnTax ? pstRate * ( 1.0 + gstRate / 100.0d ) : pstRate; } /** * get gst, or gst portion of hst * * @return gst amount */ public double getGstAmount() { return pennyRounded( gstAmount ); } /** * get gst rate in effect at the time as a percentage or gst portion of hst * * @return gst rate */ public double getGstRate() { return gstRate; } /** * get hst or 0 * * @return hst amount */ public double getHstAmount() { return pennyRounded( hstAmount ); } /** * get hst rate in effect at the time as a percentage. * * @return hst rate */ public double getHstRate() { return hstRate; } /** * total payable rounded to nearest nickel * * @return rounded to govt spec */ public double getNickelRoundedtotalPayableAmount() { return nickelRounded( pennyRounded( totalPayableAmount ) ); } /** * get pst, or pst portion of hst * * @return pst amount */ public double getPstAmount() { return pennyRounded( pstAmount ); } /** * get pst rate in effect at the time as a percentage or pst portion of hst * * @return pst rate */ public double getPstRate() { return pstRate; } /** * sale amount plus hst or gst + pst * * @return total payable */ public double getTotalPayableAmount() { return pennyRounded( totalPayableAmount ); } /** * hst or pst + gst * * @return total tax */ public double getTotalTaxAmount() { return pennyRounded( totalTaxAmount ); } /** * get HST or GST + effective PST rate if province quoted the rate honestly as if there were not tax on tax. * * @return next tax rate */ public double getTotalTaxRate() { return hstRate > 0 ? hstRate : gstRate + ( taxOnTax ? pstRate * ( 1.0 + gstRate / 100.0d ) : pstRate ); } /** * does province cheat and compound pst on gst? * * @return true if rate is compounded. */ public boolean isTaxOnTax() { return taxOnTax; } }