Skip to content

Commit

Permalink
UI: Tax Number and Tax Residency (#48)
Browse files Browse the repository at this point in the history
* add TaxNumber and TaxResidency component to btr-common-component

* add TaxNumber and TaxResidency to the main app

* update Tax Number component and add tests

* fix bugs in tax number

* Tax Number component update - check the radio button when the input box is clicked

* small tweak for tax number component

---------

Co-authored-by: Patrick Wang <[email protected]>
  • Loading branch information
patrickpeinanw and Patrick Wang authored Nov 11, 2023
1 parent c778a7c commit 42ee4b5
Show file tree
Hide file tree
Showing 12 changed files with 409 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -1,58 +1,65 @@
describe('forms -> preferred name -> validate that the preferred name component work inside example form', () => {
let en: any

beforeEach(() => {
// load the English version of the language file
cy.readFile('lang/en.json').then((json) => {
en = json
})

// navigate to index page and check footer and header exist
cy.visit('/examples/form')
cy.wait(1000)
})

it('test the validation rule for the maximum name length', () => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.include('The legal name must not exceed 150 characters')
expect(err.message).to.include(en.errors.validation.fullName.maxLengthExceeded)
return false
})

const invalidLongName = 'a'.repeat(151)
const validLongName = ' ' + 'a'.repeat(150) + ' '

cy.get('#testFullName').type(invalidLongName).blur()
cy.contains('The legal name must not exceed 150 characters').should('exist')
cy.contains(en.errors.validation.fullName.maxLengthExceeded).should('exist')

cy.get('#testFullName').clear().type(validLongName).blur()
cy.contains('The legal name must not exceed 150 characters').should('not.exist')
cy.contains(en.errors.validation.fullName.maxLengthExceeded).should('not.exist')
})

it('test the validation rule for the minimum name length', () => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.include('The legal name should contain at least one character')
expect(err.message).to.include(en.errors.validation.fullName.empty)
return false
})

const singleCharacter = 'a'
cy.get('#testFullName').type(singleCharacter).blur()
cy.contains('The legal name should contain at least one character').should('not.exist')
cy.contains(en.errors.validation.fullName.empty).should('not.exist')

cy.get('#testFullName').clear().blur()
cy.contains('The legal name should contain at least one character').should('exist')
cy.contains(en.errors.validation.fullName.empty).should('exist')
})

it('test the validation rule for special character', () => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.include('The legal name should not contain special character')
expect(err.message).to.include(en.errors.validation.fullName.specialCharacter)
return false
})

const invalidName = 'first - last'
const validName = 'first last'

cy.get('#testFullName').type(invalidName).blur()
cy.contains('The legal name should not contain special character').should('exist')
cy.contains(en.errors.validation.fullName.specialCharacter).should('exist')

cy.get('#testFullName').clear().type(validName).blur()
cy.contains('The legal name should not contain special character').should('not.exist')
cy.contains(en.errors.validation.fullName.specialCharacter).should('not.exist')
})

it('the full name field should accept UTF-8 characters', () => {
cy.contains('Full Legal Name:')
cy.contains(en.labels.fullName)
const email = '[email protected]'
cy.get('#testEmail').type(email)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
describe('forms -> preferred name -> validate that the preferred name component work inside example form', () => {
let en: any

beforeEach(() => {
// load the English version of the language file
cy.readFile('lang/en.json').then((json) => {
en = json
})

// navigate to index page and check footer and header exist
cy.visit('/examples/form')
cy.wait(1000)
})

it('test the validation rule for the maximum length of the preferred name', () => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.include('The preferred name must not exceed 150 characters')
expect(err.message).to.include(en.errors.validation.preferredName.maxLengthExceeded)
return false
})

const invalidLongName = 'a'.repeat(151)
const validLongName = ' ' + 'a'.repeat(150) + ' '

cy.get('#testPreferredName').type(invalidLongName).blur()
cy.contains('The preferred name must not exceed 150 characters').should('exist')
cy.contains(en.errors.validation.preferredName.maxLengthExceeded).should('exist')

cy.get('#testPreferredName').clear().type(validLongName).blur()
cy.contains('The preferred name must not exceed 150 characters').should('not.exist')
cy.contains(en.errors.validation.preferredName.maxLengthExceeded).should('not.exist')
})

it('preferred name can be empty', () => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.include('The preferred name should contain at least one character')
return false
})

cy.get('#testPreferredName').clear().blur()
cy.contains('The preferred name should contain at least one character').should('not.exist')
cy.get('#testPreferredName').type('a').clear().blur()
})

it('test the validation rule for special character', () => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.include('The preferred name should not contain special character')
expect(err.message).to.include(en.errors.validation.preferredName.specialCharacter)
return false
})

Expand All @@ -43,16 +44,16 @@ describe('forms -> preferred name -> validate that the preferred name component
const unicodeName2 = 'José 玛丽'

cy.get('#testPreferredName').type(invalidName).blur()
cy.contains('The preferred name should not contain special character').should('exist')
cy.contains(en.errors.validation.preferredName.specialCharacter).should('exist')

cy.get('#testPreferredName').clear().type(validName).blur()
cy.contains('The preferred name should not contain special character').should('not.exist')
cy.contains(en.errors.validation.preferredName.specialCharacter).should('not.exist')

cy.get('#testPreferredName').clear().type(unicodeName1).blur()
cy.contains('The preferred name should not contain special character').should('not.exist')
cy.contains(en.errors.validation.preferredName.specialCharacter).should('not.exist')

cy.get('#testPreferredName').clear().type(unicodeName2).blur()
cy.contains('The preferred name should not contain special character').should('not.exist')
cy.contains(en.errors.validation.preferredName.specialCharacter).should('not.exist')
})

it('the displayed name should be normalized', () => {
Expand Down
4 changes: 2 additions & 2 deletions btr-web/btr-common-components/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@
"fullName": {
"empty": "The legal name should contain at least one character",
"maxLengthExceeded": "The legal name must not exceed 150 characters",
"specialCharacter": "The legal name should not contain special character"
"specialCharacter": "The legal name should not contain special characters"
},
"preferredName": {
"maxLengthExceeded": "The preferred name must not exceed 150 characters",
"specialCharacter": "The preferred name should not contain special character"
"specialCharacter": "The preferred name should not contain special characters"
}
}
},
Expand Down
6 changes: 4 additions & 2 deletions btr-web/btr-common-components/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { validateEmailRfc6532Regex, validateNameCharacters, validatePreferredName, normalizeName }
from './validation/form_inputs'
export {
validateEmailRfc6532Regex, validateNameCharacters, validatePreferredName, normalizeName,
checkSpecialCharacters, checkTaxNumberLength, validateTaxNumber
} from './validation/form_inputs'

// canada post retrieve api
export type { CanadaPostRetrieveItemI, CanadaPostApiRetrieveParamsI } from './canadaPostAddressApi/retrieve-v2.11'
Expand Down
50 changes: 50 additions & 0 deletions btr-web/btr-common-components/utils/validation/form_inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,53 @@ export const normalizeName = (name?: string): string => {
}
return name.trim().replace(/\s+/g, ' ')
}

/**
* Check a tax number to ensure that it only consists of digits and whitespace.
* @param {string} taxNumber - string representation of the tax number input
*/
export const checkSpecialCharacters = (taxNumber: string): boolean => {
return /^[\d\s]*$/.test(taxNumber)
}

/**
* Check if the tax number has 9 digits
* @param {string} taxNumber - string representation of the tax number input
*/
export const checkTaxNumberLength = (taxNumber: string): boolean => {
const digits = taxNumber.replace(/\s+/g, '')
return digits.length === 9
}

/**
* Check if the tax number is valid
* @param {string} taxNumber - string representation of the tax number input
*/
export const validateTaxNumber = (taxNumber: string): boolean => {
// TODO: To confirm the validation algorithms for SIN, ITN, and TTN
// SIN Validation rule used: https://en.wikipedia.org/wiki/Social_insurance_number

const digits = taxNumber.replace(/\s+/g, '')

if (digits.length !== 9) {
return false
}

let checkSum = 0

for (let i = 0; i < 9; i++) {
const digit = parseInt(digits[i])
if (i % 2 === 0) {
checkSum += digit
} else {
const product = digit * 2
if (product < 10) {
checkSum += product
} else {
checkSum += product - 9
}
}
}

return checkSum % 10 === 0
}
66 changes: 63 additions & 3 deletions btr-web/btr-main-app/components/individual-person/AddNew.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
/>
</div>
</UForm>

<div class="text-blue-700 py-5 align-middle">
<a
id="add-person-manually-toggle"
Expand Down Expand Up @@ -127,12 +128,44 @@
v-model:citizenships="citizenships"
/>
</div>
<UForm
:schema="schema"
:state="state"
>
<div>
<p class="font-bold py-5">
{{ $t('labels.taxNumber') }}
</p>
<p class="text-justify">
{{ $t('texts.taxNumber') }}
</p>
<IndividualPersonTaxInfoTaxNumber
id="addNewPersonTaxNumber"
v-model="taxInfoModel"
name="taxNumber"
data-cy="testTaxNumber"
/>
</div>
</UForm>
<div>
<p class="font-bold py-5">
{{ $t('labels.taxResidency') }}
</p>
<p class="text-justify">
{{ $t('texts.taxResidency') }}
</p>
<IndividualPersonTaxInfoTaxResidency
id="addNewPersonTaxResidency"
v-model="isTaxResident"
data-cy="testTaxResidency"
/>
</div>
</template>
</div>
</template>

<script setup lang="ts">
import { Ref, ref } from 'vue'
import { Ref, ref, computed } from 'vue'
import { z } from 'zod'
const showAddInfoManually = ref(false)
Expand Down Expand Up @@ -182,14 +215,41 @@ const schema = z.object({
email: z.string()
.min(1, t('errors.validation.email.empty'))
.max(254, 'errors.validation.email.maxLengthExceeded')
.refine(validateEmailRfc6532Regex, t('errors.validation.email.invalid'))
.refine(validateEmailRfc6532Regex, t('errors.validation.email.invalid')),
hasTaxNumber: z.boolean(),
taxNumber: z.union([
z.undefined(),
z.string()
.refine(checkSpecialCharacters, t('errors.validation.taxNumber.specialCharacter'))
.refine(checkTaxNumberLength, t('errors.validation.taxNumber.invalidLength'))
.refine(validateTaxNumber, t('errors.validation.taxNumber.invalidNumber'))
])
})
const state = reactive({
email: undefined,
fullName: undefined,
preferredName: undefined
preferredName1: undefined,
hasTaxNumber: undefined,
taxNumber: undefined
})
// tax number input
const taxInfoModel = computed({
get () {
return {
hasTaxNumber: state.hasTaxNumber,
taxNumber: state.taxNumber
}
},
set (value) {
state.hasTaxNumber = value.hasTaxNumber
state.taxNumber = value.taxNumber
}
})
// tax residency
const isTaxResident: Ref<string | undefined> = ref(undefined)
</script>

<style scoped></style>
Loading

0 comments on commit 42ee4b5

Please sign in to comment.