Skip to content

Commit

Permalink
HMA-3006 fix for InvalidMutabilityException (#44)
Browse files Browse the repository at this point in the history
* HMA-3006 fix for InvalidMutabilityException

* HMA-3006 moved CalculatorTests into commonTest
  • Loading branch information
jvanderwee authored Jun 2, 2020
1 parent ef0f691 commit cf0d988
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 74 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Build Status](https://app.bitrise.io/app/cd7fb52c258b9273/status.svg?token=lntO8o4xz5AUEvLwVzbo3A&branch=master)](https://app.bitrise.io/app/cd7fb52c258b9273)
![LINE](https://img.shields.io/badge/line--coverage-98%25-brightgreen.svg)
![BRANCH](https://img.shields.io/badge/branch--coverage-93%25-brightgreen.svg)
![COMPLEXITY](https://img.shields.io/badge/complexity-1.51-brightgreen.svg)
![COMPLEXITY](https://img.shields.io/badge/complexity-1.52-brightgreen.svg)
[ ![Download](https://api.bintray.com/packages/hmrc/mobile-releases/tax-kalculator/images/download.svg) ](https://bintray.com/hmrc/mobile-releases/tax-kalculator/_latestVersion)

## Calculate take-home pay
Expand Down
51 changes: 17 additions & 34 deletions src/commonMain/kotlin/uk/gov/hmrc/calculator/Calculator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import uk.gov.hmrc.calculator.exception.InvalidWagesException
import uk.gov.hmrc.calculator.model.BandBreakdown
import uk.gov.hmrc.calculator.model.CalculatorResponse
import uk.gov.hmrc.calculator.model.CalculatorResponsePayPeriod
import uk.gov.hmrc.calculator.model.Country
import uk.gov.hmrc.calculator.model.Country.ENGLAND
import uk.gov.hmrc.calculator.model.PayPeriod
import uk.gov.hmrc.calculator.model.PayPeriod.FOUR_WEEKLY
Expand Down Expand Up @@ -75,24 +74,25 @@ class Calculator @JvmOverloads constructor(

val taxCode = this.taxCode.toTaxCode()

val taxBands = TaxBands.getBands(taxYear, taxCode.country)
val taxBands = TaxBands.getAdjustedBands(taxYear, taxCode)

val taxFreeAmount = adjustTaxBands(taxBands, taxCode)[0].upper
val taxFreeAmount = taxBands[0].upper
val amountToAddToWages = if (taxCode is KTaxCode) taxCode.amountToAddToWages else null

return createResponse(
taxCode,
yearlyWages,
taxFreeAmount,
amountToAddToWages
)
}

private fun createResponse(
taxCode: TaxCode,
yearlyWages: Double,
taxFreeAmount: Double,
amountToAddToWages: Double?
): CalculatorResponse {
val taxCode = this.taxCode.toTaxCode()
val taxPayable = taxToPay(yearlyWages, taxCode)
val employeesNI = employeeNIToPay(yearlyWages)
val employersNI = employerNIToPay(yearlyWages)
Expand Down Expand Up @@ -143,15 +143,20 @@ class Calculator @JvmOverloads constructor(
}

private fun taxToPay(yearlyWages: Double, taxCode: TaxCode): Double {
val taxBands = TaxBands.getBands(taxYear, taxCode.country)

return when (taxCode) {
is StandardTaxCode, is AdjustedTaxFreeTCode, is EmergencyTaxCode, is MarriageTaxCodes ->
getTotalFromBands(adjustTaxBands(taxBands, taxCode), yearlyWages)
is StandardTaxCode, is AdjustedTaxFreeTCode, is EmergencyTaxCode, is MarriageTaxCodes -> {
val taxBands = TaxBands.getAdjustedBands(taxYear, taxCode)
getTotalFromBands(taxBands, yearlyWages)
}
is NoTaxTaxCode -> getTotalFromSingleBand(yearlyWages, taxCode.taxFreeAmount)
is SingleBandTax -> getTotalFromSingleBand(yearlyWages, taxBands[taxCode.taxAllAtBand].percentageAsDecimal)
is KTaxCode ->
getTotalFromBands(adjustTaxBands(taxBands, taxCode), yearlyWages + taxCode.amountToAddToWages)
is SingleBandTax -> {
val taxBands = TaxBands.getBands(taxYear, taxCode.country)
getTotalFromSingleBand(yearlyWages, taxBands[taxCode.taxAllAtBand].percentageAsDecimal)
}
is KTaxCode -> {
val taxBands = TaxBands.getAdjustedBands(taxYear, taxCode)
getTotalFromBands(taxBands, yearlyWages + taxCode.amountToAddToWages)
}
else -> throw InvalidTaxCodeException("$this is an invalid tax code")
}
}
Expand All @@ -162,23 +167,6 @@ class Calculator @JvmOverloads constructor(
return taxToPayForSingleBand
}

private fun adjustTaxBands(taxBands: List<Band>, taxCode: TaxCode): List<Band> {
// The full tax free amount e.g. 12509
val bandAdjuster = getDefaultTaxAllowance(taxYear, taxCode.country)

taxBands[0].upper = taxCode.taxFreeAmount
taxBands[1].lower = taxCode.taxFreeAmount
taxBands[1].upper = taxBands[1].upper + taxCode.taxFreeAmount - bandAdjuster

for (bandNumber in 2 until taxBands.size) {
taxBands[bandNumber].lower = taxBands[bandNumber].lower + taxCode.taxFreeAmount - bandAdjuster
if (taxBands[bandNumber].upper != -1.0) {
taxBands[bandNumber].upper = taxBands[bandNumber].upper + taxCode.taxFreeAmount - bandAdjuster
}
}
return taxBands
}

private fun employerNIToPay(yearlyWages: Double) =
if (isPensionAge) 0.0 else getTotalFromBands(EmployerNIBands(taxYear).bands, yearlyWages)

Expand Down Expand Up @@ -212,13 +200,8 @@ class Calculator @JvmOverloads constructor(

fun getDefaultTaxCode(): String {
val taxYear = TaxYear().currentTaxYear()
val defaultTaxAllowance = getDefaultTaxAllowance(taxYear)
val defaultTaxAllowance = TaxBands.getBands(taxYear, ENGLAND)[0].upper.toInt()
return "${(defaultTaxAllowance / 10)}L"
}

internal fun getDefaultTaxAllowance(
taxYear: Int,
country: Country = ENGLAND
) = TaxBands.getBands(taxYear, country)[0].upper.toInt()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,46 @@ package uk.gov.hmrc.calculator.model.bands
import uk.gov.hmrc.calculator.exception.InvalidTaxYearException
import uk.gov.hmrc.calculator.model.Country
import uk.gov.hmrc.calculator.model.Country.SCOTLAND
import uk.gov.hmrc.calculator.model.taxcodes.TaxCode

internal object TaxBands {

private object Year2020 {
val scotland: List<Band> = listOf(
TaxBand(0.0, 12509.00, 0.0),
TaxBand(12509.00, 14585.00, 0.19),
TaxBand(14585.00, 25158.00, 0.20),
TaxBand(25158.00, 43430.00, 0.21),
TaxBand(43430.00, 150000.00, 0.41),
TaxBand(150000.0, -1.0, 0.46)
)
val other: List<Band> = listOf(
TaxBand(0.0, 12509.00, 0.0),
TaxBand(12509.0, 50000.00, 0.2),
TaxBand(50000.0, 150000.00, 0.4),
TaxBand(150000.0, -1.0, 0.45)
)
}

fun getBands(taxYear: Int, country: Country) = when (taxYear) {
2020 -> when (country) {
SCOTLAND -> Year2020.scotland
else -> Year2020.other
SCOTLAND -> listOf(
TaxBand(0.0, 12509.00, 0.0),
TaxBand(12509.00, 14585.00, 0.19),
TaxBand(14585.00, 25158.00, 0.20),
TaxBand(25158.00, 43430.00, 0.21),
TaxBand(43430.00, 150000.00, 0.41),
TaxBand(150000.0, -1.0, 0.46)
)
else -> listOf(
TaxBand(0.0, 12509.00, 0.0),
TaxBand(12509.0, 50000.00, 0.2),
TaxBand(50000.0, 150000.00, 0.4),
TaxBand(150000.0, -1.0, 0.45)
)
}
else -> throw InvalidTaxYearException("$taxYear")
}

fun getAdjustedBands(taxYear: Int, taxCode: TaxCode): List<Band> {
val taxBands = getBands(taxYear, taxCode.country).toMutableList()

// The full tax free amount e.g. 12509
val bandAdjuster = taxBands[0].upper.toInt()

taxBands[0].upper = taxCode.taxFreeAmount
taxBands[1].lower = taxCode.taxFreeAmount
taxBands[1].upper = taxBands[1].upper + taxCode.taxFreeAmount - bandAdjuster

for (bandNumber in 2 until taxBands.size) {
taxBands[bandNumber].lower = taxBands[bandNumber].lower + taxCode.taxFreeAmount - bandAdjuster
if (taxBands[bandNumber].upper != -1.0) {
taxBands[bandNumber].upper = taxBands[bandNumber].upper + taxCode.taxFreeAmount - bandAdjuster
}
}
return taxBands
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
*/
package uk.gov.hmrc.calculator

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import uk.gov.hmrc.calculator.exception.InvalidHoursException
import uk.gov.hmrc.calculator.exception.InvalidWagesException
import uk.gov.hmrc.calculator.model.PayPeriod
Expand All @@ -27,35 +27,35 @@ internal class CalculatorTests {
@Test
fun `GIVEN hours is zero and pay period hour WHEN calculate THEN exception`() {
assertFailsWith<InvalidHoursException> {
Calculator("1250L", 20.0, payPeriod = PayPeriod.HOURLY, howManyAWeek = 0.0).run()
Calculator(taxCode = "1250L", wages = 20.0, payPeriod = PayPeriod.HOURLY, howManyAWeek = 0.0).run()
}
}

@Test
fun `GIVEN hours is null and pay period hour WHEN calculate THEN exception`() {
assertFailsWith<InvalidHoursException> {
Calculator("1250L", 20.0, payPeriod = PayPeriod.HOURLY).run()
Calculator(taxCode = "1250L", wages = 20.0, payPeriod = PayPeriod.HOURLY).run()
}
}

@Test
fun `GIVEN wages is below zero WHEN calculate THEN exception`() {
assertFailsWith<InvalidWagesException> {
Calculator("1250L", -190.0, payPeriod = PayPeriod.WEEKLY).run()
Calculator(taxCode = "1250L", wages = -190.0, payPeriod = PayPeriod.WEEKLY).run()
}
}

@Test
fun `GIVEN wages is zero WHEN calculate THEN exception`() {
assertFailsWith<InvalidWagesException> {
Calculator("1250L", 0.0, payPeriod = PayPeriod.YEARLY).run()
Calculator(taxCode = "1250L", wages = 0.0, payPeriod = PayPeriod.YEARLY).run()
}
}

@Test
fun `GIVEN wages too high WHEN calculate THEN exception`() {
assertFailsWith<InvalidWagesException> {
Calculator("1250L", 10000000.0, payPeriod = PayPeriod.YEARLY).run()
Calculator(taxCode = "1250L", wages = 10000000.0, payPeriod = PayPeriod.YEARLY).run()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import kotlin.test.assertFailsWith
import uk.gov.hmrc.calculator.exception.InvalidTaxYearException
import uk.gov.hmrc.calculator.model.Country.ENGLAND
import uk.gov.hmrc.calculator.model.Country.SCOTLAND
import uk.gov.hmrc.calculator.model.Country.WALES
import uk.gov.hmrc.calculator.utils.taxcode.toTaxCode

class TaxBandsTests {

@Test
fun invalidYear() {
fun `GIVEN invalid year WHEN get bands THEN fail with exception`() {
val exception = assertFailsWith<InvalidTaxYearException> {
TaxBands.getBands(
2017,
Expand All @@ -36,17 +38,126 @@ class TaxBandsTests {
}

@Test
fun bandsForScotland2020() {
val taxBand = TaxBands.getBands(2020, SCOTLAND)[1]
assertEquals(14585.00, taxBand.upper)
assertEquals(12509.00, taxBand.lower)
assertEquals(0.19, taxBand.percentageAsDecimal)

assertEquals(false, taxBand.inBand(12509.00))
assertEquals(false, taxBand.inBand(12508.00))
assertEquals(true, taxBand.inBand(12510.00))
assertEquals(true, taxBand.inBand(14585.00))
assertEquals(true, taxBand.inBand(14584.00))
assertEquals(false, taxBand.inBand(14586.00))
fun `GIVEN invalid year WHEN get adjusted bands THEN fail with exception`() {
val exception = assertFailsWith<InvalidTaxYearException> {
TaxBands.getAdjustedBands(
2017,
"1250L".toTaxCode()
)
}
assertEquals(exception.message, "2017")
}

@Test
fun `GIVEN year is 2020 WHEN get bands for Scotland THEN bands are as expected`() {
val taxBands = TaxBands.getBands(2020, SCOTLAND)

assertEquals(0.0, taxBands[0].lower)
assertEquals(12509.00, taxBands[0].upper)
assertEquals(0.0, taxBands[0].percentageAsDecimal)

assertEquals(12509.00, taxBands[1].lower)
assertEquals(14585.00, taxBands[1].upper)
assertEquals(0.19, taxBands[1].percentageAsDecimal)

assertEquals(14585.00, taxBands[2].lower)
assertEquals(25158.00, taxBands[2].upper)
assertEquals(0.20, taxBands[2].percentageAsDecimal)

assertEquals(25158.00, taxBands[3].lower)
assertEquals(43430.00, taxBands[3].upper)
assertEquals(0.21, taxBands[3].percentageAsDecimal)

assertEquals(43430.00, taxBands[4].lower)
assertEquals(150000.00, taxBands[4].upper)
assertEquals(0.41, taxBands[4].percentageAsDecimal)

assertEquals(150000.00, taxBands[5].lower)
assertEquals(-1.0, taxBands[5].upper)
assertEquals(0.46, taxBands[5].percentageAsDecimal)
}

@Test
fun `GIVEN year is 2020 WHEN get bands for ENGLAND THEN bands are as expected`() {
val taxBands = TaxBands.getBands(2020, ENGLAND)

assertEquals(0.0, taxBands[0].lower)
assertEquals(12509.00, taxBands[0].upper)
assertEquals(0.0, taxBands[0].percentageAsDecimal)

assertEquals(12509.00, taxBands[1].lower)
assertEquals(50000.0, taxBands[1].upper)
assertEquals(0.2, taxBands[1].percentageAsDecimal)

assertEquals(50000.0, taxBands[2].lower)
assertEquals(150000.0, taxBands[2].upper)
assertEquals(0.4, taxBands[2].percentageAsDecimal)

assertEquals(150000.0, taxBands[3].lower)
assertEquals(-1.0, taxBands[3].upper)
assertEquals(0.45, taxBands[3].percentageAsDecimal)
}

@Test
fun `GIVEN year is 2020 WHEN get bands for WALES THEN bands are as expected`() {
val taxBands = TaxBands.getBands(2020, WALES)

assertEquals(0.0, taxBands[0].lower)
assertEquals(12509.00, taxBands[0].upper)
assertEquals(0.0, taxBands[0].percentageAsDecimal)

assertEquals(12509.00, taxBands[1].lower)
assertEquals(50000.0, taxBands[1].upper)
assertEquals(0.2, taxBands[1].percentageAsDecimal)

assertEquals(50000.0, taxBands[2].lower)
assertEquals(150000.0, taxBands[2].upper)
assertEquals(0.4, taxBands[2].percentageAsDecimal)

assertEquals(150000.0, taxBands[3].lower)
assertEquals(-1.0, taxBands[3].upper)
assertEquals(0.45, taxBands[3].percentageAsDecimal)
}

@Test
fun `GIVEN year is 2020 WHEN get adjusted bands for 1250L THEN bands are as expected`() {
val taxBands = TaxBands.getAdjustedBands(2020, "1250L".toTaxCode())

assertEquals(0.0, taxBands[0].lower)
assertEquals(12509.00, taxBands[0].upper)
assertEquals(0.0, taxBands[0].percentageAsDecimal)

assertEquals(12509.00, taxBands[1].lower)
assertEquals(50000.0, taxBands[1].upper)
assertEquals(0.2, taxBands[1].percentageAsDecimal)

assertEquals(50000.0, taxBands[2].lower)
assertEquals(150000.0, taxBands[2].upper)
assertEquals(0.4, taxBands[2].percentageAsDecimal)

assertEquals(150000.0, taxBands[3].lower)
assertEquals(-1.0, taxBands[3].upper)
assertEquals(0.45, taxBands[3].percentageAsDecimal)
}

@Test
fun `GIVEN year is 2020 WHEN get adjusted bands for BR THEN bands are as expected`() {
val taxBands = TaxBands.getAdjustedBands(2020, "BR".toTaxCode())

assertEquals(0.0, taxBands[0].lower)
assertEquals(0.0, taxBands[0].upper)
assertEquals(0.0, taxBands[0].percentageAsDecimal)

assertEquals(0.0, taxBands[1].lower)
assertEquals(37491.0, taxBands[1].upper)
assertEquals(0.2, taxBands[1].percentageAsDecimal)

assertEquals(37491.0, taxBands[2].lower)
assertEquals(137491.0, taxBands[2].upper)
assertEquals(0.4, taxBands[2].percentageAsDecimal)

assertEquals(137491.0, taxBands[3].lower)
assertEquals(-1.0, taxBands[3].upper)
assertEquals(0.45, taxBands[3].percentageAsDecimal)
}
}

0 comments on commit cf0d988

Please sign in to comment.