Skip to content

Commit

Permalink
Add DirectDebitField component
Browse files Browse the repository at this point in the history
This is drop in replacement for the PaymentBankData component
that is more accessible and integrated with our form fields
components.

## Refactored the bankdata store
- Added accountNumber and bankCode fields, as the direct debit
  field will accept both IBAN and account numbers making the
  naming confusing.
- Added getters actions and mutations for the new fields.
- Updated the typescript interfaces to reflect the new structure.

## Added an API resource
This makes the API calls that validate the bank data more
consistent with our other API resources

## Added a BankFields component
The pattern for our form fields is that they take in model
values and do not access the store directly, We then use
wrapper components to manage store access. That's what this
component is for.

## Added a DirectDebitField
This handles visibility of labels, and formatting values
when fields receive input.

## Added TextValueField
As we're formatting the value in the input box we need
a field similar to TextField that uses a prop as the value
rather than take in a model.

Ticket: https://phabricator.wikimedia.org/T364953
  • Loading branch information
Abban committed Aug 27, 2024
1 parent b25ae5e commit de1eb6d
Show file tree
Hide file tree
Showing 27 changed files with 1,334 additions and 297 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 );
}
}
17 changes: 9 additions & 8 deletions src/components/pages/donation_form/AddressFormErrorSummaries.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},
{
Expand Down Expand Up @@ -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'
},
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
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
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
4 changes: 2 additions & 2 deletions src/components/pages/membership_form/SubmitValues.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<input type="hidden" name="payment_type" :value="fee.type">
<input type="hidden" name="membership_fee_interval" :value="fee.interval">
<input type="hidden" name="membership_fee" :value="fee.fee">
<input type="hidden" name="iban" :value="bankdata.iban">
<input type="hidden" name="bic" :value="bankdata.bic">
<input type="hidden" name="iban" :value="bankdata.accountNumber">
<input type="hidden" name="bic" :value="bankdata.bankCode">

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

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="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>
Loading

0 comments on commit de1eb6d

Please sign in to comment.