From 8533b4b7a5687482e44e766baf4eb1eda8464b85 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Tue, 20 Dec 2016 12:15:21 +0100 Subject: [PATCH] Introduce verification-document component to payments page --- .../payments/funds-recipient/details-form.js | 2 +- .../identity-document-file-upload.js | 106 +++++++++++++++ .../funds-recipient/verification-document.js | 46 +++++++ .../project/settings/donations/payments.js | 123 +++++++++++------- app/models/stripe-connect-account.js | 2 + .../components/payments/funds-recipient.hbs | 7 +- .../funds-recipient/verification-document.hbs | 26 ++++ package.json | 1 + .../identity-document-file-upload-test.js | 19 +++ .../verification-document-test.js | 94 +++++++++++++ .../funds-recipient/verification-document.js | 7 + .../models/stripe-connect-account-test.js | 2 + 12 files changed, 383 insertions(+), 52 deletions(-) create mode 100644 app/components/payments/funds-recipient/identity-document-file-upload.js create mode 100644 app/components/payments/funds-recipient/verification-document.js create mode 100644 app/templates/components/payments/funds-recipient/verification-document.hbs create mode 100644 tests/integration/components/identity-document-file-upload-test.js create mode 100644 tests/integration/components/payments/funds-recipient/verification-document-test.js create mode 100644 tests/pages/components/payments/funds-recipient/verification-document.js diff --git a/app/components/payments/funds-recipient/details-form.js b/app/components/payments/funds-recipient/details-form.js index b002ff5c2..97720add5 100644 --- a/app/components/payments/funds-recipient/details-form.js +++ b/app/components/payments/funds-recipient/details-form.js @@ -27,7 +27,7 @@ export default Component.extend({ isIndividual: equal('recipientType', 'individual'), init() { - let recipientType = get(this, 'account.recipientType') || 'individual'; + let recipientType = get(this, 'stripeConnnectAccount.recipientType') || 'individual'; set(this, 'recipientType', recipientType); this._super(...arguments); }, diff --git a/app/components/payments/funds-recipient/identity-document-file-upload.js b/app/components/payments/funds-recipient/identity-document-file-upload.js new file mode 100644 index 000000000..f16ace39f --- /dev/null +++ b/app/components/payments/funds-recipient/identity-document-file-upload.js @@ -0,0 +1,106 @@ +import Ember from 'ember'; +import EmberUploader from 'ember-uploader'; +import ENV from 'code-corps-ember/config/environment'; + +const { + computed, + get, + getProperties, + isEmpty +} = Ember; + +const { Uploader } = EmberUploader; + +/** + * `payments/funds-recipient/identity-document-file-upload` provides a file input + * for uploading an identity verification document to stripe. + * + * + * @class identity-document-file-upload + * @module Component + * @extends EmberUploader.FileField + */ +export default EmberUploader.FileField.extend({ + classNames: ['identity-document-file-upload'], + + /** + * Object containing additional properties to be passed + * in as part of the form data being uploaded + * @type {Object} + */ + additionalUploadData: { + // required to authorize the upload request + key: ENV.stripe.publishableKey, + purpose: 'identity_document' + }, + + maxFileSize: 1024 * 1024 * 8, // 8mb + multiple: false, + supportedFileTypes: ['image/jpeg', 'image/jpg', 'image/png'], + url: 'https://uploads.stripe.com/v1/files', + + /** + * A computed property containing settings for the ajax request + * Used to set stripe account id in the request header + * @return {Object} + */ + ajaxSettings: computed('stripeConnectAccount', function() { + let headers = { + 'Stripe-Account': get(this, 'stripeConnectAccount.idFromStripe') + }; + + return { headers }; + }), + + /** + * Triggers when the file selection for the rendered file input changes + * @param {[File]} files An array of files selected by the user. + * Since the `multiple` setting is set to false, only 1 file + * is in the array. + */ + filesDidChange(files) { + if (!isEmpty(files) && this._validate(files[0])) { + this._performUpload(files[0]); + } + }, + + _validate({ size, type }) { + let { maxFileSize, supportedFileTypes } = getProperties(this, 'maxFileSize', 'supportedFileTypes'); + let isValid = (size <= maxFileSize) && (supportedFileTypes.indexOf(type) >= 0); + + if (!isValid) { + this.sendAction('validationError'); + } + + return isValid; + }, + + _performUpload(file) { + this.sendAction('uploadStarted'); + + let params = getProperties(this, 'url', 'ajaxSettings'); + let uploader = Uploader.create(params); + + uploader.on('progress', (event) => this._handleUploadProgress(event)); + + let additionalUploadData = get(this, 'additionalUploadData'); + + uploader.upload(file, additionalUploadData) + .then((event) => this._handleUploadDone(event)) + .catch((reason) => this._handleUploadError(reason)); + }, + + // error handlers + + _handleUploadDone({ id }) { + this.sendAction('uploadDone', id); + }, + + _handleUploadError(reason) { + this.sendAction('uploadError', reason); + }, + + _handleUploadProgress(event) { + this.sendAction('uploadProgress', event.percent); + } +}); diff --git a/app/components/payments/funds-recipient/verification-document.js b/app/components/payments/funds-recipient/verification-document.js new file mode 100644 index 000000000..b8b0d19e8 --- /dev/null +++ b/app/components/payments/funds-recipient/verification-document.js @@ -0,0 +1,46 @@ +import Ember from 'ember'; + +const { + Component, + computed, + get, + set +} = Ember; + +const VALIDATION_ERROR = 'The file you selected is invalid. Only .jpg and .png images of up to 8mb in size are supported.'; +const UPLOAD_ERROR = 'There was a problem with uploading your file. Please try again.'; + +export default Component.extend({ + classNames: ['verification-document'], + status: computed.alias('stripeConnectAccount.verificationDocumentStatus'), + + progressPercentage: 0, + progressMessage: computed('progressPercentage', function() { + let percentage = get(this, 'progressPercentage'); + return `Uploading... ${percentage}`; + }), + + onUploadStarted() { + set(this, 'isUploading', true); + set(this, 'error', null); + }, + + onUploadProgress(percentage) { + set(this, 'progressPercentage', percentage); + }, + + onUploadDone(stripeFileUploadId) { + set(this, 'isUploading', false); + let onVerificationDocumentSubmitted = get(this, 'onVerificationDocumentSubmitted'); + onVerificationDocumentSubmitted(stripeFileUploadId); + }, + + onUploadError() { + set(this, 'isUploading', false); + set(this, 'error', UPLOAD_ERROR); + }, + + onValidationError() { + set(this, 'error', VALIDATION_ERROR); + } +}); diff --git a/app/controllers/project/settings/donations/payments.js b/app/controllers/project/settings/donations/payments.js index e076d13e2..02b7af57f 100644 --- a/app/controllers/project/settings/donations/payments.js +++ b/app/controllers/project/settings/donations/payments.js @@ -11,8 +11,9 @@ const { } = Ember; const ACCOUNT_ADDING_ERROR = 'There was a problem submitting your bank account information.'; -const ACCOUNT_TOKEN_CREATION_ERROR = 'There was a problem with your bank account information. Please check your input and try again.'; +const ACCOUNT_TOKEN_CREATION_ERROR = 'There was a problem in using your bank account information. Please check your input and try again.'; const STRIPE_ACCOUNT_CREATION_ERROR = 'There was a problem with your account information. Please check your input and try again.'; +const VERIFICATION_DOCUMENT_ERROR = 'There was a problem in attaching the verification document to your stripe account'; export default Controller.extend({ currentUser: service(), @@ -20,63 +21,82 @@ export default Controller.extend({ stripe: service(), actions: { - onBankAccountInformationSubmitted({ accountNumber, routingNumber }) { + onRecipientDetailsSubmitted(recipientInformation) { set(this, 'isBusy', true); let promises = { - tokenData: this._createAccountToken(accountNumber, routingNumber), - stripeConnectAccount: get(this, 'stripeConnectAccount') + organization: get(this, 'project.organization'), + email: get(this, 'currentUser.user.email') }; RSVP.hash(promises) - .then(({ tokenData, stripeConnectAccount }) => this._addBankAccount(tokenData, stripeConnectAccount)) - .catch((response) => this._handleError(response)) + .then(({ organization, email }) => this._createStripeAccount(recipientInformation, organization, email)) + .catch((reason) => this._handleError(reason)) .finally(() => set(this, 'isBusy', false)); }, - onPersonalIdNumberSubmitted() { - // TODO: FIX THIS - return; - }, - - onRecipientDetailsSubmitted(recipientInformation) { + onBankAccountInformationSubmitted({ accountNumber, routingNumber }) { set(this, 'isBusy', true); let promises = { - organization: get(this, 'project.organization'), - email: get(this, 'currentUser.user.email') + tokenData: this._createAccountToken(accountNumber, routingNumber), + stripeConnectAccount: get(this, 'stripeConnectAccount') }; RSVP.hash(promises) - .then(({ organization, email }) => this._createStripeAccount(recipientInformation, organization, email)) - .catch((reason) => this._handleError(reason)) + .then(({ tokenData, stripeConnectAccount }) => this._addBankAccount(tokenData, stripeConnectAccount)) + .catch((response) => this._handleError(response)) .finally(() => set(this, 'isBusy', false)); }, - onVerificationDocumentSubmitted() { - // TODO: FIX THIS - return; + onVerificationDocumentSubmitted(stripeFileUploadId) { + set(this, 'isBusy', true); + + get(this, 'stripeConnectAccount') + .then((account) => this._assignIdentityVerificationDocument(account, stripeFileUploadId)) + .catch((response) => this._handleError(response)) + .finally(() => set(this, 'isBusy', false)); + }, + + onPersonalIdNumberSubmitted() { + console.log(arguments); } }, - _addBankAccount(tokenData, stripeConnectAccount) { - set(stripeConnectAccount, 'externalAccount', tokenData.id); + // creating a stripe account - return stripeConnectAccount.save() - .then((stripeConnectAccount) => RSVP.resolve(stripeConnectAccount)) - .catch((reason) => this._handleAddBankAccountError(reason)); + _createStripeAccount(recipientInformation, organization, email) { + let accountParams = merge(recipientInformation, { organization, email }); + + return get(this, 'store') + .createRecord('stripe-connect-account', accountParams) + .save() + .then((account) => RSVP.resolve(account)) + .catch((reason) => this._handleStripeAccountCreationError(reason)); }, - _bankAccountTokenParams(accountNumber, routingNumber) { - return { - account_number: accountNumber, - routing_number: routingNumber, - object: 'bank_account', - country: 'US', - currency: 'USD' - }; + _handleStripeAccountCreationError() { + let friendlyError = new FriendlyError(STRIPE_ACCOUNT_CREATION_ERROR); + return RSVP.reject(friendlyError); + }, + + // uploading and assigning an id verification document + + _assignIdentityVerificationDocument(account, stripeFileUploadId) { + set(account, 'identityDocumentId', stripeFileUploadId); + + return account.save() + .then((account) => RSVP.resolve(account)) + .catch((reason) => this._handleIdentityVerificationDocumentError(reason)); + }, + + _handleIdentityVerificationDocumentError() { + let friendlyError = new FriendlyError(VERIFICATION_DOCUMENT_ERROR); + return RSVP.reject(friendlyError); }, + // bank account - token step + _createAccountToken(accountNumber, routingNumber) { let stripe = get(this, 'stripe'); let params = this._bankAccountTokenParams(accountNumber, routingNumber); @@ -86,14 +106,29 @@ export default Controller.extend({ .catch((reason) => this._handleBankAccountTokenError(reason)); }, - _createStripeAccount(recipientInformation, organization, email) { - let accountParams = merge(recipientInformation, { organization, email }); + _bankAccountTokenParams(accountNumber, routingNumber) { + return { + account_number: accountNumber, + country: 'US', + currency: 'USD', + object: 'bank_account', + routing_number: routingNumber + }; + }, - return get(this, 'store') - .createRecord('stripe-connect-account', accountParams) - .save() - .then((account) => RSVP.resolve(account)) - .catch((reason) => this._handleStripeAccountCreationError(reason)); + _handleBankAccountTokenError() { + let friendlyError = new FriendlyError(ACCOUNT_TOKEN_CREATION_ERROR); + return RSVP.reject(friendlyError); + }, + + // bank account - updating connect account record step + + _addBankAccount(tokenData, stripeConnectAccount) { + set(stripeConnectAccount, 'externalAccount', tokenData.id); + + return stripeConnectAccount.save() + .then((stripeConnectAccount) => RSVP.resolve(stripeConnectAccount)) + .catch((reason) => this._handleAddBankAccountError(reason)); }, _handleAddBankAccountError() { @@ -101,17 +136,9 @@ export default Controller.extend({ return RSVP.reject(friendlyError); }, - _handleBankAccountTokenError() { - let friendlyError = new FriendlyError(ACCOUNT_TOKEN_CREATION_ERROR); - return RSVP.reject(friendlyError); - }, + // general catch-all error handler _handleError(error) { set(this, 'error', error); - }, - - _handleStripeAccountCreationError() { - let friendlyError = new FriendlyError(STRIPE_ACCOUNT_CREATION_ERROR); - return RSVP.reject(friendlyError); } }); diff --git a/app/models/stripe-connect-account.js b/app/models/stripe-connect-account.js index b25c9ec5f..1f2023936 100644 --- a/app/models/stripe-connect-account.js +++ b/app/models/stripe-connect-account.js @@ -18,6 +18,7 @@ export default Model.extend({ dobYear: attr(), email: attr(), firstName: attr(), + identityDocumentId: attr(), idFromStripe: attr(), insertedAt: attr(), lastName: attr(), @@ -26,6 +27,7 @@ export default Model.extend({ ssnLast4: attr(), state: attr(), updatedAt: attr(), + verificationDocumentStatus: attr(), verificationFieldsNeeded: attr(), zip: attr(), diff --git a/app/templates/components/payments/funds-recipient.hbs b/app/templates/components/payments/funds-recipient.hbs index 85eab9d73..06c8b915a 100644 --- a/app/templates/components/payments/funds-recipient.hbs +++ b/app/templates/components/payments/funds-recipient.hbs @@ -10,9 +10,10 @@ {{/if}} {{#if (eq status 'verifying')}} -
- TODO: verification-document component goes here -
+ {{payments/funds-recipient/verification-document + isBusy=isBusy + onVerificationDocumentSubmitted=(action onVerificationDocumentSubmitted) + stripeConnectAccount=stripeConnectAccount}}
TODO: personal-id-number component goes here
diff --git a/app/templates/components/payments/funds-recipient/verification-document.hbs b/app/templates/components/payments/funds-recipient/verification-document.hbs new file mode 100644 index 000000000..0f1ae7389 --- /dev/null +++ b/app/templates/components/payments/funds-recipient/verification-document.hbs @@ -0,0 +1,26 @@ + +{{#if (eq status 'required')}} + {{#if isBusy}} + Processing... + {{else if isUploading}} + {{progressMessage}} + {{else}} +
we need you to upload a scan of your ID (form + submit)
+
+ {{payments/funds-recipient/identity-document-file-upload + isBusy=isBusy + stripeConnectAccount=stripeConnectAccount + uploadStarted=(action onUploadStarted) + uploadProgress=(action onUploadProgress) + uploadDone=(action onUploadDone) + uploadError=(action onUploadError) + validationError=(action onValidationError)}} +
+ {{#if error}} +
{{error}}
+ {{/if}} + {{/if}} +{{/if}} +{{#if (eq status 'verifying')}} + Please be patient while we review the document you provided. +{{/if}} \ No newline at end of file diff --git a/package.json b/package.json index a06c5dc0c..ef3167220 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "ember-tooltips": "2.5.0", "ember-truth-helpers": "1.2.0", "ember-typed": "0.1.3", + "ember-uploader": "1.2.2", "emberx-select": "2.2.2", "eslint-plugin-ember-suave": "^1.0.0", "loader.js": "^4.0.10", diff --git a/tests/integration/components/identity-document-file-upload-test.js b/tests/integration/components/identity-document-file-upload-test.js new file mode 100644 index 000000000..7d4ddd205 --- /dev/null +++ b/tests/integration/components/identity-document-file-upload-test.js @@ -0,0 +1,19 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('identity-document-file-upload', 'Integration | Component | identity document file upload', { + integration: true +}); + +test('it renders', function(assert) { + this.render(hbs`{{payments/funds-recipient/identity-document-file-upload}}`); + assert.equal(this.$('.identity-document-file-upload').length, 1); +}); + +// TODO: figure out how to write these. Might require unit testing instead +// test('it sends an uploadStarted action when upload starts', function(assert) {}); +// test('it sends a validationError action if file is larger than 8mb', function(assert) {}); +// test('it sends a validationError action if file is not jpg or png', function(assert) {}); +// test('it sends an uploadError action if file upload fails due to stripe', function(assert) {}); +// test('it updates on upload progress', function(assert) {}); +// test('it sends an uploadDone action with the stripe file id when done uploading', function(assert) {}); diff --git a/tests/integration/components/payments/funds-recipient/verification-document-test.js b/tests/integration/components/payments/funds-recipient/verification-document-test.js new file mode 100644 index 000000000..75558209c --- /dev/null +++ b/tests/integration/components/payments/funds-recipient/verification-document-test.js @@ -0,0 +1,94 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import PageObject from 'ember-cli-page-object'; + +import verificationDocumentComponent from 'code-corps-ember/tests/pages/components/payments/funds-recipient/verification-document'; + +let page = PageObject.create(verificationDocumentComponent); + +function renderPage() { + page.render(hbs` + {{payments/funds-recipient/verification-document + error=error + isBusy=isBusy + isUploading=isUploading + progressPercentage=progressPercentage + stripeConnectAccount=stripeConnectAccount}} + `); +} + +moduleForComponent('payments/funds-recipient/verification-document', 'Integration | Component | payments/funds recipient/verification document', { + integration: true, + beforeEach() { + page.setContext(this); + }, + afterEach() { + page.removeContext(); + } +}); + +test('it renders nothing if status is not "required" or "verifying"', function(assert) { + assert.expect(1); + + this.set('stripeConnectAccount', { verificationDocumentStatus: 'pending_required' }); + + renderPage(); + + assert.equal(page.text, '', 'Nothing is rendered'); +}); + +test('it renders file upload subcomponent if status is "required"', function(assert) { + assert.expect(1); + + this.set('stripeConnectAccount', { verificationDocumentStatus: 'required' }); + + renderPage(); + + assert.ok(page.rendersFileUpload, 'File upload subcomponent is rendered'); +}); + +test('it renders information if stautus is "verifying"', function(assert) { + assert.expect(1); + + this.set('stripeConnectAccount', { verificationDocumentStatus: 'verifying' }); + + renderPage(); + + assert.equal(page.text, 'Please be patient while we review the document you provided.', 'Info text is rendered'); +}); + +test('it shows as busy if busy', function(assert) { + assert.expect(1); + + this.set('stripeConnectAccount', { verificationDocumentStatus: 'required' }); + this.set('isBusy', true); + + renderPage(); + + assert.equal(page.text, 'Processing...', 'Only a busy indicator is rendered.'); +}); + +test('it shows the progress message when uploading', function(assert) { + assert.expect(1); + + this.set('stripeConnectAccount', { verificationDocumentStatus: 'required' }); + this.set('isUploading', true); + this.set('progressPercentage', 20); + + renderPage(); + + assert.equal(page.text, 'Uploading... 20', 'Progress message is rendered'); +}); + +test('it shows errors if any', function(assert) { + assert.expect(1); + this.set('stripeConnectAccount', { verificationDocumentStatus: 'required' }); + this.set('error', 'Lorem ipsum'); + + renderPage(); + + assert.equal(page.errorText, 'Lorem ipsum', 'Error is rendered'); +}); + +// TODO: Figure out how to write these +// test('it sends out 'onVerificationDocumentUploaded' action when upload by subcomponent is done', function(assert) {}) diff --git a/tests/pages/components/payments/funds-recipient/verification-document.js b/tests/pages/components/payments/funds-recipient/verification-document.js new file mode 100644 index 000000000..c80020e78 --- /dev/null +++ b/tests/pages/components/payments/funds-recipient/verification-document.js @@ -0,0 +1,7 @@ +import { isVisible, text } from 'ember-cli-page-object'; + +export default { + errorText: text('.error'), + rendersFileUpload: isVisible('input[type=file]') + +}; diff --git a/tests/unit/models/stripe-connect-account-test.js b/tests/unit/models/stripe-connect-account-test.js index 33afc722a..8b6007700 100644 --- a/tests/unit/models/stripe-connect-account-test.js +++ b/tests/unit/models/stripe-connect-account-test.js @@ -27,6 +27,7 @@ testForAttributes('stripe-connect-account', [ 'dobYear', 'email', 'firstName', + 'identityDocumentId', 'idFromStripe', 'insertedAt', 'lastName', @@ -35,6 +36,7 @@ testForAttributes('stripe-connect-account', [ 'ssnLast4', 'state', 'updatedAt', + 'verificationDocumentStatus', 'verificationFieldsNeeded', 'zip' ]);