diff --git a/src/api/BankValidationResource.ts b/src/api/BankValidationResource.ts new file mode 100644 index 000000000..e24591427 --- /dev/null +++ b/src/api/BankValidationResource.ts @@ -0,0 +1,64 @@ +import { BankAccountNumberRequest, BankAccountResponse, BankIbanRequest } from '@src/view_models/BankAccount'; +import axios from 'axios'; + +export interface BankValidationResource { + validateIban: ( data: BankIbanRequest ) => Promise; + validateBankNumber: ( data: BankAccountNumberRequest ) => Promise; +} + +export interface APIResponse { + status: string; + iban: string; + bic: string; + account: string; + bankCode: string; + bankName?: string; +} + +export class ApiBankValidationResource implements BankValidationResource { + validateIbanEndpoint: string; + validateBankNumberEndpoint: string; + + constructor( validateIbanEndpoint: string, validateBankNumberEndpoint: string ) { + this.validateIbanEndpoint = validateIbanEndpoint; + this.validateBankNumberEndpoint = validateBankNumberEndpoint; + } + + async validateIban( data: BankIbanRequest ): Promise { + return this.validateBankData( this.validateIbanEndpoint, data ).then( ( response: APIResponse ) => { + return { + accountNumber: response.account, + bankCode: response.bankCode, + bankName: response.bankName, + iban: response.iban, + bic: response.bic, + }; + } ); + } + + async validateBankNumber( data: BankAccountNumberRequest ): Promise { + return this.validateBankData( this.validateBankNumberEndpoint, data ).then( ( response: APIResponse ) => { + return { + accountNumber: response.account, + bankCode: response.bankCode, + bankName: response.bankName, + iban: response.iban, + bic: response.bic, + }; + } ); + } + + async validateBankData( endpoint: string, data: BankAccountNumberRequest | BankIbanRequest ): Promise { + let validationResult = await axios( endpoint, { + method: 'get', + headers: { 'Content-Type': 'multipart/form-data' }, + params: data, + } ); + + if ( validationResult.data.status === 'ERR' ) { + return Promise.reject( 'ERR' ); + } + + return Promise.resolve( validationResult.data ); + } +} diff --git a/src/components/pages/donation_form/AddressFormErrorSummaries.vue b/src/components/pages/donation_form/AddressFormErrorSummaries.vue index a8430eef9..31d3940ed 100644 --- a/src/components/pages/donation_form/AddressFormErrorSummaries.vue +++ b/src/components/pages/donation_form/AddressFormErrorSummaries.vue @@ -4,9 +4,9 @@ :is-visible="showErrorSummary" :items="[ { - validity: store.state.bankdata.validity.bankdata, + validity: store.getters[ 'bankdata/bankDataIsInvalid' ] ? Validity.INVALID : Validity.VALID, message: $t( 'donation_form_payment_bankdata_error' ), - focusElement: 'iban', + focusElement: 'account-number', scrollElement: 'iban-scroll-target' }, { @@ -70,9 +70,9 @@ :is-visible="showErrorSummary" :items="[ { - validity: store.state.bankdata.validity.bankdata, + validity: store.getters[ 'bankdata/bankDataIsInvalid' ] ? Validity.INVALID : Validity.VALID, message: $t( 'donation_form_payment_bankdata_error' ), - focusElement: 'iban', + focusElement: 'account-number', scrollElement: 'iban-scroll-target' }, { @@ -118,10 +118,10 @@ :is-visible="showErrorSummary" :items="[ { - validity: store.state.address.validity.salutation, - message: $t( 'donation_form_salutation_error' ), - focusElement: 'email-salutation-0', - scrollElement: 'email-salutation-scroll-target' + validity: store.getters[ 'bankdata/bankDataIsInvalid' ] ? Validity.INVALID : Validity.VALID, + message: $t( 'donation_form_payment_bankdata_error' ), + focusElement: 'account-number', + scrollElement: 'iban-scroll-target' }, { validity: store.state.address.validity.firstName, @@ -150,6 +150,7 @@ import ErrorSummary from '@src/components/shared/validation_summary/ErrorSummary.vue'; import { useStore } from 'vuex'; import { AddressTypeModel } from '@src/view_models/AddressTypeModel'; +import { Validity } from '@src/view_models/Validity'; interface Props { showErrorSummary: boolean; diff --git a/src/components/pages/donation_form/AddressForms.vue b/src/components/pages/donation_form/AddressForms.vue index 39bb09c85..43c678531 100644 --- a/src/components/pages/donation_form/AddressForms.vue +++ b/src/components/pages/donation_form/AddressForms.vue @@ -158,14 +158,13 @@ import { useAddressFunctions } from './AddressFunctions'; import { Salutation } from '@src/view_models/Salutation'; import { TrackingData } from '@src/view_models/TrackingData'; import { CampaignValues } from '@src/view_models/CampaignValues'; -import { StoreKey } from '@src/store/donation_store'; -import { injectStrict } from '@src/util/injectStrict'; import { AddressTypeIds } from '@src/components/pages/donation_form/AddressTypeIds'; import { Validity } from '@src/view_models/Validity'; import ValueEqualsPlaceholderWarning from '@src/components/shared/ValueEqualsPlaceholderWarning.vue'; import { useReceiptModel } from '@src/components/pages/donation_form/useReceiptModel'; import { useMailingListModel } from '@src/components/shared/form_fields/useMailingListModel'; import ScrollTarget from '@src/components/shared/ScrollTarget.vue'; +import { useStore } from 'vuex'; interface Props { countries: Country[], @@ -183,7 +182,7 @@ const props = withDefaults( defineProps(), { } ); const { addressType, isFullSelected, addressValidationPatterns } = toRefs( props ); -const store = injectStrict( StoreKey ); +const store = useStore(); const { formData, fieldErrors, diff --git a/src/components/pages/donation_form/DonationReceipt/AddressFields.vue b/src/components/pages/donation_form/DonationReceipt/AddressFields.vue index fafb2533b..f526340d6 100644 --- a/src/components/pages/donation_form/DonationReceipt/AddressFields.vue +++ b/src/components/pages/donation_form/DonationReceipt/AddressFields.vue @@ -107,7 +107,6 @@ import RadioField from '@src/components/shared/form_fields/RadioField.vue'; import { AddressTypeModel } from '@src/view_models/AddressTypeModel'; import { useStore } from 'vuex'; -import { StoreKey } from '@src/store/donation_store'; import { useAddressTypeModel } from '@src/components/pages/donation_form/DonationReceipt/useAddressTypeModel'; import { AddressFormData, AddressValidity } from '@src/view_models/Address'; import TextField from '@src/components/shared/form_fields/TextField.vue'; @@ -129,7 +128,7 @@ interface Props { const props = defineProps(); const emit = defineEmits( [ 'field-changed' ] ); -const store = useStore( StoreKey ); +const store = useStore(); const addressType = useAddressTypeModel( store ); const showStreetWarning = computed( () => /^\D+$/.test( props.formData.street.value ) ); diff --git a/src/components/pages/donation_form/DonationReceipt/AddressFormErrorSummaries.vue b/src/components/pages/donation_form/DonationReceipt/AddressFormErrorSummaries.vue index adbcd2032..94ab3b18e 100644 --- a/src/components/pages/donation_form/DonationReceipt/AddressFormErrorSummaries.vue +++ b/src/components/pages/donation_form/DonationReceipt/AddressFormErrorSummaries.vue @@ -4,9 +4,9 @@ :is-visible="showErrorSummary" :items="[ { - validity: store.state.bankdata.validity.bankdata, + validity: store.getters[ 'bankdata/bankDataIsInvalid' ] ? Validity.INVALID : Validity.VALID, message: $t( 'donation_form_payment_bankdata_error' ), - focusElement: 'iban', + focusElement: 'account-number', scrollElement: 'iban-scroll-target' }, { @@ -46,9 +46,9 @@ :is-visible="showErrorSummary" :items="[ { - validity: store.state.bankdata.validity.bankdata, + validity: store.getters[ 'bankdata/bankDataIsInvalid' ] ? Validity.INVALID : Validity.VALID, message: $t( 'donation_form_payment_bankdata_error' ), - focusElement: 'iban', + focusElement: 'account-number', scrollElement: 'iban-scroll-target' }, { @@ -112,9 +112,9 @@ :is-visible="showErrorSummary" :items="[ { - validity: store.state.bankdata.validity.bankdata, + validity: store.getters[ 'bankdata/bankDataIsInvalid' ] ? Validity.INVALID : Validity.VALID, message: $t( 'donation_form_payment_bankdata_error' ), - focusElement: 'iban', + focusElement: 'account-number', scrollElement: 'iban-scroll-target' }, { diff --git a/src/components/pages/donation_form/subpages/AddressPage.vue b/src/components/pages/donation_form/subpages/AddressPage.vue index c747852e7..802d3b2e3 100644 --- a/src/components/pages/donation_form/subpages/AddressPage.vue +++ b/src/components/pages/donation_form/subpages/AddressPage.vue @@ -7,7 +7,6 @@ ref="pageRef" >

{{ $t( 'donation_form_heading' ) }}

-

{{ $t( 'donation_form_address_subheading' ) }}

{{ $t( 'donation_form_section_address_tagline' ) }}

- - +

{{ $t( 'donation_form_payment_bankdata_title' ) }}

+ +

{{ $t( 'donation_form_address_subheading' ) }}

+

{{ $t( 'donation_form_heading' ) }}

-

{{ $t( 'donation_form_address_subheading' ) }}

{{ $t( 'donation_form_section_address_tagline' ) }}

+ +

{{ $t( 'donation_form_payment_bankdata_title' ) }}

+ + + +

{{ $t( 'donation_form_address_subheading' ) }}

+
- - (); const emit = defineEmits( [ 'previous-page' ] ); -const store = useStore( StoreKey ); +const store = useStore(); const { addressType, addressTypeName } = useAddressType( store ); const { addressSummary, inlineSummaryLanguageItem } = useAddressSummary( store ); diff --git a/src/components/pages/membership_form/SubmitValues.vue b/src/components/pages/membership_form/SubmitValues.vue index cb678a7fa..7a90c1cbb 100644 --- a/src/components/pages/membership_form/SubmitValues.vue +++ b/src/components/pages/membership_form/SubmitValues.vue @@ -6,8 +6,8 @@ - - + + diff --git a/src/components/shared/BankFields.vue b/src/components/shared/BankFields.vue new file mode 100644 index 000000000..c7a4a3647 --- /dev/null +++ b/src/components/shared/BankFields.vue @@ -0,0 +1,105 @@ + + + diff --git a/src/components/shared/PaymentBankData.vue b/src/components/shared/PaymentBankData.vue index 2130173fd..470fecdbf 100644 --- a/src/components/shared/PaymentBankData.vue +++ b/src/components/shared/PaymentBankData.vue @@ -9,7 +9,7 @@ - + +
- {{ getBankName }} - ({{ bankIdentifier }}) + {{ bankName }} ({{ bankIdentifier }}) @@ -63,8 +63,8 @@ export default defineComponent( { }, data: function (): BankAccountData { return { - accountId: this.$store.getters[ 'bankdata/getAccountId' ], - bankId: this.$store.getters[ 'bankdata/getBankId' ], + accountNumber: this.$store.getters[ 'bankdata/accountNumber' ], + bankCode: this.$store.getters[ 'bankdata/bankCode' ], }; }, props: { @@ -87,7 +87,7 @@ export default defineComponent( { if ( this.bankIdentifier !== '' ) { return true; } - if ( this.$store.getters[ 'bankdata/getBankId' ] !== '' ) { + if ( this.$store.getters[ 'bankdata/bankCode' ] !== '' ) { return true; } return false; @@ -95,7 +95,7 @@ export default defineComponent( { bankInfoValidatedButInfoMissing(): boolean { return this.$store.getters[ 'bankdata/bankDataIsValid' ] && this.bankIdentifier === '' && - this.$store.getters[ 'bankdata/getBankId' ] === ''; + this.$store.getters[ 'bankdata/bankCode' ] === ''; }, showBankId(): boolean { return this.bankIdentifier !== '' && this.looksLikeIban(); @@ -106,12 +106,12 @@ export default defineComponent( { bankIdentifier: { get: function (): string { if ( this.looksLikeGermanIban() ) { - return this.$store.getters[ 'bankdata/getBankId' ]; + return this.$store.getters[ 'bankdata/bankCode' ]; } - return this.$data.bankId; + return this.$data.bankCode; }, - set: function ( bankId: string ) { - this.$data.bankId = bankId; + set: function ( bankCode: string ) { + this.$data.bankCode = bankCode; }, }, labels() { @@ -136,7 +136,7 @@ export default defineComponent( { }, ...mapGetters( 'bankdata', [ 'bankDataIsInvalid', - 'getBankName', + 'bankName', ] ), }, methods: { @@ -154,7 +154,7 @@ export default defineComponent( { action( 'bankdata', 'setBankData' ), { validationUrl: this.validateBankDataUrl, - requestParams: { iban: this.$data.accountId.toUpperCase() }, + requestParams: { iban: this.$data.accountNumber.toUpperCase() }, } as BankAccountRequest ); } else { @@ -162,28 +162,28 @@ export default defineComponent( { action( 'bankdata', 'setBankData' ), { validationUrl: this.validateLegacyBankDataUrl, - requestParams: { accountNumber: this.$data.accountId, bankCode: this.$data.bankId }, + requestParams: { accountNumber: this.$data.accountNumber, bankCode: this.$data.bankCode }, } as BankAccountRequest ); } }, - setAccountId: function ( accountId: string ) { - this.$data.accountId = accountId; + setAccountId: function ( accountNumber: string ) { + this.$data.accountNumber = accountNumber; }, isAccountIdEmpty: function () { - return this.$data.accountId === ''; + return this.$data.accountNumber === ''; }, isBankIdEmpty: function () { return this.bankId === ''; }, looksLikeIban: function () { - return /^[A-Z]{2}[A-Z0-9\s]+$/i.test( this.$data.accountId ); + return /^[A-Z]{2}[A-Z0-9\s]+$/i.test( this.$data.accountNumber ); }, looksLikeBankAccountNumber: function () { - return /^\d+$/.test( this.$data.accountId ); + return /^\d+$/.test( this.$data.accountNumber ); }, looksLikeGermanIban() { - return /^DE[0-9\s]+$/i.test( this.$data.accountId ); + return /^DE[0-9\s]+$/i.test( this.$data.accountNumber ); }, looksLikeValidAccountNumber() { return this.looksLikeIban() || this.looksLikeBankAccountNumber(); diff --git a/src/components/shared/form_fields/DirectDebitField.vue b/src/components/shared/form_fields/DirectDebitField.vue new file mode 100644 index 000000000..f36ecc4ac --- /dev/null +++ b/src/components/shared/form_fields/DirectDebitField.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/components/shared/form_fields/TextValueField.vue b/src/components/shared/form_fields/TextValueField.vue new file mode 100644 index 000000000..7fc1896dc --- /dev/null +++ b/src/components/shared/form_fields/TextValueField.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/src/pages/donation_form.ts b/src/pages/donation_form.ts index 397675ea2..a56f55bd7 100644 --- a/src/pages/donation_form.ts +++ b/src/pages/donation_form.ts @@ -20,6 +20,7 @@ import App from '@src/components/App.vue'; import DonationForm from '@src/components/pages/DonationForm.vue'; import { ApiCityAutocompleteResource } from '@src/util/CityAutocompleteResource'; import { createFeatureFetcher } from '@src/util/FeatureFetcher'; +import { ApiBankValidationResource } from '@src/api/BankValidationResource'; interface DonationFormModel { initialFormValues: any, @@ -88,6 +89,10 @@ dataPersister.initialize( persistenceItems ).then( () => { }, } ); app.provide( 'cityAutocompleteResource', new ApiCityAutocompleteResource() ); + app.provide( 'bankValidationResource', new ApiBankValidationResource( + pageData.applicationVars.urls.validateIban, + pageData.applicationVars.urls.convertBankData + ) ); app.provide( StoreKey, store ); app.use( store ); app.mount( '#app' ); diff --git a/src/store/bankdata/actions.ts b/src/store/bankdata/actions.ts index 33b12a3d4..dee11ada7 100644 --- a/src/store/bankdata/actions.ts +++ b/src/store/bankdata/actions.ts @@ -1,50 +1,88 @@ import { ActionContext } from 'vuex'; import axios, { AxiosResponse } from 'axios'; -import { BankAccount, BankAccountData, BankAccountRequest, BankAccountResponse } from '@src/view_models/BankAccount'; +import { BankAccount, BankAccountData, BankAccountRequest } from '@src/view_models/BankAccount'; import { Validity } from '@src/view_models/Validity'; export const actions = { + initializeBankData( context: ActionContext, payload: BankAccountData ): void { + context.commit( 'SET_ACCOUNT_NUMBER', payload.accountNumber ); + context.commit( 'SET_BANK_CODE', payload.bankCode ); + context.commit( 'SET_BANK_NAME', payload.bankName ); + + if ( payload.accountNumber !== '' ) { + context.commit( 'SET_ACCOUNT_NUMBER_VALIDITY', Validity.RESTORED ); + } + + if ( payload.bankCode !== '' ) { + context.commit( 'SET_BANK_CODE_VALIDITY', Validity.RESTORED ); + } + }, setBankData( context: ActionContext, payload: BankAccountRequest ): Promise { context.commit( 'SET_IS_VALIDATING', true ); + return axios( payload.validationUrl, { method: 'get', headers: { 'Content-Type': 'multipart/form-data' }, params: payload.requestParams, - } ).then( ( validationResult: AxiosResponse ) => { - const validity = validationResult.data.status === 'ERR' ? Validity.INVALID : Validity.VALID; - context.commit( 'SET_BANK_DATA_VALIDITY', validity ); - if ( validity === Validity.VALID ) { - context.commit( 'SET_BANKNAME', validationResult.data.bankName ); - context.commit( 'SET_BANKDATA', { - accountId: validationResult.data.iban, - bankId: validationResult.data.bic, - } ); + } ).then( ( validationResult: AxiosResponse<{ + status: string; + iban: string; + bic: string; + account: string; + bankCode: string; + bankName?: string; + }> ) => { + if ( validationResult.data.status === 'OK' ) { + context.commit( 'SET_ACCOUNT_NUMBER_VALIDITY', Validity.VALID ); + context.commit( 'SET_BANK_CODE_VALIDITY', Validity.VALID ); + context.commit( 'SET_ACCOUNT_NUMBER', validationResult.data.account ); + context.commit( 'SET_BANK_CODE', validationResult.data.bankCode ); + context.commit( 'SET_BANK_NAME', validationResult.data.bankName ); + context.commit( 'SET_IBAN', validationResult.data.iban ); + context.commit( 'SET_BIC', validationResult.data.bic ); } else { - context.commit( 'SET_BANKNAME', '' ); + context.commit( 'SET_ACCOUNT_NUMBER_VALIDITY', Validity.INVALID ); + context.commit( 'SET_BANK_CODE_VALIDITY', Validity.INVALID ); + context.commit( 'SET_BANK_NAME', '' ); } context.commit( 'SET_IS_VALIDATING', false ); } ); }, - initializeBankData( context: ActionContext, payload: BankAccountData & { bankName: string} ): void { - if ( payload.accountId === '' ) { - return; - } - context.commit( 'SET_BANKDATA', { - accountId: payload.accountId, - bankId: payload.bankId, - } ); - context.commit( 'SET_BANKNAME', payload.bankName ); - context.commit( 'SET_BANK_DATA_VALIDITY', Validity.VALID ); + setValidating( context: ActionContext, payload: boolean ): void { + context.commit( 'SET_IS_VALIDATING', payload ); + }, + setAccountNumber( context: ActionContext, payload: string ): void { + context.commit( 'SET_ACCOUNT_NUMBER', payload ); + context.commit( 'VALIDATE_ACCOUNT_NUMBER' ); + }, + setBankCode( context: ActionContext, payload: string ): void { + context.commit( 'SET_BANK_CODE', payload ); + context.commit( 'VALIDATE_BANK_CODE' ); + }, + setBankName( context: ActionContext, payload: string ): void { + context.commit( 'SET_BANK_NAME', payload ); + }, + setIban( context: ActionContext, payload: string ): void { + context.commit( 'SET_IBAN', payload ); + }, + setBic( context: ActionContext, payload: string ): void { + context.commit( 'SET_BIC', payload ); }, markEmptyFieldsAsInvalid( context: ActionContext ): void { context.commit( 'MARK_EMPTY_FIELDS_INVALID' ); }, + setBankDataValidity( context: ActionContext, payload: Validity ): void { + context.commit( 'SET_ACCOUNT_NUMBER_VALIDITY', payload ); + context.commit( 'SET_BANK_CODE_VALIDITY', payload ); + }, markBankDataAsIncomplete( context: ActionContext ): void { - context.commit( 'MARK_BANKDATA_INCOMPLETE' ); - context.commit( 'SET_BANKNAME', '' ); + context.commit( 'SET_ACCOUNT_NUMBER_VALIDITY', Validity.INCOMPLETE ); + context.commit( 'SET_BANK_CODE_VALIDITY', Validity.INCOMPLETE ); + context.commit( 'SET_BANK_NAME', '' ); }, markBankDataAsInvalid( context: ActionContext ): void { - context.commit( 'SET_BANK_DATA_VALIDITY', Validity.INVALID ); - context.commit( 'SET_BANKNAME', '' ); + context.commit( 'SET_ACCOUNT_NUMBER_VALIDITY', Validity.INVALID ); + context.commit( 'SET_BANK_CODE_VALIDITY', Validity.INVALID ); + context.commit( 'SET_BANK_NAME', '' ); }, }; diff --git a/src/store/bankdata/getters.ts b/src/store/bankdata/getters.ts index 67eb6be87..e9f70ddaf 100644 --- a/src/store/bankdata/getters.ts +++ b/src/store/bankdata/getters.ts @@ -1,21 +1,39 @@ import { GetterTree } from 'vuex'; import { BankAccount } from '@src/view_models/BankAccount'; import { Validity } from '@src/view_models/Validity'; +import { looksLikeIban } from '@src/util/bank_account_number_helpers'; +// It's important to note here that !bankDataIsInvalid is not the same as bankDataIsValid and +// vice versa. This is because we have states Validity.INCOMPLETE and Validity.RESTORED. If +// the validity of one of the values is one of these states then both of these will return false. export const getters: GetterTree = { bankDataIsInvalid: function ( state: BankAccount ): boolean { - return state.validity.bankdata === Validity.INVALID; + if ( state.values.accountNumber === '' || looksLikeIban( state.values.accountNumber ) ) { + return state.validity.accountNumber === Validity.INVALID; + } + + return state.validity.accountNumber === Validity.INVALID || state.validity.bankCode === Validity.INVALID; }, bankDataIsValid: function ( state: BankAccount ): boolean { - return state.validity.bankdata === Validity.VALID; + if ( state.values.accountNumber === '' || looksLikeIban( state.values.accountNumber ) ) { + return state.validity.accountNumber === Validity.VALID; + } + + return state.validity.accountNumber === Validity.VALID && state.validity.bankCode === Validity.VALID; + }, + accountNumber: function ( state: BankAccount ): string { + return state.values.accountNumber; + }, + bankCode: function ( state: BankAccount ): string { + return state.values.bankCode; }, - getBankName: function ( state: BankAccount ): string { + bankName: function ( state: BankAccount ): string { return state.values.bankName; }, - getAccountId: function ( state: BankAccount ): string { + iban: function ( state: BankAccount ): string { return state.values.iban; }, - getBankId: function ( state: BankAccount ): string { + bic: function ( state: BankAccount ): string { return state.values.bic; }, }; diff --git a/src/store/bankdata/index.ts b/src/store/bankdata/index.ts index 1617d6062..c02b9d9a5 100644 --- a/src/store/bankdata/index.ts +++ b/src/store/bankdata/index.ts @@ -9,12 +9,15 @@ export default function (): Module { const state: BankAccount = { isValidating: false, validity: { - bankdata: Validity.INCOMPLETE, + accountNumber: Validity.INCOMPLETE, + bankCode: Validity.INCOMPLETE, }, values: { + accountNumber: '', + bankCode: '', + bankName: '', iban: '', bic: '', - bankName: '', }, }; diff --git a/src/store/bankdata/mutations.ts b/src/store/bankdata/mutations.ts index 9d50199b9..a63f4b4e9 100644 --- a/src/store/bankdata/mutations.ts +++ b/src/store/bankdata/mutations.ts @@ -1,27 +1,48 @@ import { MutationTree } from 'vuex'; import { BankAccount } from '@src/view_models/BankAccount'; import { Validity } from '@src/view_models/Validity'; +import { looksLikeIban } from '@src/util/bank_account_number_helpers'; export const mutations: MutationTree = { - SET_BANKDATA( state: BankAccount, bankData: any ) { - state.values.iban = bankData.accountId; - state.values.bic = bankData.bankId; + VALIDATE_ACCOUNT_NUMBER( state: BankAccount ) { + state.validity.accountNumber = state.values.accountNumber === '' ? Validity.INVALID : Validity.VALID; }, - SET_BANKNAME( state: BankAccount, bankName: string ) { + SET_ACCOUNT_NUMBER( state: BankAccount, accountNumber: string ) { + state.values.accountNumber = accountNumber; + }, + SET_BANK_CODE( state: BankAccount, bankCode: string ) { + state.values.bankCode = bankCode; + }, + VALIDATE_BANK_CODE( state: BankAccount ) { + state.validity.bankCode = state.values.bankCode === '' ? Validity.INVALID : Validity.VALID; + }, + SET_BANK_NAME( state: BankAccount, bankName: string ) { state.values.bankName = bankName; }, - SET_BANK_DATA_VALIDITY( state: BankAccount, validity: Validity ) { - state.validity.bankdata = validity; + SET_IBAN( state: BankAccount, iban: string ) { + state.values.iban = iban; + }, + SET_BIC( state: BankAccount, bic: string ) { + state.values.bic = bic; + }, + SET_ACCOUNT_NUMBER_VALIDITY( state: BankAccount, validity: Validity ) { + state.validity.accountNumber = validity; + }, + SET_BANK_CODE_VALIDITY( state: BankAccount, validity: Validity ) { + state.validity.bankCode = validity; }, SET_IS_VALIDATING( state: BankAccount, isValidating: boolean ) { state.isValidating = isValidating; }, MARK_EMPTY_FIELDS_INVALID( state: BankAccount ) { - if ( state.validity.bankdata === Validity.INCOMPLETE ) { - state.validity.bankdata = Validity.INVALID; + if ( [ Validity.INCOMPLETE, Validity.RESTORED ].includes( state.validity.accountNumber ) ) { + state.validity.accountNumber = Validity.INVALID; + } + + if ( looksLikeIban( state.values.accountNumber ) ) { + state.validity.bankCode = Validity.INCOMPLETE; + } else if ( [ Validity.INCOMPLETE, Validity.RESTORED ].includes( state.validity.bankCode ) ) { + state.validity.bankCode = Validity.INVALID; } - }, - MARK_BANKDATA_INCOMPLETE( state: BankAccount ) { - state.validity.bankdata = Validity.INCOMPLETE; }, }; diff --git a/src/store/dataInitializers.ts b/src/store/dataInitializers.ts index eb0244868..0e329dd1b 100644 --- a/src/store/dataInitializers.ts +++ b/src/store/dataInitializers.ts @@ -120,21 +120,21 @@ export const createInitialMembershipFeeValues = ( dataPersister: DataPersister, /** * Look for initial bank fields in initial form data */ -export const createInitialBankDataValues = ( initialFormValues: InitialBankAccountData|null ): BankAccountData & { bankName: string } => { +export const createInitialBankDataValues = ( initialFormValues: InitialBankAccountData|null ): BankAccountData => { - let iban = ''; - let bic = ''; - let bankname = ''; + let accountNumber = ''; + let bankCode = ''; + let bankName = ''; if ( initialFormValues ) { - iban = initialFormValues.iban || ''; - bic = initialFormValues.bic || ''; - bankname = initialFormValues.bankname || ''; + accountNumber = initialFormValues.accountNumber || ''; + bankCode = initialFormValues.bankCode || ''; + bankName = initialFormValues.bankname || ''; } return { - accountId: iban, - bankId: bic, - bankName: bankname, + accountNumber: accountNumber, + bankCode: bankCode, + bankName: bankName, }; }; diff --git a/src/util/bank_account_number_helpers.ts b/src/util/bank_account_number_helpers.ts new file mode 100644 index 000000000..734ba314c --- /dev/null +++ b/src/util/bank_account_number_helpers.ts @@ -0,0 +1,15 @@ +export function looksLikeIban( bankNumber: string ): boolean { + return /^[A-Z]{2}[A-Z0-9\s]+$/i.test( bankNumber ); +} + +export function looksLikeBankAccountNumber( bankNumber: string ): boolean { + return /^\d+$/.test( bankNumber ); +} + +export function looksLikeGermanIban( bankNumber: string ): boolean { + return /^DE[0-9\s]+$/i.test( bankNumber ); +} + +export function looksLikeValidAccountNumber( bankNumber: string ): boolean { + return looksLikeIban( bankNumber ) || looksLikeBankAccountNumber( bankNumber ); +} diff --git a/src/view_models/BankAccount.ts b/src/view_models/BankAccount.ts index fd2329820..d858288f6 100644 --- a/src/view_models/BankAccount.ts +++ b/src/view_models/BankAccount.ts @@ -1,31 +1,56 @@ import { Validity } from './Validity'; export interface BankAccount { - isValidating: boolean, + isValidating: boolean; validity: { - [key: string]: Validity - }, + accountNumber: Validity; + bankCode: Validity; + }; values: { - [key: string]: string - } + accountNumber: string; + bankCode: string; + bankName: string; + iban: string; + bic: string; + }; +} + +export enum AccountNumberType { + None, + IBAN, + Account } export interface BankAccountData { - accountId: string - bankId: string + accountNumber: string; + bankCode: string; + bankName?: string; } export interface InitialBankAccountData { - iban?: string - bic?: string - bankname?: string + accountNumber?: string; + bankCode?: string; + bankname?: string; } export interface BankAccountRequest { - validationUrl: string - requestParams: object + validationUrl: string; + requestParams: object; +} + +export interface BankAccountNumberRequest { + accountNumber: string; + bankCode: string; +} + +export interface BankIbanRequest { + iban: string; } export interface BankAccountResponse { - [key: string]: string + iban: string; + bic: string; + accountNumber: string; + bankCode: string; + bankName?: string; } diff --git a/tests/unit/TestDoubles/FakeBankValidationResource.ts b/tests/unit/TestDoubles/FakeBankValidationResource.ts new file mode 100644 index 000000000..1afdc15ac --- /dev/null +++ b/tests/unit/TestDoubles/FakeBankValidationResource.ts @@ -0,0 +1,24 @@ +import { BankValidationResource } from '@src/api/BankValidationResource'; +import { BankAccountResponse } from '@src/view_models/BankAccount'; + +export class FakeBankValidationResource implements BankValidationResource { + async validateIban(): Promise { + return { + accountNumber: '', + bankCode: '', + bankName: '', + iban: '', + bic: '', + }; + } + + async validateBankNumber(): Promise { + return { + accountNumber: '', + bankCode: '', + bankName: '', + iban: '', + bic: '', + }; + } +} diff --git a/tests/unit/components/pages/donation_form/AddressPage.spec.ts b/tests/unit/components/pages/donation_form/AddressPage.spec.ts index e2083d9ec..eaed44ed4 100644 --- a/tests/unit/components/pages/donation_form/AddressPage.spec.ts +++ b/tests/unit/components/pages/donation_form/AddressPage.spec.ts @@ -2,9 +2,8 @@ import { flushPromises, mount, VueWrapper } from '@vue/test-utils'; import axios from 'axios'; import AddressPage from '@src/components/pages/donation_form/subpages/AddressPage.vue'; -import { createStore, StoreKey } from '@src/store/donation_store'; +import { createStore } from '@src/store/donation_store'; import { action } from '@src/store/util'; -import PaymentBankData from '@src/components/shared/PaymentBankData.vue'; import { AddressTypeModel } from '@src/view_models/AddressTypeModel'; import { createFeatureToggle } from '@src/util/createFeatureToggle'; import { Store } from 'vuex'; @@ -15,6 +14,8 @@ import { nextTick } from 'vue'; import AddressTypeBasic from '@src/components/pages/donation_form/AddressTypeBasic.vue'; import { Validity } from '@src/view_models/Validity'; import { Salutation } from '@src/view_models/Salutation'; +import { FakeBankValidationResource } from '@test/unit/TestDoubles/FakeBankValidationResource'; +import BankFields from '@src/components/shared/BankFields.vue'; const testCountry = { countryCode: 'de', @@ -60,7 +61,7 @@ describe( 'AddressPage.vue', () => { Address: true, }, provide: { - [ StoreKey as symbol ]: store, + bankValidationResource: new FakeBankValidationResource(), }, components: { FeatureToggle: createFeatureToggle( [ 'campaigns.address_type_steps.preselect' ] ), @@ -87,11 +88,11 @@ describe( 'AddressPage.vue', () => { it( 'shows bank data fields if payment type is direct debit', async () => { const { wrapper, store } = getWrapper(); - expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeFalsy(); + expect( wrapper.findComponent( BankFields ).exists() ).toBeFalsy(); await setPaymentType( store, 'BEZ' ); - expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeTruthy(); + expect( wrapper.findComponent( BankFields ).exists() ).toBeTruthy(); } ); it( 'hides bank data fields if payment type is not direct debit', async () => { @@ -99,11 +100,11 @@ describe( 'AddressPage.vue', () => { await setPaymentType( store, 'BEZ' ); - expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeTruthy(); + expect( wrapper.findComponent( BankFields ).exists() ).toBeTruthy(); await setPaymentType( store, 'UEB' ); - expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeFalsy(); + expect( wrapper.findComponent( BankFields ).exists() ).toBeFalsy(); } ); it( 'sets address type in store when it receives address-type event', () => { diff --git a/tests/unit/components/pages/donation_form/AddressPageDonationReceipt.spec.ts b/tests/unit/components/pages/donation_form/AddressPageDonationReceipt.spec.ts index 529d25abf..5f2c4cc39 100644 --- a/tests/unit/components/pages/donation_form/AddressPageDonationReceipt.spec.ts +++ b/tests/unit/components/pages/donation_form/AddressPageDonationReceipt.spec.ts @@ -1,9 +1,8 @@ import { flushPromises, mount, VueWrapper } from '@vue/test-utils'; import axios from 'axios'; -import { createStore, StoreKey } from '@src/store/donation_store'; +import { createStore } from '@src/store/donation_store'; import { action } from '@src/store/util'; -import PaymentBankData from '@src/components/shared/PaymentBankData.vue'; import { AddressTypeModel } from '@src/view_models/AddressTypeModel'; import { createFeatureToggle } from '@src/util/createFeatureToggle'; import { Store } from 'vuex'; @@ -14,6 +13,8 @@ import { nextTick } from 'vue'; import { Validity } from '@src/view_models/Validity'; import { Salutation } from '@src/view_models/Salutation'; import AddressPageDonationReceipt from '@src/components/pages/donation_form/subpages/AddressPageDonationReceipt.vue'; +import BankFields from '@src/components/shared/BankFields.vue'; +import { FakeBankValidationResource } from '@test/unit/TestDoubles/FakeBankValidationResource'; const testCountry = { countryCode: 'de', @@ -59,7 +60,7 @@ describe( 'AddressPageDonationReceipt.vue', () => { Address: true, }, provide: { - [ StoreKey as symbol ]: store, + bankValidationResource: new FakeBankValidationResource(), }, components: { FeatureToggle: createFeatureToggle( [ 'campaigns.address_type_steps.preselect' ] ), @@ -86,11 +87,11 @@ describe( 'AddressPageDonationReceipt.vue', () => { it( 'shows bank data fields if payment type is direct debit', async () => { const { wrapper, store } = getWrapper(); - expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeFalsy(); + expect( wrapper.findComponent( BankFields ).exists() ).toBeFalsy(); await setPaymentType( store, 'BEZ' ); - expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeTruthy(); + expect( wrapper.findComponent( BankFields ).exists() ).toBeTruthy(); } ); it( 'hides bank data fields if payment type is not direct debit', async () => { @@ -98,11 +99,11 @@ describe( 'AddressPageDonationReceipt.vue', () => { await setPaymentType( store, 'BEZ' ); - expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeTruthy(); + expect( wrapper.findComponent( BankFields ).exists() ).toBeTruthy(); await setPaymentType( store, 'UEB' ); - expect( wrapper.findComponent( PaymentBankData ).exists() ).toBeFalsy(); + expect( wrapper.findComponent( BankFields ).exists() ).toBeFalsy(); } ); it( 'emits previous event', async () => { diff --git a/tests/unit/components/pages/membership_form/__snapshots__/SubmitValues.spec.ts.snap b/tests/unit/components/pages/membership_form/__snapshots__/SubmitValues.spec.ts.snap index dcd2509a2..8102b7b3b 100644 --- a/tests/unit/components/pages/membership_form/__snapshots__/SubmitValues.spec.ts.snap +++ b/tests/unit/components/pages/membership_form/__snapshots__/SubmitValues.spec.ts.snap @@ -27,12 +27,10 @@ exports[`SubmitValues.vue renders input fields 1`] = ` { store.dispatch = jest.fn(); const iban = wrapper.find( '#iban' ); - wrapper.setData( { accountId: 'DE12345605171238489890' } ); + wrapper.setData( { accountNumber: 'DE12345605171238489890' } ); iban.trigger( 'blur' ); const expectedAction = action( 'bankdata', 'setBankData' ); const expectedPayload = { @@ -46,7 +46,7 @@ describe( 'BankData.vue', () => { store.dispatch = jest.fn(); const iban = wrapper.find( '#iban' ); - wrapper.setData( { accountId: 'NL18ABNA0484869868 ' } ); + wrapper.setData( { accountNumber: 'NL18ABNA0484869868 ' } ); iban.trigger( 'blur' ); const expectedAction = action( 'bankdata', 'setBankData' ); const expectedPayload = { @@ -62,10 +62,10 @@ describe( 'BankData.vue', () => { store.dispatch = jest.fn(); const iban = wrapper.find( '#iban' ); - wrapper.setData( { accountId: '34560517' } ); + wrapper.setData( { accountNumber: '34560517' } ); iban.trigger( 'blur' ); const bic = wrapper.find( '#bic' ); - wrapper.setData( { bankId: '50010517' } ); + wrapper.setData( { bankCode: '50010517' } ); bic.trigger( 'blur' ); const expectedAction = action( 'bankdata', 'setBankData' ); const expectedPayload = { @@ -81,7 +81,7 @@ describe( 'BankData.vue', () => { store.dispatch = jest.fn(); const iban = wrapper.find( '#iban' ); - wrapper.setData( { accountId: 'DE123456051äh?' } ); + wrapper.setData( { accountNumber: 'DE123456051äh?' } ); iban.trigger( 'blur' ); const expectedAction = action( 'bankdata', 'markBankDataAsInvalid' ); @@ -94,10 +94,10 @@ describe( 'BankData.vue', () => { const iban = wrapper.find( '#iban' ); - wrapper.setData( { accountId: 'DE12345605171238489890' } ); + wrapper.setData( { accountNumber: 'DE12345605171238489890' } ); iban.trigger( 'blur' ); - wrapper.setData( { accountId: '' } ); + wrapper.setData( { accountNumber: '' } ); iban.trigger( 'blur' ); const expectedAction = action( 'bankdata', 'markBankDataAsIncomplete' ); @@ -110,7 +110,7 @@ describe( 'BankData.vue', () => { const iban = wrapper.find( '#iban' ); const bic = wrapper.find( '#bic' ); - await wrapper.setData( { accountId: 'AT12345605171238489890', bankId: 'ABCDDEFFXXX' } ); + await wrapper.setData( { accountNumber: 'AT12345605171238489890', bankCode: 'ABCDDEFFXXX' } ); expect( ( ( iban.element ).value ) ).toMatch( 'AT12 3456 0517 1238 4898 90' ); expect( ( ( bic.element ).value ) ).toMatch( 'ABCDDEFFXXX' ); @@ -119,13 +119,13 @@ describe( 'BankData.vue', () => { it( 'renders the bank name set in the store', async () => { const { wrapper, store } = getWrapper(); - store.commit( 'bankdata/SET_BANKNAME', 'Test Bank' ); + store.commit( 'bankdata/SET_BANK_NAME', 'Test Bank' ); await nextTick(); expect( wrapper.find( '#bank-name-iban' ).text() ).toMatch( 'Test Bank' ); } ); - it( 'renders info message when bank info is valid but no bankname and bankId available', async () => { + it( 'renders info message when bank info is valid but no bank name and bank code available', async () => { const { wrapper } = getWrapper( new Vuex.Store( { modules: { [ 'bankdata' ]: { @@ -133,11 +133,12 @@ describe( 'BankData.vue', () => { getters: { bankDataIsValid: () => true, bankDataIsInvalid: () => false, - getBankName: () => '', - getBankId: () => '', - getAccountId: () => '', + accountNumber: () => '', + bankCode: () => '', + bankName: () => '', }, actions: { + markBankDataAsIncomplete: () => {}, setBankData: () => {}, }, }, @@ -156,7 +157,7 @@ describe( 'BankData.vue', () => { expect( wrapper.find( 'input#bic' ).isVisible() ).toBe( false ); - await wrapper.setData( { accountId: '123' } ); + await wrapper.setData( { accountNumber: '123' } ); expect( wrapper.find( 'input#bic' ).isVisible() ).toBe( true ); } ); @@ -169,9 +170,9 @@ describe( 'BankData.vue', () => { getters: { bankDataIsValid: () => true, bankDataIsInvalid: () => false, - getBankName: () => 'gute Bank', - getAccountId: () => '', - getBankId: (): string => '', + accountNumber: () => '', + bankCode: (): string => '', + bankName: () => 'gute Bank', }, }, }, @@ -190,9 +191,9 @@ describe( 'BankData.vue', () => { getters: { bankDataIsValid: () => false, bankDataIsInvalid: () => true, - getBankName: () => '', - getAccountId: () => '', - getBankId: (): string => '', + accountNumber: () => '', + bankCode: (): string => '', + bankName: () => '', }, }, }, @@ -215,7 +216,7 @@ describe( 'BankData.vue', () => { it( 'renders the appropriate labels for IBANs', async () => { const { wrapper } = getWrapper(); - await wrapper.setData( { accountId: 'DE12345605171238489890', bankId: 'ABCDDEFFXXX' } ); + await wrapper.setData( { accountNumber: 'DE12345605171238489890', bankCode: 'ABCDDEFFXXX' } ); const bankDataLabels = wrapper.findAll( 'label' ); expect( bankDataLabels.at( 0 ).text() ).toMatch( 'donation_form_payment_bankdata_account_iban_label' ); @@ -225,7 +226,7 @@ describe( 'BankData.vue', () => { it( 'renders the appropriate labels for legacy bank accounts', async () => { const { wrapper } = getWrapper(); - await wrapper.setData( { accountId: '34560517', bankId: '50010517' } ); + await wrapper.setData( { accountNumber: '34560517', bankCode: '50010517' } ); const bankDataLabels = wrapper.findAll( 'label' ); expect( bankDataLabels.at( 0 ).text() ).toMatch( 'donation_form_payment_bankdata_account_legacy_label' ); @@ -235,8 +236,8 @@ describe( 'BankData.vue', () => { it( 'puts initial values form the store in the fields', async () => { const store = createStore(); await store.dispatch( action( 'bankdata', 'initializeBankData' ), { - accountId: 'DE12345605171238489890', - bankId: 'ABCDDEFFXXX', + accountNumber: 'DE12345605171238489890', + bankCode: 'ABCDDEFFXXX', bankName: 'Cool Bank', } ); diff --git a/tests/unit/components/shared/BankFields.spec.ts b/tests/unit/components/shared/BankFields.spec.ts new file mode 100644 index 000000000..90e8ef077 --- /dev/null +++ b/tests/unit/components/shared/BankFields.spec.ts @@ -0,0 +1,211 @@ +import { flushPromises, mount, VueWrapper } from '@vue/test-utils'; +import BankFields from '@src/components/shared/BankFields.vue'; +import { createStore } from '@src/store/donation_store'; +import { BankValidationResource } from '@src/api/BankValidationResource'; +import DirectDebitField from '@src/components/shared/form_fields/DirectDebitField.vue'; +import { BankAccountResponse } from '@src/view_models/BankAccount'; +import { Store } from 'vuex'; +import { action } from '@src/store/util'; +import { nextTick } from 'vue'; + +const IBAN = 'DE12500105170648489890'; +const BIC = 'INGDDEFFXXX'; +const bankAccountNumber = '0648489890'; +const bankCode = '50010517'; +const bankName = 'ING-DiBa'; + +describe( 'BankFields.vue', () => { + + let store: Store; + + const succeedingBankValidationResource = ( apiReturnValue: BankAccountResponse = null ): BankValidationResource => { + const returnValue: BankAccountResponse = apiReturnValue ?? { + accountNumber: IBAN, + bankCode, + bankName, + iban: IBAN, + bic: BIC, + }; + return { + validateBankNumber: jest.fn().mockReturnValue( returnValue ), + validateIban: jest.fn().mockReturnValue( returnValue ), + }; + }; + + const failingBankValidationResource = (): BankValidationResource => { + return { + validateBankNumber: jest.fn().mockRejectedValue( 'ERR' ), + validateIban: jest.fn().mockRejectedValue( 'ERR' ), + }; + }; + + const getWrapper = ( bankValidationResource: BankValidationResource = null ): VueWrapper => { + store = createStore(); + const validationResource = bankValidationResource ?? succeedingBankValidationResource(); + + return mount( BankFields, { + global: { + plugins: [ store ], + provide: { + bankValidationResource: validationResource, + }, + }, + } ); + }; + + it( 'sets IBAN fields validity', async () => { + const wrapper = getWrapper(); + + await wrapper.find( '#account-number' ).setValue( IBAN ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'account-number' ); + + await nextTick(); + expect( store.getters[ 'bankdata/bankDataIsValid' ] ).toBeTruthy(); + + await wrapper.find( '#account-number' ).setValue( '' ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'account-number' ); + + await nextTick(); + expect( store.getters[ 'bankdata/bankDataIsValid' ] ).toBeFalsy(); + } ); + + it( 'sets account number fields validity', async () => { + const wrapper = getWrapper(); + + await wrapper.find( '#account-number' ).setValue( bankAccountNumber ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'account-number' ); + await nextTick(); + await wrapper.find( '#bank-code' ).setValue( bankCode ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'bank-code' ); + + await nextTick(); + expect( store.getters[ 'bankdata/bankDataIsValid' ] ).toBeTruthy(); + + await wrapper.find( '#bank-code' ).setValue( '' ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'bank-code' ); + + await nextTick(); + expect( store.getters[ 'bankdata/bankDataIsValid' ] ).toBeFalsy(); + } ); + + it( 'checks with the server for a valid IBAN', async () => { + const resource = succeedingBankValidationResource(); + const wrapper = getWrapper( resource ); + + await wrapper.find( '#account-number' ).setValue( IBAN ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'account-number' ); + + await flushPromises(); + + expect( resource.validateIban ).toHaveBeenCalledWith( { iban: IBAN } ); + expect( store.getters[ 'bankdata/accountNumber' ] ).toStrictEqual( IBAN ); + expect( store.getters[ 'bankdata/bankCode' ] ).toStrictEqual( '' ); + expect( store.getters[ 'bankdata/bankName' ] ).toStrictEqual( bankName ); + expect( store.getters[ 'bankdata/iban' ] ).toStrictEqual( IBAN ); + expect( store.getters[ 'bankdata/bic' ] ).toStrictEqual( BIC ); + expect( store.getters[ 'bankdata/bankDataIsValid' ] ).toBeTruthy(); + } ); + + it( 'checks with the server for a valid Account Number', async () => { + const resource = succeedingBankValidationResource( { + accountNumber: bankAccountNumber, + bankCode, + bankName, + iban: IBAN, + bic: BIC, + } ); + const wrapper = getWrapper( resource ); + + await wrapper.find( '#account-number' ).setValue( bankAccountNumber ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'account-number' ); + + await nextTick(); + + await wrapper.find( '#bank-code' ).setValue( bankCode ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'bank-code' ); + + await flushPromises(); + + expect( resource.validateBankNumber ).toHaveBeenCalledWith( { accountNumber: bankAccountNumber, bankCode: bankCode } ); + expect( store.getters[ 'bankdata/accountNumber' ] ).toStrictEqual( bankAccountNumber ); + expect( store.getters[ 'bankdata/bankCode' ] ).toStrictEqual( bankCode ); + expect( store.getters[ 'bankdata/bankName' ] ).toStrictEqual( bankName ); + expect( store.getters[ 'bankdata/iban' ] ).toStrictEqual( IBAN ); + expect( store.getters[ 'bankdata/bic' ] ).toStrictEqual( BIC ); + expect( store.getters[ 'bankdata/bankDataIsValid' ] ).toBeTruthy(); + } ); + + it( 'does not check server when IBAN is invalid', async () => { + const resource = succeedingBankValidationResource(); + const wrapper = getWrapper( resource ); + + await wrapper.find( '#account-number' ).setValue( '$ invalid IBAN $' ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'account-number' ); + + await flushPromises(); + + expect( resource.validateIban ).not.toHaveBeenCalled(); + } ); + + it( 'does not check server when account number field is empty', async () => { + const resource = succeedingBankValidationResource(); + const wrapper = getWrapper( resource ); + + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'account-number' ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'bank-code' ); + + await flushPromises(); + + expect( resource.validateBankNumber ).not.toHaveBeenCalled(); + } ); + + it( 'does not check server when bank code field is empty', async () => { + const resource = succeedingBankValidationResource(); + const wrapper = getWrapper( resource ); + + await wrapper.find( '#account-number' ).setValue( bankAccountNumber ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'account-number' ); + + await flushPromises(); + + expect( resource.validateBankNumber ).not.toHaveBeenCalled(); + } ); + + it( 'handles server IBAN error response', async () => { + const resource = failingBankValidationResource(); + const wrapper = getWrapper( resource ); + + await store.dispatch( action( 'bankdata', 'setBankName' ), bankName ); + await store.dispatch( action( 'bankdata', 'setIban' ), IBAN ); + await store.dispatch( action( 'bankdata', 'setBic' ), BIC ); + + await wrapper.find( '#account-number' ).setValue( IBAN ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'account-number' ); + + await flushPromises(); + + expect( store.getters[ 'bankdata/bankDataIsValid' ] ).toBeFalsy(); + expect( store.getters[ 'bankdata/bankName' ] ).toStrictEqual( '' ); + expect( store.getters[ 'bankdata/iban' ] ).toStrictEqual( '' ); + expect( store.getters[ 'bankdata/bic' ] ).toStrictEqual( '' ); + } ); + + it( 'handles server account number error response', async () => { + const resource = failingBankValidationResource(); + const wrapper = getWrapper( resource ); + await store.dispatch( action( 'bankdata', 'setBankName' ), bankName ); + + await wrapper.find( '#account-number' ).setValue( bankAccountNumber ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'account-number' ); + + await nextTick(); + + await wrapper.find( '#bank-code' ).setValue( bankCode ); + wrapper.findComponent( DirectDebitField ).vm.$emit( 'field-changed', 'bank-code' ); + + await flushPromises(); + + expect( store.getters[ 'bankdata/bankDataIsValid' ] ).toBeFalsy(); + expect( store.getters[ 'bankdata/bankName' ] ).toStrictEqual( '' ); + } ); +} ); diff --git a/tests/unit/components/shared/form_fields/DirectDebitField.spec.ts b/tests/unit/components/shared/form_fields/DirectDebitField.spec.ts new file mode 100644 index 000000000..d9716fa1e --- /dev/null +++ b/tests/unit/components/shared/form_fields/DirectDebitField.spec.ts @@ -0,0 +1,144 @@ +import { mount, VueWrapper } from '@vue/test-utils'; +import DirectDebitField from '@src/components/shared/form_fields/DirectDebitField.vue'; +import { FakeBankValidationResource } from '@test/unit/TestDoubles/FakeBankValidationResource'; +import TextValueField from '@src/components/shared/form_fields/TextValueField.vue'; +import { nextTick } from 'vue'; +import { AccountNumberType } from '@src/view_models/BankAccount'; + +const IBAN = 'DE12500105170648489890'; +const bankAccountNumber = '0648489890'; +const bankCode = '50010517'; +const bankName = 'ING-DiBa'; + +describe( 'DirectDebitField.vue', () => { + + const getWrapper = (): VueWrapper => { + return mount( DirectDebitField, { + props: { + accountNumber: '', + bankCode: '', + bankName: '', + bic: '', + accountNumberType: AccountNumberType.None, + showAccountNumberError: false, + showBankCodeError: false, + bankValidationResource: new FakeBankValidationResource(), + }, + } ); + }; + + it( 'sets correct account number labels', async () => { + const wrapper = getWrapper(); + + expect( wrapper.find( 'label[for="account-number"]' ).text() ).toStrictEqual( 'donation_form_payment_bankdata_account_default_label' ); + + await wrapper.setProps( { accountNumberType: AccountNumberType.IBAN } ); + + expect( wrapper.find( 'label[for="account-number"]' ).text() ).toStrictEqual( 'donation_form_payment_bankdata_account_iban_label' ); + + await wrapper.setProps( { accountNumberType: AccountNumberType.Account } ); + + expect( wrapper.find( 'label[for="account-number"]' ).text() ).toStrictEqual( 'donation_form_payment_bankdata_account_legacy_label' ); + } ); + + it( 'shows the bank code field', async () => { + const wrapper = getWrapper(); + + await wrapper.setProps( { accountNumberType: AccountNumberType.IBAN } ); + + expect( wrapper.find( '#bank-code' ).exists() ).toBeFalsy(); + + await wrapper.setProps( { accountNumberType: AccountNumberType.Account } ); + + expect( wrapper.find( '#bank-code' ).exists() ).toBeTruthy(); + } ); + + it( 'shows the IBAN bank name', async () => { + const wrapper = getWrapper(); + + await wrapper.setProps( { bankName: bankName, bic: bankCode, accountNumberType: AccountNumberType.IBAN } ); + + expect( wrapper.find( '.iban-bank-name' ).text() ).toContain( bankName ); + expect( wrapper.find( '.iban-bank-name' ).text() ).toContain( bankCode ); + } ); + + it( 'shows the account number bank name', async () => { + const wrapper = getWrapper(); + + await wrapper.setProps( { accountNumberType: AccountNumberType.Account } ); + await wrapper.setProps( { bankName: bankName } ); + + expect( wrapper.find( '.bank-name' ).text() ).toStrictEqual( bankName ); + } ); + + it( 'updates the account number value when it is changed externally', async () => { + const wrapper = getWrapper(); + + expect( wrapper.find( '#account-number' ).element.value ).toStrictEqual( '' ); + + await wrapper.setProps( { accountNumber: IBAN } ); + + expect( wrapper.find( '#account-number' ).element.value ).toStrictEqual( 'DE12 5001 0517 0648 4898 90' ); + } ); + + it( 'updates the bank code value when it is changed externally', async () => { + const wrapper = getWrapper(); + + await wrapper.setProps( { accountNumberType: AccountNumberType.Account } ); + + expect( wrapper.find( '#bank-code' ).element.value ).toStrictEqual( '' ); + + await wrapper.setProps( { bankCode: bankCode } ); + + expect( wrapper.find( '#bank-code' ).element.value ).toStrictEqual( bankCode ); + } ); + + it( 'resets the cursor and value when the account number field is edited', async () => { + const wrapper = getWrapper(); + + await wrapper.find( '#account-number' ).setValue( IBAN ); + + expect( wrapper.find( '#account-number' ).element.selectionStart ).toStrictEqual( 27 ); + expect( wrapper.find( '#account-number' ).element.selectionEnd ).toStrictEqual( 27 ); + + wrapper.findComponent( TextValueField ).vm.$emit( 'input', 'DE12 5001 0517R 0648 4898 90', 14 ); + + await nextTick(); + await nextTick(); + await nextTick(); + + expect( wrapper.find( '#account-number' ).element.value ).toStrictEqual( 'DE12 5001 0517 R064 8489 890' ); + expect( wrapper.find( '#account-number' ).element.selectionStart ).toStrictEqual( 15 ); + expect( wrapper.find( '#account-number' ).element.selectionEnd ).toStrictEqual( 15 ); + + wrapper.findComponent( TextValueField ).vm.$emit( 'input', 'DE12 5001 0517 RG064 8489 890', 17 ); + + await nextTick(); + await nextTick(); + await nextTick(); + + expect( wrapper.find( '#account-number' ).element.value ).toStrictEqual( 'DE12 5001 0517 RG06 4848 9890' ); + expect( wrapper.find( '#account-number' ).element.selectionStart ).toStrictEqual( 17 ); + expect( wrapper.find( '#account-number' ).element.selectionEnd ).toStrictEqual( 17 ); + } ); + + it( 'emits field changed events', async () => { + const wrapper = getWrapper(); + + await wrapper.find( '#account-number' ).setValue( IBAN ); + await wrapper.find( '#account-number' ).trigger( 'blur' ); + + await wrapper.setProps( { accountNumberType: AccountNumberType.Account } ); + + await wrapper.find( '#account-number' ).setValue( bankAccountNumber ); + await wrapper.find( '#account-number' ).trigger( 'blur' ); + + await wrapper.find( '#bank-code' ).setValue( bankCode ); + await wrapper.find( '#bank-code' ).trigger( 'blur' ); + + expect( wrapper.emitted( 'field-changed' ).length ).toStrictEqual( 3 ); + expect( wrapper.emitted( 'field-changed' )[ 0 ][ 0 ] ).toStrictEqual( 'account-number' ); + expect( wrapper.emitted( 'field-changed' )[ 1 ][ 0 ] ).toStrictEqual( 'account-number' ); + expect( wrapper.emitted( 'field-changed' )[ 2 ][ 0 ] ).toStrictEqual( 'bank-code' ); + } ); +} ); diff --git a/tests/unit/components/shared/form_fields/TextValueField.spec.ts b/tests/unit/components/shared/form_fields/TextValueField.spec.ts new file mode 100644 index 000000000..bd5585342 --- /dev/null +++ b/tests/unit/components/shared/form_fields/TextValueField.spec.ts @@ -0,0 +1,101 @@ +import { mount, VueWrapper } from '@vue/test-utils'; +import TextValueField from '@src/components/shared/form_fields/TextValueField.vue'; +import { nextTick } from 'vue'; + +describe( 'TextValueField.vue', () => { + const getWrapper = ():VueWrapper => { + return mount( TextValueField, { + props: { + label: 'textField', + name: 'textField', + inputId: 'textField', + inputType: 'text', + placeholder: 'textField', + value: 'Garfield', + errorMessage: '404 Lasagne not found', + showError: false, + disabled: false, + required: false, + autocomplete: 'on', + }, + slots: { + message: `I hate Mondays`, + }, + } ); + }; + + it( 'shows the error message', async () => { + const wrapper = getWrapper(); + + await wrapper.setProps( { showError: true } ); + + expect( wrapper.find( 'span.help.is-danger' ).exists() ).toBeTruthy(); + expect( wrapper.find( 'span.help.is-danger' ).text() ).toStrictEqual( '404 Lasagne not found' ); + expect( wrapper.find( 'input' ).attributes( 'aria-describedby' ) ).toStrictEqual( 'textField-error' ); + } ); + + it( 'shows the message slot', async () => { + const wrapper = getWrapper(); + + expect( wrapper.find( '.test-message' ).exists() ).toBeTruthy(); + expect( wrapper.find( '.test-message' ).text() ).toStrictEqual( 'I hate Mondays' ); + } ); + + it( 'updates the value when changed externally', async () => { + const wrapper = getWrapper(); + const input = wrapper.find( 'input' ); + + expect( input.element.value ).toStrictEqual( 'Garfield' ); + + await wrapper.setProps( { value: 'Odie' } ); + + expect( input.element.value ).toStrictEqual( 'Odie' ); + } ); + + it( 'updates the cursor position when changed externally', async () => { + const wrapper = getWrapper(); + + expect( wrapper.find( 'input' ).element.selectionStart ).toStrictEqual( 8 ); + expect( wrapper.find( 'input' ).element.selectionEnd ).toStrictEqual( 8 ); + + // Set a long string to play with + await wrapper.setProps( { value: 'Barmy Bazs Buttery Nubs! Barmy Bazs Buttery Nubs! Barmy Bazs Buttery Nubs!' } ); + await wrapper.setProps( { cursorPosition: 42 } ); + + // The test browser takes a sec to update the values so we need to wait + await nextTick(); + await nextTick(); + await nextTick(); + + expect( wrapper.find( 'input' ).element.selectionStart ).toStrictEqual( 42 ); + expect( wrapper.find( 'input' ).element.selectionEnd ).toStrictEqual( 42 ); + } ); + + it( 'emits events', async () => { + const wrapper = getWrapper(); + + await wrapper.find( 'input' ).setValue( 'Odie' ); + await wrapper.find( 'input' ).trigger( 'blur' ); + await wrapper.find( 'input' ).trigger( 'paste' ); + + expect( wrapper.emitted( 'input' ).length ).toStrictEqual( 2 ); + expect( wrapper.emitted( 'input' )[ 0 ] ).toStrictEqual( [ 'Odie', 4 ] ); + expect( wrapper.emitted( 'input' )[ 1 ] ).toStrictEqual( [ 'Odie', 4 ] ); + expect( wrapper.emitted( 'field-changed' ).length ).toStrictEqual( 1 ); + expect( wrapper.emitted( 'field-changed' )[ 0 ][ 0 ] ).toStrictEqual( 'textField' ); + } ); + + it( 'sets aria-describedby', async () => { + const wrapper = getWrapper(); + expect( wrapper.find( '[aria-describedby]' ).exists() ).toBeFalsy(); + + await wrapper.setProps( { helpText: 'help-text' } ); + + expect( wrapper.find( '#textField' ).attributes( 'aria-describedby' ) ).toStrictEqual( 'textField-help-text' ); + + await wrapper.setProps( { showError: true } ); + + expect( wrapper.find( '#textField' ).attributes( 'aria-describedby' ) ).toStrictEqual( 'textField-help-text textField-error' ); + } ); + +} ); diff --git a/tests/unit/store/bankdata_store.spec.ts b/tests/unit/store/bankdata_store.spec.ts index cc511c858..8ca553833 100644 --- a/tests/unit/store/bankdata_store.spec.ts +++ b/tests/unit/store/bankdata_store.spec.ts @@ -2,21 +2,24 @@ import { getters } from '@src/store/bankdata/getters'; import { actions } from '@src/store/bankdata/actions'; import { Validity } from '@src/view_models/Validity'; import each from 'jest-each'; -import { BankAccount, BankAccountRequest, BankAccountResponse } from '@src/view_models/BankAccount'; +import { BankAccount, BankAccountData, BankAccountRequest } from '@src/view_models/BankAccount'; import mockAxios from 'jest-mock-axios'; import { mutations } from '@src/store/bankdata/mutations'; -function newMinimalStore( overrides: Object ): BankAccount { +function newMinimalStore( overrides: Object = {} ): BankAccount { return Object.assign( { isValidating: false, validity: { - bankdata: Validity.INCOMPLETE, + accountNumber: Validity.INCOMPLETE, + bankCode: Validity.INCOMPLETE, }, values: { + accountNumber: '', + bankCode: '', bankName: '', - bic: '', iban: '', + bic: '', }, }, overrides @@ -26,124 +29,109 @@ function newMinimalStore( overrides: Object ): BankAccount { describe( 'BankData', () => { const testIban = 'DE12345605171238489890', - testBIC = 'ABCDDEFFXXX', + testBic = 'INGDDEFFXXX', testAccount = '34560517', testBankCode = '50010517', testBankName = 'Cool Bank 3000'; describe( 'Getters/bankDataIsInvalid', () => { it( 'does not return invalid bank data on initalization', () => { - expect( getters.bankDataIsInvalid( - newMinimalStore( {} ), - null, - null, - null - ) ).toBe( false ); + expect( getters.bankDataIsInvalid( newMinimalStore(), null, null, null ) ).toBe( false ); } ); - const validityCases = [ - [ Validity.VALID, false ], - [ Validity.INVALID, true ], - [ Validity.INCOMPLETE, false ], - ]; - - each( validityCases ).it( 'returns correct boolean representation of bank data validity (test index %#)', - ( bankDataValidity, isInvalid ) => { - const state = { - validity: { - bankdata: bankDataValidity, - }, - }; - expect( getters.bankDataIsInvalid( - newMinimalStore( state ), - null, - null, - null - ) ).toBe( isInvalid ); - }, - ); + each( [ + [ Validity.VALID, Validity.INCOMPLETE, '', false ], + [ Validity.INVALID, Validity.INCOMPLETE, '', true ], + [ Validity.INCOMPLETE, Validity.INCOMPLETE, '', false ], + [ Validity.VALID, Validity.INCOMPLETE, testIban, false ], + [ Validity.INVALID, Validity.INCOMPLETE, testIban, true ], + [ Validity.INCOMPLETE, Validity.INCOMPLETE, testIban, false ], + [ Validity.INCOMPLETE, Validity.INCOMPLETE, '$$ Not IBAN $$', false ], + [ Validity.INVALID, Validity.INCOMPLETE, '$$ Not IBAN $$', true ], + [ Validity.INCOMPLETE, Validity.INVALID, '$$ Not IBAN $$', true ], + [ Validity.INVALID, Validity.INVALID, '$$ Not IBAN $$', true ], + ] ).it( 'returns correct invalidity (test index %#)', ( + accountNumberValidity: Validity, + bankCodeValidity: Validity, + accountNumber: string, + isInvalid: boolean + ) => { + const store = newMinimalStore(); + store.validity.accountNumber = accountNumberValidity; + store.validity.bankCode = bankCodeValidity; + store.values.accountNumber = accountNumber; + + expect( getters.bankDataIsInvalid( store, null, null, null ) ).toBe( isInvalid ); + } ); } ); describe( 'Getters/bankDataIsValid', () => { it( 'does not return valid bank data on initalization', () => { - expect( getters.bankDataIsValid( - newMinimalStore( {} ), - null, - null, - null - ) ).toBe( false ); + expect( getters.bankDataIsValid( newMinimalStore(), null, null, null ) ).toBe( false ); } ); - const validityCases = [ - [ Validity.VALID, true ], - [ Validity.INVALID, false ], - [ Validity.INCOMPLETE, false ], - ]; - - each( validityCases ).it( 'returns correct boolean representation of bank data validity (test index %#)', - ( bankDataValidity, isValid ) => { - const state = { - validity: { - bankdata: bankDataValidity, - }, - }; - expect( getters.bankDataIsValid( - newMinimalStore( state ), - null, - null, - null - ) ).toBe( isValid ); - }, - ); + each( [ + [ Validity.VALID, Validity.INCOMPLETE, '', true ], + [ Validity.INVALID, Validity.INCOMPLETE, '', false ], + [ Validity.INCOMPLETE, Validity.INCOMPLETE, '', false ], + [ Validity.VALID, Validity.INCOMPLETE, testIban, true ], + [ Validity.INVALID, Validity.INCOMPLETE, testIban, false ], + [ Validity.INCOMPLETE, Validity.INCOMPLETE, testIban, false ], + [ Validity.INCOMPLETE, Validity.INCOMPLETE, '$$ Not IBAN $$', false ], + [ Validity.VALID, Validity.INCOMPLETE, '$$ Not IBAN $$', false ], + [ Validity.INCOMPLETE, Validity.VALID, '$$ Not IBAN $$', false ], + [ Validity.VALID, Validity.VALID, '$$ Not IBAN $$', true ], + ] ).it( 'returns correct boolean representation of bank data validity (test index %#)', ( + accountNumberValidity: Validity, + bankCodeValidity: Validity, + accountNumber: string, + isValid: boolean + ) => { + const store = newMinimalStore(); + store.validity.accountNumber = accountNumberValidity; + store.validity.bankCode = bankCodeValidity; + store.values.accountNumber = accountNumber; + + expect( getters.bankDataIsValid( store, null, null, null ) ).toBe( isValid ); + } ); } ); - describe( 'Getters/getBankName', () => { - it( 'does not return a bank name on initalization', () => { - expect( getters.getBankName( - newMinimalStore( {} ), - null, - null, - null - ) ).toBe( '' ); + describe( 'Getters/accountNumber', () => { + it( 'does not return a bank identifier on initalization', () => { + expect( getters.accountNumber( newMinimalStore(), null, null, null ) ).toBe( '' ); } ); - it( 'does returns bank name from the store', () => { - const state = { - values: { - bankName: 'Cool Bank 3000', - }, - }; - expect( getters.getBankName( - newMinimalStore( state ), - null, - null, - null - ) ).toBe( 'Cool Bank 3000' ); + it( 'returns bank identifier from the store', () => { + const store = newMinimalStore(); + store.values.accountNumber = 'ABCDDEFFXXX'; + + expect( getters.accountNumber( store, null, null, null ) ).toBe( 'ABCDDEFFXXX' ); } ); } ); - describe( 'Getters/getBankId', () => { + describe( 'Getters/bankCode', () => { it( 'does not return a bank identifier on initalization', () => { - expect( getters.getBankId( - newMinimalStore( {} ), - null, - null, - null - ) ).toBe( '' ); + expect( getters.bankCode( newMinimalStore(), null, null, null ) ).toBe( '' ); } ); it( 'returns bank identifier from the store', () => { - const state = { - values: { - bic: 'ABCDDEFFXXX', - }, - }; - expect( getters.getBankId( - newMinimalStore( state ), - null, - null, - null - ) ).toBe( 'ABCDDEFFXXX' ); + const store = newMinimalStore(); + store.values.bankCode = 'ABCDDEFFXXX'; + + expect( getters.bankCode( store, null, null, null ) ).toBe( 'ABCDDEFFXXX' ); + } ); + } ); + + describe( 'Getters/bankName', () => { + it( 'does not return a bank name on initalization', () => { + expect( getters.bankName( newMinimalStore(), null, null, null ) ).toBe( '' ); + } ); + + it( 'does returns bank name from the store', () => { + const store = newMinimalStore(); + store.values.bankName = 'Cool Bank 3000'; + + expect( getters.bankName( store, null, null, null ) ).toBe( 'Cool Bank 3000' ); } ); } ); @@ -153,20 +141,24 @@ describe( 'BankData', () => { mockAxios.reset(); } ); - it( 'commits to mutations [SET_BANK_DATA_VALIDITY], [SET_BANKNAME], [SET_BANKDATA], [SET_IS_VALIDATING]', () => { + it( 'commits to the required mutations', () => { const context = { commit: jest.fn(), }, payload = { validationUrl: '/check-iban', - requestParams: { iban: testIban }, + requestParams: { iban: testAccount }, } as BankAccountRequest, action = actions.setBankData as any; const actionResult = action( context, payload ).then( function () { - expect( context.commit ).toHaveBeenCalledWith( 'SET_BANK_DATA_VALIDITY', Validity.VALID ); - expect( context.commit ).toHaveBeenCalledWith( 'SET_BANKNAME', testBankName ); - expect( context.commit ).toHaveBeenCalledWith( 'SET_BANKDATA', { accountId: testIban, bankId: testBIC } ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_ACCOUNT_NUMBER_VALIDITY', Validity.VALID ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_BANK_CODE_VALIDITY', Validity.VALID ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_ACCOUNT_NUMBER', testAccount ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_BANK_CODE', testBankCode ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_BANK_NAME', testBankName ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_IBAN', testIban ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_BIC', testBic ); expect( context.commit ).toHaveBeenCalledWith( 'SET_IS_VALIDATING', true ); expect( context.commit ).toHaveBeenCalledWith( 'SET_IS_VALIDATING', false ); } ); @@ -175,12 +167,12 @@ describe( 'BankData', () => { status: 200, data: { status: 'OK', - bic: testBIC, - iban: testIban, account: testAccount, bankCode: testBankCode, bankName: testBankName, - } as BankAccountResponse, + iban: testIban, + bic: testBic, + }, } ); return actionResult; @@ -197,14 +189,15 @@ describe( 'BankData', () => { action = actions.setBankData as any; const actionResult = action( context, payload ).then( function () { - expect( context.commit ).toHaveBeenCalledWith( 'SET_BANK_DATA_VALIDITY', Validity.INVALID ); - expect( context.commit ).toHaveBeenCalledWith( 'SET_BANKNAME', '' ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_ACCOUNT_NUMBER_VALIDITY', Validity.INVALID ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_BANK_CODE_VALIDITY', Validity.INVALID ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_BANK_NAME', '' ); } ); mockAxios.mockResponse( { status: 200, data: { status: 'ERR', - } as BankAccountResponse, + }, } ); return actionResult; } ); @@ -230,8 +223,9 @@ describe( 'BankData', () => { action = actions.markBankDataAsIncomplete as any; action( context ); - expect( context.commit ).toHaveBeenNthCalledWith( 1, 'MARK_BANKDATA_INCOMPLETE' ); - expect( context.commit ).toHaveBeenNthCalledWith( 2, 'SET_BANKNAME', '' ); + expect( context.commit ).toHaveBeenNthCalledWith( 1, 'SET_ACCOUNT_NUMBER_VALIDITY', Validity.INCOMPLETE ); + expect( context.commit ).toHaveBeenNthCalledWith( 2, 'SET_BANK_CODE_VALIDITY', Validity.INCOMPLETE ); + expect( context.commit ).toHaveBeenNthCalledWith( 3, 'SET_BANK_NAME', '' ); } ); } ); @@ -243,21 +237,25 @@ describe( 'BankData', () => { action = actions.markBankDataAsInvalid as any; action( context ); - expect( context.commit ).toHaveBeenNthCalledWith( 1, 'SET_BANK_DATA_VALIDITY', Validity.INVALID ); - expect( context.commit ).toHaveBeenNthCalledWith( 2, 'SET_BANKNAME', '' ); + expect( context.commit ).toHaveBeenNthCalledWith( 1, 'SET_ACCOUNT_NUMBER_VALIDITY', Validity.INVALID ); + expect( context.commit ).toHaveBeenNthCalledWith( 2, 'SET_BANK_CODE_VALIDITY', Validity.INVALID ); + expect( context.commit ).toHaveBeenNthCalledWith( 3, 'SET_BANK_NAME', '' ); } ); } ); - function getState( overrides = {} ) { + function getState( overrides = {} ): BankAccount { return { isValidating: false, validity: { - bankdata: Validity.INCOMPLETE, + accountNumber: Validity.INCOMPLETE, + bankCode: Validity.INCOMPLETE, }, values: { + accountNumber: '', + bankCode: '', + bankName: '', iban: '', bic: '', - bankName: '', }, ...overrides, }; @@ -268,44 +266,72 @@ describe( 'BankData', () => { const context = { commit: jest.fn(), }, - payload = { - accountId: testIban, - bankId: testBIC, + payload: BankAccountData = { + accountNumber: testIban, + bankCode: testBankCode, bankName: testBankName, }, action = actions.initializeBankData as any; action( context, payload ); - expect( context.commit ).toHaveBeenCalledWith( 'SET_BANKDATA', { - accountId: testIban, - bankId: testBIC, - } ); - expect( context.commit ).toHaveBeenCalledWith( 'SET_BANKNAME', testBankName ); - expect( context.commit ).toHaveBeenCalledWith( 'SET_BANK_DATA_VALIDITY', Validity.VALID ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_ACCOUNT_NUMBER', testIban ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_BANK_CODE', testBankCode ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_BANK_NAME', testBankName ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_ACCOUNT_NUMBER_VALIDITY', Validity.RESTORED ); + expect( context.commit ).toHaveBeenCalledWith( 'SET_BANK_CODE_VALIDITY', Validity.RESTORED ); + } ); + + it( 'does not restore validity if fields are empty', () => { + const context = { + commit: jest.fn(), + }, + payload: BankAccountData = { + accountNumber: '', + bankCode: '', + bankName: '', + }, + action = actions.initializeBankData as any; + + action( context, payload ); + + expect( context.commit ).not.toHaveBeenCalledWith( 'SET_ACCOUNT_NUMBER_VALIDITY', Validity.RESTORED ); + expect( context.commit ).not.toHaveBeenCalledWith( 'SET_BANK_CODE_VALIDITY', Validity.RESTORED ); } ); } ); describe( 'mutations/MARK_EMPTY_FIELDS_INVALID', () => { - it( 'marks validity as invalid when validity is INCOMPLETE', () => { + test.each( [ + [ Validity.INCOMPLETE, Validity.INVALID ], + [ Validity.RESTORED, Validity.INVALID ], + [ Validity.INVALID, Validity.INVALID ], + [ Validity.VALID, Validity.VALID ], + ] )( 'updates account number validity', ( startValidity: Validity, endValidity: Validity ) => { const state = getState(); + state.validity.accountNumber = startValidity; + state.values.accountNumber = testIban; mutations.MARK_EMPTY_FIELDS_INVALID( state ); - expect( state.validity.bankdata ).toBe( Validity.INVALID ); - } ); - - it( 'marks keeps validity validity is VALID', () => { - const state = getState( { validity: { bankdata: Validity.VALID } } ); - mutations.MARK_EMPTY_FIELDS_INVALID( state ); - expect( state.validity.bankdata ).toBe( Validity.VALID ); + expect( state.validity.accountNumber ).toBe( endValidity ); } ); - it( 'marks keeps validity validity is INVALID', () => { - const state = getState( { validity: { bankdata: Validity.INVALID } } ); + test.each( [ + [ Validity.INCOMPLETE, testIban, Validity.INCOMPLETE ], + [ Validity.RESTORED, testIban, Validity.INCOMPLETE ], + [ Validity.INVALID, testIban, Validity.INCOMPLETE ], + [ Validity.VALID, testIban, Validity.INCOMPLETE ], + [ Validity.INCOMPLETE, '', Validity.INVALID ], + [ Validity.RESTORED, '', Validity.INVALID ], + [ Validity.INVALID, '', Validity.INVALID ], + [ Validity.VALID, '', Validity.VALID ], + ] )( 'updates bank code validity', ( startValidity: Validity, accountNumber: string, endValidity: Validity ) => { + const state = getState(); + state.validity.bankCode = startValidity; + state.values.accountNumber = accountNumber; mutations.MARK_EMPTY_FIELDS_INVALID( state ); - expect( state.validity.bankdata ).toBe( Validity.INVALID ); - } ); + expect( state.validity.bankCode ).toBe( endValidity ); + } ); } ); } ); diff --git a/tests/unit/store/dataInitializers.spec.ts b/tests/unit/store/dataInitializers.spec.ts index 199ecf0a1..b651f9d3b 100644 --- a/tests/unit/store/dataInitializers.spec.ts +++ b/tests/unit/store/dataInitializers.spec.ts @@ -9,6 +9,7 @@ import { createInitialMembershipAddressValues, createInitialMembershipFeeValues, } from '@src/store/dataInitializers'; +import { InitialBankAccountData } from '@src/view_models/BankAccount'; describe( 'createInitialDonationAddressValues', () => { it( 'fills data from storage', () => { @@ -264,24 +265,24 @@ describe( 'createInitialMembershipFeeValues', () => { describe( 'createInitialBankDataValues', () => { it( 'fills data from initial data', () => { - const initialValues = { - iban: 'fakeAccountID', - bic: 'IAmBIC', + const initialValues: InitialBankAccountData = { + accountNumber: 'fakeAccountID', + bankCode: 'IAmBIC', bankname: 'Bank of fakey fake', }; const values = createInitialBankDataValues( initialValues ); - expect( values.accountId ).toEqual( initialValues.iban ); - expect( values.bankId ).toEqual( initialValues.bic ); + expect( values.accountNumber ).toEqual( initialValues.accountNumber ); + expect( values.bankCode ).toEqual( initialValues.bankCode ); expect( values.bankName ).toEqual( initialValues.bankname ); } ); it( 'handles null initial value object', () => { const values = createInitialBankDataValues( null ); - expect( values.accountId ).toEqual( '' ); - expect( values.bankId ).toEqual( '' ); + expect( values.accountNumber ).toEqual( '' ); + expect( values.bankCode ).toEqual( '' ); expect( values.bankName ).toEqual( '' ); } ); } ); diff --git a/tests/unit/store/membership_store.spec.ts b/tests/unit/store/membership_store.spec.ts index 418321929..228ac98b2 100644 --- a/tests/unit/store/membership_store.spec.ts +++ b/tests/unit/store/membership_store.spec.ts @@ -4,6 +4,7 @@ import { AddressTypeModel } from '@src/view_models/AddressTypeModel'; import { action } from '@src/store/util'; import { validateFeeDataRemotely } from '@src/store/axios'; import { FeeValidity } from '@src/view_models/MembershipFee'; +import { BankAccountData } from '@src/view_models/BankAccount'; jest.mock( '@src/store/axios' ); @@ -63,17 +64,17 @@ describe( 'Membership Store', () => { } ); it( 'initializes initial bank account data when available', async () => { - const initialData = { - accountId: 'fakeAccountID', - bankId: 'IAmBIC', + const initialData: BankAccountData = { + accountNumber: 'fakeAccountID', + bankCode: 'IAmBIC', bankName: 'Bank of fakey fake', }; const store = createStore(); await store.dispatch( action( 'bankdata', 'initializeBankData' ), initialData ); - expect( store.state.bankdata.values.iban ).toBe( initialData.accountId ); - expect( store.state.bankdata.values.bic ).toBe( initialData.bankId ); + expect( store.state.bankdata.values.accountNumber ).toBe( initialData.accountNumber ); + expect( store.state.bankdata.values.bankCode ).toBe( initialData.bankCode ); expect( store.state.bankdata.values.bankName ).toBe( initialData.bankName ); } ); } );