Skip to content

Commit

Permalink
Add new bank data fields
Browse files Browse the repository at this point in the history
  • Loading branch information
Abban committed Aug 23, 2024
1 parent 7474df2 commit f098189
Show file tree
Hide file tree
Showing 22 changed files with 972 additions and 53 deletions.
64 changes: 64 additions & 0 deletions src/api/BankValidationResource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { BankAccountNumberRequest, BankAccountResponse, BankIbanRequest } from '@src/view_models/BankAccount';
import axios from 'axios';

export interface BankValidationResource {
validateIban: ( data: BankIbanRequest ) => Promise<BankAccountResponse>;
validateBankNumber: ( data: BankAccountNumberRequest ) => Promise<BankAccountResponse>;
}

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<BankAccountResponse> {
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<BankAccountResponse> {
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<APIResponse> {
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 );
}
}
5 changes: 2 additions & 3 deletions src/components/pages/donation_form/AddressForms.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand All @@ -183,7 +182,7 @@ const props = withDefaults( defineProps<Props>(), {
} );
const { addressType, isFullSelected, addressValidationPatterns } = toRefs( props );
const store = injectStrict( StoreKey );
const store = useStore();
const {
formData,
fieldErrors,
Expand Down
4 changes: 2 additions & 2 deletions src/components/pages/donation_form/SubmitValues.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<input type="hidden" name="interval" :value="payment.interval">
<input type="hidden" name="amount" :value="payment.amount">

<input type="hidden" name="iban" :value="bankdata.accountNumber">
<input type="hidden" name="bic" :value="bankdata.bankCode">
<input type="hidden" name="iban" :value="bankdata.iban">
<input type="hidden" name="bic" :value="bankdata.bic">

<input type="hidden" name="addressType" :value="addressType">

Expand Down
11 changes: 5 additions & 6 deletions src/components/pages/donation_form/subpages/AddressPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
ref="pageRef"
>
<h1 id="donation-form-heading" class="form-title">{{ $t( 'donation_form_heading' ) }}</h1>
<h2 id="donation-form-subheading" class="form-subtitle">{{ $t( 'donation_form_address_subheading' ) }}</h2>
<p id="donation-form-tagline">{{ $t( 'donation_form_section_address_tagline' ) }}</p>

<PaymentSummary
Expand All @@ -19,13 +18,13 @@
</PaymentSummary>

<form v-if="isDirectDebitPayment" id="bank-data-details" @submit="evt => evt.preventDefault()">
<h2 v-if="isDirectDebitPayment" id="donation-form-subheading" class="form-subtitle">{{ $t( 'donation_form_payment_bankdata_title' ) }}</h2>
<ScrollTarget target-id="iban-scroll-target"/>
<PaymentBankData
:validateBankDataUrl="validateBankDataUrl"
:validateLegacyBankDataUrl="validateLegacyBankDataUrl"
/>
<BankFields/>
</form>

<h2 v-if="isDirectDebitPayment" id="donation-form-subheading" class="form-subtitle">{{ $t( 'donation_form_address_subheading' ) }}</h2>

<form id="address-type-selection" @submit="evt => evt.preventDefault()">
<ScrollTarget target-id="address-type-scroll-target"/>
<AddressTypeBasic
Expand Down Expand Up @@ -96,7 +95,6 @@ import AddressTypeBasic from '@src/components/pages/donation_form/AddressTypeBas
import DonationSummary from '@src/components/pages/donation_form/DonationSummary.vue';
import PaymentSummary from '@src/components/pages/donation_form/PaymentSummary.vue';
import SubmitValues from '@src/components/pages/donation_form/SubmitValues.vue';
import PaymentBankData from '@src/components/shared/PaymentBankData.vue';
import PaymentTextFormButton from '@src/components/shared/form_elements/PaymentTextFormButton.vue';
import FormButton from '@src/components/shared/form_elements/FormButton.vue';
import FormSummary from '@src/components/shared/FormSummary.vue';
Expand All @@ -114,6 +112,7 @@ import { QUERY_STRING_INJECTION_KEY } from '@src/util/createCampaignQueryString'
import { useStore } from 'vuex';
import ScrollTarget from '@src/components/shared/ScrollTarget.vue';
import AddressFormErrorSummaries from '@src/components/pages/donation_form/AddressFormErrorSummaries.vue';
import BankFields from '@src/components/shared/BankFields.vue';
interface Props {
assetsPath: string;
Expand Down
105 changes: 105 additions & 0 deletions src/components/shared/BankFields.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<template>
<div class="payment-bank-data-section">
<ScrollTarget target-id="payment-form-iban-scroll-target"/>
<DirectDebitField
v-model:account-number="accountNumber"
v-model:bank-code="bankCode"
:bank-name="bankName"
:bic="bic"
:account-number-type="accountNumberType"
:show-account-number-error="showError"
:show-bank-code-error="showError"
:bank-validation-resource="bankValidationResource"
@field-changed="validateFields"
/>
</div>
</template>

<script setup lang="ts">
import DirectDebitField from '@src/components/shared/form_fields/DirectDebitField.vue';
import { computed, inject, ref } from 'vue';
import ScrollTarget from '@src/components/shared/ScrollTarget.vue';
import { useStore } from 'vuex';
import { BankValidationResource } from '@src/api/BankValidationResource';
import { action } from '@src/store/util';
import { looksLikeBankAccountNumber, looksLikeIban } from '@src/util/bank_account_number_helpers';
import { AccountNumberType, BankAccountResponse } from '@src/view_models/BankAccount';
import { Validity } from '@src/view_models/Validity';
const store = useStore();
const bankValidationResource = inject<BankValidationResource>( 'bankValidationResource' );
const accountNumber = ref<string>( store.getters[ 'bankdata/accountNumber' ] );
const bankCode = ref<string>( store.getters[ 'bankdata/bankCode' ] );
const bankName = computed<string>( () => store.getters[ 'bankdata/bankName' ] );
const bic = computed<string>( () => store.getters[ 'bankdata/bic' ] );
const showError = computed<boolean>( () => store.getters[ 'bankdata/bankDataIsInvalid' ] );
const accountNumberType = computed<AccountNumberType>( () => {
if ( looksLikeIban( accountNumber.value ) ) {
return AccountNumberType.IBAN;
}
if ( looksLikeBankAccountNumber( accountNumber.value ) ) {
return AccountNumberType.Account;
}
return AccountNumberType.None;
} );
const validateFields = async ( fieldName: 'account-number' | 'bank-code' ): Promise<void> => {
if ( fieldName === 'account-number' ) {
await store.dispatch( action( 'bankdata', 'setAccountNumber' ), accountNumber.value );
}
if ( fieldName === 'bank-code' ) {
await store.dispatch( action( 'bankdata', 'setBankCode' ), bankCode.value );
}
if ( accountNumberType.value !== AccountNumberType.Account || fieldName === 'bank-code' ) {
await store.dispatch( action( 'bankdata', 'markEmptyFieldsAsInvalid' ) );
}
if ( !store.getters[ 'bankdata/bankDataIsValid' ] ) {
return Promise.resolve();
}
let response: BankAccountResponse;
await store.dispatch( action( 'bankdata', 'setValidating' ), true );
try {
if ( accountNumberType.value === AccountNumberType.IBAN ) {
response = await bankValidationResource.validateIban( {
iban: accountNumber.value.toUpperCase(),
} );
await store.dispatch( action( 'bankdata', 'setAccountNumber' ), response.iban );
await store.dispatch( action( 'bankdata', 'setBankCode' ), '' );
} else {
response = await bankValidationResource.validateBankNumber( {
accountNumber: accountNumber.value,
bankCode: bankCode.value,
} );
await store.dispatch( action( 'bankdata', 'setAccountNumber' ), response.accountNumber );
await store.dispatch( action( 'bankdata', 'setBankCode' ), response.bankCode );
}
await store.dispatch( action( 'bankdata', 'setBankDataValidity' ), Validity.VALID );
await store.dispatch( action( 'bankdata', 'setBankName' ), response.bankName );
await store.dispatch( action( 'bankdata', 'setIban' ), response.iban );
await store.dispatch( action( 'bankdata', 'setBic' ), response.bic );
} catch ( e ) {
await store.dispatch( action( 'bankdata', 'setBankDataValidity' ), Validity.INVALID );
await store.dispatch( action( 'bankdata', 'setBankName' ), '' );
await store.dispatch( action( 'bankdata', 'setIban' ), '' );
await store.dispatch( action( 'bankdata', 'setBic' ), '' );
}
await store.dispatch( action( 'bankdata', 'setValidating' ), false );
};
store.watch( ( state, getters ) => getters[ 'bankdata/accountNumber' ], ( newAccountNumber: string ) => {
accountNumber.value = newAccountNumber;
} );
store.watch( ( state, getters ) => getters[ 'bankdata/bankCode' ], ( newBankCode: string ) => {
bankCode.value = newBankCode;
} );
</script>
132 changes: 132 additions & 0 deletions src/components/shared/form_fields/DirectDebitField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<template>
<div class="form-field-direct-debit">
<TextValueField
:value="formattedBankNumber"
:cursor-position="bankNumberCursorPosition"
input-id="account-number"
name="account-number"
:label="$t( accountNumberLabel )"
:placeholder="$t( 'donation_form_payment_bankdata_account_iban_placeholder' )"
:error-message="$t( 'donation_form_payment_bankdata_error' )"
:show-error="showAccountNumberError"
help-text="Please enter your IBAN or your German bank account number"
@field-changed="$emit( 'field-changed', 'account-number' )"
@input="onAccountNumberInput"
>
<template #message>
<span v-if="bankName !== '' && accountNumberType === AccountNumberType.IBAN" class="iban-bank-name">{{ bankName }} ({{ bic }})</span>
</template>
</TextValueField>

<TextField
v-if="accountNumberType === AccountNumberType.Account"
v-model="bankCode"
input-id="bank-code"
name="bank-code"
:label="$t( 'donation_form_payment_bankdata_bank_legacy_label' )"
:placeholder="$t( 'donation_form_payment_bankdata_bank_legacy_placeholder' )"
error-message=""
:show-error="showBankCodeError"
help-text="It looks like you entered a bank account number, if so please enter your bank code"
@field-changed="$emit( 'field-changed', 'bank-code' )"
@update:model-value="onBankCodeUpdate"
>
<template #message>
<span v-if="bankName !== ''" class="bank-name">{{ bankName }}</span>
</template>
</TextField>
</div>
</template>

<script setup lang="ts">
import TextField from '@src/components/shared/form_fields/TextField.vue';
import { computed, nextTick, ref, watch } from 'vue';
import { BankValidationResource } from '@src/api/BankValidationResource';
import TextValueField from '@src/components/shared/form_fields/TextValueField.vue';
import { useFieldModel } from '@src/components/shared/form_fields/useFieldModel';
import { AccountNumberType } from '@src/view_models/BankAccount';
interface Props {
accountNumber: string;
bankCode: string;
bankName: string;
bic: string;
accountNumberType: AccountNumberType
showAccountNumberError: boolean;
showBankCodeError: boolean;
bankValidationResource: BankValidationResource;
}
const getDisplayValue = ( newValue: string ) => {
return newValue.replace( /(.{4})/g, '$& ' ).trim();
};
const props = defineProps<Props>();
const emit = defineEmits( [ 'field-changed', 'update:accountNumber', 'update:bankCode' ] );
const accountNumber = useFieldModel<string>( () => props.accountNumber, props.accountNumber );
const bankCode = useFieldModel<string>( () => props.bankCode, props.bankCode );
const formattedBankNumber = ref<string>( getDisplayValue( accountNumber.value ) );
const bankNumberCursorPosition = ref<number>( 0 );
const accountNumberLabel = computed<string>( () => {
if ( props.accountNumberType === AccountNumberType.IBAN ) {
return 'donation_form_payment_bankdata_account_iban_label';
} else if ( props.accountNumberType === AccountNumberType.Account ) {
return 'donation_form_payment_bankdata_account_legacy_label';
}
return 'donation_form_payment_bankdata_account_default_label';
} );
/**
* 1. Get the text before the cursor
* 2. Clear the spaces
* 3. Add new spaces in the correct positions, but don't trim
* 4. Return the length
*
* @param newValue
* @param cursorPosition
*/
const getNewCursorPosition = ( newValue: string, cursorPosition: number ): number => {
return newValue
.slice( 0, cursorPosition )
.replace( /\s/g, '' )
.replace( /(.{4})/g, '$& ' )
.length;
};
const onAccountNumberInput = async ( newValue: string, cursorPosition: number ): Promise<void> => {
accountNumber.value = newValue.replace( /\s/g, '' );
await nextTick();
formattedBankNumber.value = getDisplayValue( accountNumber.value );
await nextTick();
// When we replace the field value the browser jumps the cursor to the end
// so we reset it after changing the value. This allows the donor to edit
// their bank number if they spot a mistake.
bankNumberCursorPosition.value = getNewCursorPosition( newValue, cursorPosition );
emit( 'update:accountNumber', accountNumber.value );
};
const onBankCodeUpdate = ( newBankCode: string ): void => {
emit( 'update:bankCode', newBankCode );
};
watch( accountNumber, ( newAccountNumber: string ) => {
formattedBankNumber.value = getDisplayValue( newAccountNumber );
} );
</script>

<style lang="scss">
@use '@src/scss/settings/forms';
@use 'sass:map';
.form-field-direct-debit {
max-width: map.get( forms.$input, 'max-width' );
}
</style>
Loading

0 comments on commit f098189

Please sign in to comment.