Skip to content

Commit

Permalink
HMA-8916: Updated calculation to use Scottish rate if `userPaysScotti…
Browse files Browse the repository at this point in the history
…shTax` set to true (#118)

* wip

* HMA-8916: Updated calculation to use Scottish rate if `userPaysScottishTax` set to true
  • Loading branch information
ngoulongkam authored Jul 30, 2024
1 parent a18e661 commit 0207bea
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
- Updated internal calculation to force Scottish Rate if consumer set `userPaysScottishTax` to true.

## [2.12.4] - 2024-07-24Z
- Added `taxableIncome` to response.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ Returns an object of type `CalculatorResponse`. This class is broken up into `we
- `otherAmount` of type `Double` (This will return 0.0 if no other amount)
- `taxableIncome` of type `Double`

> If `userSuppliedTaxCode` is set to `true`, tapering will not apply in the calculation, and it will force the calculation to be in Scottish Tax Rate.
> For tax breakdown this is the amount of tax per tax band which has two members, `percentage: Double` and `amount: Double`.
> `otherAmount` is the sum of `pensionContribution`, `finalStudentLoanAmount` and `finalPostgraduateLoanAmount`.
Expand Down
6 changes: 3 additions & 3 deletions src/commonMain/kotlin/uk/gov/hmrc/calculator/Calculator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ class Calculator @JvmOverloads constructor(
)

fun run(): CalculatorResponse {
getTaxCodeClarification(taxCode, userPaysScottishTax)?.let { listOfClarification.add(it) }

if (!WageValidator.isAboveMinimumWages(wages) || !WageValidator.isBelowMaximumWages(wages)) {
throw InvalidWagesException("Wages must be between 0 and 9999999.99")
}
Expand All @@ -122,8 +124,6 @@ class Calculator @JvmOverloads constructor(
(yearlyWageAfterPension - taxFreeAmount) + kCodeAdjustedAmount
} ?: (yearlyWageAfterPension - taxFreeAmount)

taxCodeType.getTaxCodeClarification(userPaysScottishTax)?.let { listOfClarification.add(it) }

return createResponse(
taxCodeType,
yearlyWages,
Expand Down Expand Up @@ -399,7 +399,7 @@ class Calculator @JvmOverloads constructor(
}

private val taxCodeType: TaxCode by lazy {
this.taxCode.toTaxCode()
this.taxCode.toTaxCode(forceScottishTaxCode = this.userPaysScottishTax)
}

data class StudentLoanPlans(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,21 @@ import uk.gov.hmrc.calculator.utils.toCountry
import kotlin.jvm.JvmSynthetic

@JvmSynthetic
internal fun String.toTaxCode(): TaxCode {
internal fun String.toTaxCode(forceScottishTaxCode: Boolean = false): TaxCode {
if (isBlank()) throw InvalidTaxCodeException("Tax code cannot be empty")

val formattedTaxCode = this.replace("\\s".toRegex(), "").uppercase()

return when (formattedTaxCode.toCountry()) {
val taxCode = when (formattedTaxCode.toCountry()) {
Country.SCOTLAND -> formattedTaxCode.matchScottishTaxCode()
Country.WALES -> formattedTaxCode.matchWelshTaxCode()
Country.ENGLAND -> formattedTaxCode.matchEnglishTaxCode()
Country.NONE -> NTCode()
}

return if (forceScottishTaxCode) {
formattedTaxCode.convertTaxCodeToScottishTaxCode().matchScottishTaxCode()
} else taxCode
}

@JvmSynthetic
Expand All @@ -51,6 +55,15 @@ internal fun String.extractDoubleFromEmergencyTaxCode(): Double =
.removeSuffix("T")
.toDouble()

@JvmSynthetic
internal fun String.convertTaxCodeToScottishTaxCode(): String {
val removedPrefix = this
.removePrefix("S")
.removePrefix("C")

return "S$removedPrefix"
}

/*
Tax-free amount without the "£9"
*/
Expand All @@ -62,11 +75,12 @@ internal fun TaxCode.getTrueTaxFreeAmount(): Double {

@Suppress("ComplexMethod")
@JvmSynthetic
internal fun TaxCode.getTaxCodeClarification(userPaysScottishTax: Boolean): Clarification? {
internal fun getTaxCodeClarification(userEnteredTaxCode: String, userPaysScottishTax: Boolean): Clarification? {
val taxCodeType = userEnteredTaxCode.toTaxCode(false)
val clarification = when {
(this is ScottishTaxCode) && userPaysScottishTax -> Clarification.SCOTTISH_INCOME_APPLIED
(this is ScottishTaxCode) && !userPaysScottishTax -> Clarification.SCOTTISH_CODE_BUT_OTHER_RATE
(this !is ScottishTaxCode) && userPaysScottishTax -> Clarification.NON_SCOTTISH_CODE_BUT_SCOTTISH_RATE
(taxCodeType is ScottishTaxCode) && userPaysScottishTax -> Clarification.SCOTTISH_INCOME_APPLIED
(taxCodeType is ScottishTaxCode) && !userPaysScottishTax -> Clarification.SCOTTISH_CODE_BUT_OTHER_RATE
(taxCodeType !is ScottishTaxCode) && userPaysScottishTax -> Clarification.NON_SCOTTISH_CODE_BUT_SCOTTISH_RATE
else -> null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import uk.gov.hmrc.calculator.utils.taxcode.toTaxCode
object TaxCodeValidator {
fun isValidTaxCode(taxCode: String): TaxCodeValidationResponse {
return try {
taxCode.toTaxCode()
taxCode.toTaxCode(false)
TaxCodeValidationResponse(true)
} catch (e: InvalidTaxCodeException) {
taxCode.invalidTaxCodeErrorGeneration()
Expand All @@ -35,7 +35,7 @@ object TaxCodeValidator {

fun validateTaxCodeMatchingRate(taxCode: String, isPayingScottishRate: Boolean): TaxCodeValidationResponse? {
return try {
return when (taxCode.toTaxCode().getTaxCodeClarification(isPayingScottishRate)) {
return when (getTaxCodeClarification(taxCode, isPayingScottishRate)) {
Clarification.SCOTTISH_CODE_BUT_OTHER_RATE ->
TaxCodeValidationResponse(true, ValidationError.ScottishCodeButOtherRate)
Clarification.NON_SCOTTISH_CODE_BUT_SCOTTISH_RATE ->
Expand Down
166 changes: 166 additions & 0 deletions src/commonTest/kotlin/uk/gov/hmrc/calculator/CalculatorTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,172 @@ internal class CalculatorTests {
assertEquals(16230.0, yearly.taxableIncome)
}

@Test
fun `GIVEN english tax code AND userPaysScottishTax true WHEN calculate THEN calculates scottish rate`() {
val result = Calculator(
taxCode = "1257L",
userPaysScottishTax = true,
wages = 100000.0,
payPeriod = PayPeriod.YEARLY,
taxYear = TaxYear.TWENTY_TWENTY_FOUR,
).run()
Logger.i(result.prettyPrintDataClass())

assertEquals(Country.SCOTLAND, result.country)
assertFalse(result.isKCode)

val weekly = result.weekly
assertEquals(PayPeriod.WEEKLY, weekly.payPeriod)
assertEquals(77.13, weekly.employeesNI)
assertEquals(241.23, weekly.employersNI)
assertEquals(1923.08, weekly.wages)
assertEquals(241.73, weekly.taxFree)
assertEquals(591.81, weekly.taxToPay)
assertEquals(1254.14, weekly.takeHome)
assertEquals(0.0, weekly.pensionContribution)
assertEquals(1923.08, weekly.wageAfterPensionDeduction)
assertEquals(0.0, weekly.taperingAmountDeduction)
assertNull(weekly.studentLoanBreakdown)
assertEquals(0.0, weekly.finalStudentLoanAmount)
assertEquals(0.0, weekly.finalPostgraduateLoanAmount)
assertEquals(0.0, weekly.otherAmount)
assertEquals(1681.35, weekly.taxableIncome)

val fourWeekly = result.fourWeekly
assertEquals(PayPeriod.FOUR_WEEKLY, fourWeekly.payPeriod)
assertEquals(308.51, fourWeekly.employeesNI)
assertEquals(964.94, fourWeekly.employersNI)
assertEquals(7692.31, fourWeekly.wages)
assertEquals(966.92, fourWeekly.taxFree)
assertEquals(2367.25, fourWeekly.taxToPay)
assertEquals(5016.55, fourWeekly.takeHome)
assertEquals(0.0, fourWeekly.pensionContribution)
assertEquals(7692.31, fourWeekly.wageAfterPensionDeduction)
assertEquals(0.0, fourWeekly.taperingAmountDeduction)
assertNull(fourWeekly.studentLoanBreakdown)
assertEquals(0.0, fourWeekly.finalStudentLoanAmount)
assertEquals(0.0, fourWeekly.finalPostgraduateLoanAmount)
assertEquals(0.0, fourWeekly.otherAmount)
assertEquals(6725.38, fourWeekly.taxableIncome)

val monthly = result.monthly
assertEquals(PayPeriod.MONTHLY, monthly.payPeriod)
assertEquals(334.22, monthly.employeesNI)
assertEquals(1045.35, monthly.employersNI)
assertEquals(8333.33, monthly.wages)
assertEquals(1047.50, monthly.taxFree)
assertEquals(2564.52, monthly.taxToPay)
assertEquals(5434.59, monthly.takeHome)
assertEquals(0.0, monthly.pensionContribution)
assertEquals(8333.33, monthly.wageAfterPensionDeduction)
assertEquals(0.0, monthly.taperingAmountDeduction)
assertNull(monthly.studentLoanBreakdown)
assertEquals(0.0, monthly.finalStudentLoanAmount)
assertEquals(0.0, monthly.finalPostgraduateLoanAmount)
assertEquals(0.0, monthly.otherAmount)
assertEquals(7285.83, monthly.taxableIncome)

val yearly = result.yearly
assertEquals(PayPeriod.YEARLY, yearly.payPeriod)
assertEquals(4010.6, yearly.employeesNI)
assertEquals(12544.2, yearly.employersNI)
assertEquals(100000.00, yearly.wages)
assertEquals(12570.0, yearly.taxFree)
assertEquals(30774.26, yearly.taxToPay)
assertEquals(65215.14, yearly.takeHome)
assertEquals(0.0, yearly.pensionContribution)
assertEquals(100000.0, yearly.wageAfterPensionDeduction)
assertEquals(0.0, yearly.taperingAmountDeduction)
assertNull(yearly.studentLoanBreakdown)
assertEquals(0.0, yearly.finalStudentLoanAmount)
assertEquals(0.0, yearly.finalPostgraduateLoanAmount)
assertEquals(0.0, yearly.otherAmount)
assertEquals(87430.0, yearly.taxableIncome)
}

@Test
fun `GIVEN welsh tax code AND userPaysScottishTax true WHEN calculate THEN calculates scottish rate`() {
val result = Calculator(
taxCode = "C1257L",
userPaysScottishTax = true,
wages = 100000.0,
payPeriod = PayPeriod.YEARLY,
taxYear = TaxYear.TWENTY_TWENTY_FOUR,
).run()
Logger.i(result.prettyPrintDataClass())

assertEquals(Country.SCOTLAND, result.country)
assertFalse(result.isKCode)

val weekly = result.weekly
assertEquals(PayPeriod.WEEKLY, weekly.payPeriod)
assertEquals(77.13, weekly.employeesNI)
assertEquals(241.23, weekly.employersNI)
assertEquals(1923.08, weekly.wages)
assertEquals(241.73, weekly.taxFree)
assertEquals(591.81, weekly.taxToPay)
assertEquals(1254.14, weekly.takeHome)
assertEquals(0.0, weekly.pensionContribution)
assertEquals(1923.08, weekly.wageAfterPensionDeduction)
assertEquals(0.0, weekly.taperingAmountDeduction)
assertNull(weekly.studentLoanBreakdown)
assertEquals(0.0, weekly.finalStudentLoanAmount)
assertEquals(0.0, weekly.finalPostgraduateLoanAmount)
assertEquals(0.0, weekly.otherAmount)
assertEquals(1681.35, weekly.taxableIncome)

val fourWeekly = result.fourWeekly
assertEquals(PayPeriod.FOUR_WEEKLY, fourWeekly.payPeriod)
assertEquals(308.51, fourWeekly.employeesNI)
assertEquals(964.94, fourWeekly.employersNI)
assertEquals(7692.31, fourWeekly.wages)
assertEquals(966.92, fourWeekly.taxFree)
assertEquals(2367.25, fourWeekly.taxToPay)
assertEquals(5016.55, fourWeekly.takeHome)
assertEquals(0.0, fourWeekly.pensionContribution)
assertEquals(7692.31, fourWeekly.wageAfterPensionDeduction)
assertEquals(0.0, fourWeekly.taperingAmountDeduction)
assertNull(fourWeekly.studentLoanBreakdown)
assertEquals(0.0, fourWeekly.finalStudentLoanAmount)
assertEquals(0.0, fourWeekly.finalPostgraduateLoanAmount)
assertEquals(0.0, fourWeekly.otherAmount)
assertEquals(6725.38, fourWeekly.taxableIncome)

val monthly = result.monthly
assertEquals(PayPeriod.MONTHLY, monthly.payPeriod)
assertEquals(334.22, monthly.employeesNI)
assertEquals(1045.35, monthly.employersNI)
assertEquals(8333.33, monthly.wages)
assertEquals(1047.50, monthly.taxFree)
assertEquals(2564.52, monthly.taxToPay)
assertEquals(5434.59, monthly.takeHome)
assertEquals(0.0, monthly.pensionContribution)
assertEquals(8333.33, monthly.wageAfterPensionDeduction)
assertEquals(0.0, monthly.taperingAmountDeduction)
assertNull(monthly.studentLoanBreakdown)
assertEquals(0.0, monthly.finalStudentLoanAmount)
assertEquals(0.0, monthly.finalPostgraduateLoanAmount)
assertEquals(0.0, monthly.otherAmount)
assertEquals(7285.83, monthly.taxableIncome)

val yearly = result.yearly
assertEquals(PayPeriod.YEARLY, yearly.payPeriod)
assertEquals(4010.6, yearly.employeesNI)
assertEquals(12544.2, yearly.employersNI)
assertEquals(100000.00, yearly.wages)
assertEquals(12570.0, yearly.taxFree)
assertEquals(30774.26, yearly.taxToPay)
assertEquals(65215.14, yearly.takeHome)
assertEquals(0.0, yearly.pensionContribution)
assertEquals(100000.0, yearly.wageAfterPensionDeduction)
assertEquals(0.0, yearly.taperingAmountDeduction)
assertNull(yearly.studentLoanBreakdown)
assertEquals(0.0, yearly.finalStudentLoanAmount)
assertEquals(0.0, yearly.finalPostgraduateLoanAmount)
assertEquals(0.0, yearly.otherAmount)
assertEquals(87430.0, yearly.taxableIncome)
}

@Test
fun `GIVEN hours is zero and pay period hour WHEN calculate THEN exception`() {
assertFailsWith<InvalidHoursException> {
Expand Down
Loading

0 comments on commit 0207bea

Please sign in to comment.