From c8f9d81bc7f6ae529c04206cd46c6b696ca2a493 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 12 Dec 2024 18:17:25 -0500 Subject: [PATCH] CVV validation on all transactions. --- .../existingPaymentMethods.component.js | 43 ++++++- .../existingPaymentMethods.component.spec.js | 100 +++++++++++++++- .../existingPaymentMethods.tpl.html | 25 ++-- src/app/checkout/step-2/step-2.component.js | 12 +- .../checkout/step-2/step-2.component.spec.js | 91 +++++++++++++-- src/app/checkout/step-2/step-2.tpl.html | 5 +- .../components/Recaptcha/Recaptcha.test.tsx | 52 +-------- src/common/components/Recaptcha/Recaptcha.tsx | 11 +- .../creditCardForm.component.js | 4 +- .../creditCardForm/creditCardForm.tpl.html | 22 +--- .../directives/creditCardCvv.directive.js | 35 ++++++ .../services/api/designations.service.js | 10 +- .../services/api/designations.service.spec.js | 109 +++++++----------- 13 files changed, 336 insertions(+), 183 deletions(-) create mode 100644 src/common/directives/creditCardCvv.directive.js diff --git a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.js b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.js index ce59dea7a..04b7d188d 100644 --- a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.js +++ b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.js @@ -6,19 +6,20 @@ import paymentMethodDisplay from 'common/components/paymentMethods/paymentMethod import paymentMethodFormModal from 'common/components/paymentMethods/paymentMethodForm/paymentMethodForm.modal.component' import coverFees from 'common/components/paymentMethods/coverFees/coverFees.component' +import * as cruPayments from '@cruglobal/cru-payments/dist/cru-payments' import orderService from 'common/services/api/order.service' import cartService from 'common/services/api/cart.service' import { validPaymentMethod } from 'common/services/paymentHelpers/validPaymentMethods' import giveModalWindowTemplate from 'common/templates/giveModalWindow.tpl.html' import { SignInEvent } from 'common/services/session/session.service' - +import creditCardCvv from '../../../../common/directives/creditCardCvv.directive' import template from './existingPaymentMethods.tpl.html' const componentName = 'checkoutExistingPaymentMethods' class ExistingPaymentMethodsController { /* @ngInject */ - constructor ($log, $scope, orderService, cartService, $uibModal) { + constructor ($log, $scope, orderService, cartService, $uibModal, $window) { this.$log = $log this.$scope = $scope this.orderService = orderService @@ -26,6 +27,7 @@ class ExistingPaymentMethodsController { this.$uibModal = $uibModal this.paymentFormResolve = {} this.validPaymentMethod = validPaymentMethod + this.sessionStorage = $window.sessionStorage this.$scope.$on(SignInEvent, () => { this.$onInit() @@ -33,7 +35,9 @@ class ExistingPaymentMethodsController { } $onInit () { + this.enableContinue({ $event: false }) this.loadPaymentMethods() + this.waitForFormInitialization() } $onChanges (changes) { @@ -52,6 +56,27 @@ class ExistingPaymentMethodsController { } } + waitForFormInitialization () { + const unregister = this.$scope.$watch('$ctrl.creditCardPaymentForm.securityCode', () => { + if (this.creditCardPaymentForm && this.creditCardPaymentForm.securityCode) { + unregister() + this.addCvvValidators() + this.switchPayment() + } + }) + } + + addCvvValidators () { + this.$scope.$watch('$ctrl.creditCardPaymentForm.securityCode.$viewValue', (number) => { + if (this.selectedPaymentMethod?.['card-type'] && this.creditCardPaymentForm.securityCode) { + this.creditCardPaymentForm.securityCode.$validators.minLength = cruPayments.creditCard.cvv.validate.minLength + this.creditCardPaymentForm.securityCode.$validators.maxLength = cruPayments.creditCard.cvv.validate.maxLength + this.enableContinue({ $event: cruPayments.creditCard.cvv.validate.minLength(number) && cruPayments.creditCard.cvv.validate.maxLength(number) }) + this.selectedPaymentMethod.cvv = number + } + }) + } + loadPaymentMethods () { this.orderService.getExistingPaymentMethods() .subscribe((data) => { @@ -80,6 +105,7 @@ class ExistingPaymentMethodsController { // Select the first payment method this.selectedPaymentMethod = paymentMethods[0] } + this.shouldRecoverCvv = true this.switchPayment() } @@ -130,6 +156,13 @@ class ExistingPaymentMethodsController { switchPayment () { this.onPaymentChange({ selectedPaymentMethod: this.selectedPaymentMethod }) + if (this.selectedPaymentMethod?.['card-type'] && this.creditCardPaymentForm?.securityCode) { + // Set cvv from session storage + const storage = this.shouldRecoverCvv ? JSON.parse(this.sessionStorage.getItem('cvv')) : '' + this.creditCardPaymentForm.securityCode.$setViewValue(storage) + this.creditCardPaymentForm.securityCode.$render() + this.shouldRecoverCvv = false + } if (this.selectedPaymentMethod?.['bank-name']) { // This is an EFT payment method so we need to remove any fee coverage this.orderService.storeCoverFeeDecision(false) @@ -144,7 +177,8 @@ export default angular paymentMethodFormModal.name, coverFees.name, orderService.name, - cartService.name + cartService.name, + creditCardCvv.name ]) .component(componentName, { controller: ExistingPaymentMethodsController, @@ -159,6 +193,7 @@ export default angular brandedCheckoutItem: '<', onPaymentFormStateChange: '&', onPaymentChange: '&', - onLoad: '&' + onLoad: '&', + enableContinue: '&' } }) diff --git a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.spec.js b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.spec.js index d2194b8ca..372ea7726 100644 --- a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.spec.js +++ b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.spec.js @@ -4,6 +4,7 @@ import { Observable } from 'rxjs/Observable' import 'rxjs/add/observable/of' import 'rxjs/add/observable/throw' import 'rxjs/add/operator/toPromise' +import * as cruPayments from '@cruglobal/cru-payments/dist/cru-payments' import { SignInEvent } from 'common/services/session/session.service' @@ -15,24 +16,43 @@ describe('checkout', () => { beforeEach(angular.mock.module(module.name)) const self = {} - beforeEach(inject(($componentController, $timeout) => { + beforeEach(inject(($componentController, $timeout, $window) => { self.$timeout = $timeout self.controller = $componentController(module.name, {}, { onLoad: jest.fn(), onPaymentChange: jest.fn(), + enableContinue: jest.fn(), onPaymentFormStateChange: jest.fn(), - cartData: { items: [] } + cartData: { items: [] }, + creditCardPaymentForm: { + securityCode: { + $valid: true, + $validators: { + minLength: (value) => cruPayments.creditCard.cvv.validate.minLength(value), + maxLength: cruPayments.creditCard.cvv.validate.maxLength + }, + $setViewValue: jest.fn(), + $render: jest.fn(), + } + }, + selectedPaymentMethod: { + cvv: '', + 'card-type': 'Visa' + } }) + self.$window = $window + self.$window.sessionStorage.clear() })) - describe('$onInit', () => { it('should call loadPaymentMethods', () => { jest.spyOn(self.controller, 'loadPaymentMethods').mockImplementation(() => {}) + jest.spyOn(self.controller, 'waitForFormInitialization').mockImplementation(() => {}) self.controller.$onInit() expect(self.controller.loadPaymentMethods).toHaveBeenCalled() + expect(self.controller.waitForFormInitialization).toHaveBeenCalled() }) it('should be called on sign in', () => { @@ -329,6 +349,80 @@ describe('checkout', () => { expect(self.controller.onPaymentChange).toHaveBeenCalledWith({ selectedPaymentMethod: undefined }) expect(self.controller.orderService.storeCoverFeeDecision).not.toHaveBeenCalled() }) + + it('should reset securityCode viewValue', () => { + self.controller.switchPayment() + + expect(self.controller.creditCardPaymentForm.securityCode.$setViewValue).toHaveBeenCalledWith('') + expect(self.controller.creditCardPaymentForm.securityCode.$render).toHaveBeenCalled() + }) + + it('should add securityCode viewValue from sessionStorage', () => { + self.$window.sessionStorage.setItem( + 'cvv', + '456' + ) + self.controller.shouldRecoverCvv = true + self.controller.switchPayment() + + expect(self.controller.creditCardPaymentForm.securityCode.$setViewValue).toHaveBeenCalledWith(456) + expect(self.controller.creditCardPaymentForm.securityCode.$render).toHaveBeenCalled() + }) + }) + + describe('addCvvValidators', () => { + it('should add a watch on the security code value', () => { + self.controller.creditCardPaymentForm = { + $valid: true, + $dirty: false, + securityCode: { + $viewValue: '123', + $validators: {} + } + } + self.controller.addCvvValidators() + expect(self.controller.$scope.$$watchers.length).toEqual(1) + expect(self.controller.$scope.$$watchers[0].exp).toEqual('$ctrl.creditCardPaymentForm.securityCode.$viewValue') + }) + + it('should add validator functions to creditCardPaymentForm.securityCode', () => { + jest.spyOn(self.controller, 'addCvvValidators') + self.controller.selectedPaymentMethod.self = { + type: 'cru.creditcards.named-credit-card', + uri: 'selected uri' + } + self.controller.waitForFormInitialization() + self.controller.$scope.$digest() + + expect(self.controller.addCvvValidators).toHaveBeenCalled() + expect(Object.keys(self.controller.creditCardPaymentForm.securityCode.$validators).length).toEqual(2) + expect(typeof self.controller.creditCardPaymentForm.securityCode.$validators.minLength).toBe('function') + expect(typeof self.controller.creditCardPaymentForm.securityCode.$validators.maxLength).toBe('function') + }) + + it('should call enableContinue when cvv is valid', () => { + self.controller.creditCardPaymentForm.securityCode.$viewValue = '123' + self.controller.addCvvValidators() + self.controller.$scope.$apply() + + expect(self.controller.enableContinue).toHaveBeenCalledWith({ $event: true }) + }) + + it('should call enableContinue when cvv is too long', () => { + self.controller.creditCardPaymentForm.securityCode.$viewValue = '12345' + self.controller.addCvvValidators() + self.controller.$scope.$apply() + + expect(self.controller.enableContinue).toHaveBeenCalledWith({ $event: false }) + }) + + it('should call enableContinue when cvv is too short', () => { + self.controller.creditCardPaymentForm.securityCode.$viewValue = '1' + self.controller.addCvvValidators() + self.controller.$scope.$apply() + + expect(self.controller.enableContinue).toHaveBeenCalledWith({ $event: false }) + }) }) }) }) diff --git a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.tpl.html b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.tpl.html index 54080a339..287cc758b 100644 --- a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.tpl.html +++ b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.tpl.html @@ -1,17 +1,24 @@ -
+
Your Payment Methods
-
- -
- +
+
+
+ +
+
+ +
+
+
+
{{'OPTIONAL'}}
diff --git a/src/app/checkout/step-2/step-2.component.js b/src/app/checkout/step-2/step-2.component.js index d1f384c3a..2b6c54b4e 100644 --- a/src/app/checkout/step-2/step-2.component.js +++ b/src/app/checkout/step-2/step-2.component.js @@ -78,6 +78,7 @@ class Step2Controller { onPaymentFormStateChange ($event) { this.paymentFormState = $event.state + if ($event.state === 'loading' && $event.payload) { const paymentType = $event.payload.creditCard ? $event.payload.creditCard['card-type'] : $event.payload.bankAccount ? $event.payload.bankAccount['account-type'] : 'Unknown' const request = $event.update @@ -109,6 +110,8 @@ class Step2Controller { this.changeStep({ newStep: 'review' }) this.onStateChange({ state: 'submitted' }) this.paymentFormState = 'success' + } else if ($event.state === 'submitted') { + this.orderService.storeCardSecurityCode(this.selectedPaymentMethod.cvv, this.selectedPaymentMethod.self.uri) } else if ($event.state === 'unsubmitted') { this.onStateChange({ state: 'unsubmitted' }) } else if ($event.state === 'error') { @@ -116,7 +119,10 @@ class Step2Controller { } } - getContinueDisabled () { + isContinueDisabled () { + if (this.selectedPaymentMethod?.['card-type'] && !this.isCvvValid) { + return true + } if (this.loadingPaymentMethods) { return true } @@ -129,6 +135,10 @@ class Step2Controller { } return false } + + enableContinue (isCvvValid) { + this.isCvvValid = isCvvValid + } } export default angular diff --git a/src/app/checkout/step-2/step-2.component.spec.js b/src/app/checkout/step-2/step-2.component.spec.js index f3e61ae79..32dfa424c 100644 --- a/src/app/checkout/step-2/step-2.component.spec.js +++ b/src/app/checkout/step-2/step-2.component.spec.js @@ -164,6 +164,7 @@ describe('checkout', () => { it('should update paymentFormState if transitioning to a different state', () => { self.controller.paymentFormState = 'unsubmitted' + self.controller.selectedPaymentMethod = { cvv: '123', self: { uri: 'uri'} } self.controller.onPaymentFormStateChange({ state: 'submitted' }) expect(self.controller.paymentFormState).toEqual('submitted') @@ -249,14 +250,14 @@ describe('checkout', () => { }) }) - describe('getContinueDisabled', () => { + describe('isContinueDisabled', () => { it('should return true when there are existing payment methods but none are valid', () => { self.controller.handleExistingPaymentLoading(true, true) self.controller.handlePaymentChange(undefined) expect(self.controller.existingPaymentMethods).toBe(true) expect(self.controller.selectedPaymentMethod).toBeUndefined() - expect(self.controller.getContinueDisabled()).toBe(true) + expect(self.controller.isContinueDisabled()).toBe(true) }) it('should return false when there are existing payment methods and at least one is valid', () => { @@ -265,7 +266,7 @@ describe('checkout', () => { expect(self.controller.existingPaymentMethods).toBe(true) expect(self.controller.selectedPaymentMethod).not.toBeUndefined() - expect(self.controller.getContinueDisabled()).toBe(false) + expect(self.controller.isContinueDisabled()).toBe(false) }) it('should return false when there are not existing payment methods', () => { @@ -273,19 +274,19 @@ describe('checkout', () => { expect(self.controller.existingPaymentMethods).toBe(false) expect(self.controller.selectedPaymentMethod).toBeUndefined() - expect(self.controller.getContinueDisabled()).toBe(false) + expect(self.controller.isContinueDisabled()).toBe(false) }) it('should return true while the payment methods are loading', () => { self.controller.$onInit() expect(self.controller.loadingPaymentMethods).toBe(true) - expect(self.controller.getContinueDisabled()).toBe(true) + expect(self.controller.isContinueDisabled()).toBe(true) self.controller.handleExistingPaymentLoading(true, false) expect(self.controller.loadingPaymentMethods).toBe(false) - expect(self.controller.getContinueDisabled()).toBe(false) + expect(self.controller.isContinueDisabled()).toBe(false) }) it('should return true while the payment form is encrypting or loading', () => { @@ -295,18 +296,90 @@ describe('checkout', () => { self.controller.onPaymentFormStateChange({ state: 'encrypting' }) expect(self.controller.paymentFormState).toBe('encrypting') - expect(self.controller.getContinueDisabled()).toBe(true) + expect(self.controller.isContinueDisabled()).toBe(true) self.controller.onPaymentFormStateChange({ state: 'loading', payload: {}, update: false }) expect(self.controller.paymentFormState).toBe('loading') - expect(self.controller.getContinueDisabled()).toBe(true) + expect(self.controller.isContinueDisabled()).toBe(true) deferred.resolve() self.$flushPendingTasks() expect(self.controller.paymentFormState).toBe('success') - expect(self.controller.getContinueDisabled()).toBe(false) + expect(self.controller.isContinueDisabled()).toBe(false) + }) + + describe('existing credit card used', () => { + it('should disable continue when cvv is invalid', () => { + self.controller.handleExistingPaymentLoading(true, true) + self.controller.isCvvValid = false + self.controller.handlePaymentChange({'card-type': 'visa'}) + + expect(self.controller.isContinueDisabled()).toBe(true) + }) + + it('should disable continue when cvv is valid', () => { + self.controller.handleExistingPaymentLoading(true, true) + self.controller.isCvvValid = true + self.controller.handlePaymentChange({'card-type': 'visa'}) + + expect(self.controller.isContinueDisabled()).toBe(false) + }) + + it('should not disable continue when cvv is invalid', () => { + self.controller.handleExistingPaymentLoading(true, true) + self.controller.isCvvValid = false + self.controller.handlePaymentChange({'account-type': 'checking'}) + + expect(self.controller.isContinueDisabled()).toBe(false) + }) + }) + + describe('existing EFT used', () => { + it('should not disable continue when cvv validity is undefined', () => { + self.controller.handleExistingPaymentLoading(true, true) + self.controller.isCvvValid = undefined + self.controller.handlePaymentChange({'account-type': 'checking'}) + + expect(self.controller.isContinueDisabled()).toBe(false) + }) + + it('should not disable continue when cvv is valid', () => { + self.controller.handleExistingPaymentLoading(true, true) + self.controller.isCvvValid = true + self.controller.handlePaymentChange({'account-type': 'checking'}) + + expect(self.controller.isContinueDisabled()).toBe(false) + }) + }) + + describe('new credit card used', () => { + it('should disable continue when cvv is invalid and new credit card payment is added', () => { + self.controller.handlePaymentChange({'card-type': 'visa'}) + self.controller.isCvvValid = false + expect(self.controller.isContinueDisabled()).toBe(true) + }) + }) + + describe('new EFT used', () => { + it('should not disable continue when cvv is invalid and new EFT is added', () => { + self.controller.handlePaymentChange({'account-type': 'checking'}) + self.controller.isCvvValid = false + expect(self.controller.isContinueDisabled()).toBe(false) + }) + }) + }) + + describe('enableContinue', () => { + it('should set isCvvValid to false', () => { + self.controller.enableContinue(false) + expect(self.controller.isCvvValid).toBe(false) + }) + + it('should set isCvvValid to true', () => { + self.controller.enableContinue(true) + expect(self.controller.isCvvValid).toBe(true) }) }) }) diff --git a/src/app/checkout/step-2/step-2.tpl.html b/src/app/checkout/step-2/step-2.tpl.html index 7a3edb704..d2203b9cd 100644 --- a/src/app/checkout/step-2/step-2.tpl.html +++ b/src/app/checkout/step-2/step-2.tpl.html @@ -31,7 +31,8 @@ default-payment-type="$ctrl.defaultPaymentType" hide-payment-type-options="$ctrl.hidePaymentTypeOptions" cart-data="$ctrl.cartData" - branded-checkout-item="$ctrl.brandedCheckoutItem"> + branded-checkout-item="$ctrl.brandedCheckoutItem" + enable-continue="$ctrl.enableContinue($event)">
@@ -42,7 +43,7 @@
- diff --git a/src/common/components/Recaptcha/Recaptcha.test.tsx b/src/common/components/Recaptcha/Recaptcha.test.tsx index f77a2e3dc..cb7e1994a 100644 --- a/src/common/components/Recaptcha/Recaptcha.test.tsx +++ b/src/common/components/Recaptcha/Recaptcha.test.tsx @@ -144,7 +144,7 @@ describe('Recaptcha component', () => { //@ts-ignore global.fetch = jest.fn(() => { return Promise.resolve({ - json: () => Promise.resolve({ success: true, action: 'read', score: 0.9 }) + json: () => Promise.resolve({ success: true, action: 'read' }) }) }) @@ -220,11 +220,11 @@ describe('Recaptcha component', () => { }) }) - it('should not block gifts if data is empty', async () => { + it('should not block gifts if something weird happens', async () => { //@ts-ignore global.fetch = jest.fn(() => { return Promise.resolve({ - json: () => Promise.resolve({}) + json: () => Promise.resolve() }) }) @@ -238,51 +238,7 @@ describe('Recaptcha component', () => { await waitFor(() => { expect(onSuccess).toHaveBeenCalledTimes(1) expect(onFailure).not.toHaveBeenCalled() - expect($log.warn).toHaveBeenCalledWith('Recaptcha returned an unusual response:', {}) - }) - }) - - it('should not block gifts if action is undefined', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.resolve({ success: true, score: 0.9 }) - }) - }) - - onSuccess.mockImplementation(() => console.log('success after weird')) - - const { getByRole } = render( - buildRecaptcha() - ) - - await userEvent.click(getByRole('button')) - await waitFor(() => { - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onFailure).not.toHaveBeenCalled() - expect($log.warn).toHaveBeenCalledWith('Recaptcha returned an unusual response:', { success: true, score: 0.9 }) - }) - }) - - it('should not block gifts if score is undefined', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.resolve({ success: true, action: 'submit_gift' }) - }) - }) - - onSuccess.mockImplementation(() => console.log('success after weird')) - - const { getByRole } = render( - buildRecaptcha() - ) - - await userEvent.click(getByRole('button')) - await waitFor(() => { - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onFailure).not.toHaveBeenCalled() - expect($log.warn).toHaveBeenCalledWith('Recaptcha returned an unusual response:', { success: true, action: 'submit_gift' }) + expect($log.warn).toHaveBeenCalledWith('Data was missing!') }) }) diff --git a/src/common/components/Recaptcha/Recaptcha.tsx b/src/common/components/Recaptcha/Recaptcha.tsx index ddf7bf21e..49f7163e3 100644 --- a/src/common/components/Recaptcha/Recaptcha.tsx +++ b/src/common/components/Recaptcha/Recaptcha.tsx @@ -86,12 +86,6 @@ export const Recaptcha = ({ }) const data = await serverResponse.json() - if (!data || !data.score || !data.action) { - $log.warn('Recaptcha returned an unusual response:', data) - onSuccess(componentInstance) - return - } - if (data?.success === true && isValidAction(data?.action)) { if (data.score < 0.5) { $log.warn(`Captcha score was below the threshold: ${data.score}`) @@ -106,6 +100,11 @@ export const Recaptcha = ({ onSuccess(componentInstance) return } + if (!data) { + $log.warn('Data was missing!') + onSuccess(componentInstance) + return + } if (!isValidAction(data?.action)) { $log.warn(`Invalid action: ${data?.action}`) onFailure(componentInstance) diff --git a/src/common/components/paymentMethods/creditCardForm/creditCardForm.component.js b/src/common/components/paymentMethods/creditCardForm/creditCardForm.component.js index 6e152e545..88ca925b4 100644 --- a/src/common/components/paymentMethods/creditCardForm/creditCardForm.component.js +++ b/src/common/components/paymentMethods/creditCardForm/creditCardForm.component.js @@ -21,6 +21,7 @@ import tsys from 'common/services/api/tsys.service' import template from './creditCardForm.tpl.html' import creditCardNumberDirective from '../../../directives/creditCardNumber.directive' +import creditCardCvv from '../../../../common/directives/creditCardCvv.directive' const componentName = 'creditCardForm' @@ -195,7 +196,8 @@ export default angular showErrors.name, analyticsFactory.name, tsys.name, - creditCardNumberDirective.name + creditCardNumberDirective.name, + creditCardCvv.name ]) .component(componentName, { controller: CreditCardController, diff --git a/src/common/components/paymentMethods/creditCardForm/creditCardForm.tpl.html b/src/common/components/paymentMethods/creditCardForm/creditCardForm.tpl.html index b22a1d791..1cd86debb 100644 --- a/src/common/components/paymentMethods/creditCardForm/creditCardForm.tpl.html +++ b/src/common/components/paymentMethods/creditCardForm/creditCardForm.tpl.html @@ -102,27 +102,7 @@

{{'CREDIT_CARD_PAYMENT'}}

-
- -
-
{{'CARD_SEC_CODE_ERROR'}}
-
{{'MIN_LENGTH_CARD_SEC_CODE'}}
-
{{'MAX_LENGTH_CARD_SEC_CODE'}}
-
- {{'LOCATION_OF_CODE_OTHER'}} - {{'LOCATION_OF_CODE_AMEX'}} -
-
-
+
diff --git a/src/common/directives/creditCardCvv.directive.js b/src/common/directives/creditCardCvv.directive.js new file mode 100644 index 000000000..c77e49378 --- /dev/null +++ b/src/common/directives/creditCardCvv.directive.js @@ -0,0 +1,35 @@ +import angular from 'angular' +const directiveName = 'creditCardCvv' + +const template = +`
+ + +
+
{{'CARD_SEC_CODE_ERROR'}}
+
{{'MIN_LENGTH_CARD_SEC_CODE'}}
+
{{'MAX_LENGTH_CARD_SEC_CODE'}}
+
+ {{'LOCATION_OF_CODE_OTHER'}} + {{'LOCATION_OF_CODE_AMEX'}} +
+
+
` + +const creditCardCvv = /* @ngInject */ () => { + const directiveDefinitionObject = { + restrict: 'E', + template + } + return directiveDefinitionObject +} + +export default angular + .module(directiveName, []) + .directive(directiveName, creditCardCvv) diff --git a/src/common/services/api/designations.service.js b/src/common/services/api/designations.service.js index f10dc15a2..810a3d1d2 100644 --- a/src/common/services/api/designations.service.js +++ b/src/common/services/api/designations.service.js @@ -4,7 +4,6 @@ import toFinite from 'lodash/toFinite' import startsWith from 'lodash/startsWith' import { Observable } from 'rxjs/Observable' import 'rxjs/add/observable/from' -import 'rxjs/add/observable/of' import 'rxjs/add/operator/map' import 'rxjs/add/operator/catch' import moment from 'moment' @@ -234,7 +233,7 @@ class DesignationsService { } return suggestedAmounts }) - .catch(() => Observable.of([])) + .catch(() => []) } facebookPixel (code) { @@ -254,13 +253,6 @@ class DesignationsService { // Map giving links if (data.data['jcr:content'].givingLinks) { angular.forEach(data.data['jcr:content'].givingLinks, (v, k) => { - if (!v || !v.name || !v.url) { - // Some accounts contain multiple, empty giving links. Until we figure how how they - // are being created, we are ignoring them on the frontend. - // https://jira.cru.org/browse/EP-2554 - return - } - if (toFinite(k) > 0 || startsWith(k, 'item')) { givingLinks.push({ name: v.name, diff --git a/src/common/services/api/designations.service.spec.js b/src/common/services/api/designations.service.spec.js index b956af4fd..54145e941 100644 --- a/src/common/services/api/designations.service.spec.js +++ b/src/common/services/api/designations.service.spec.js @@ -24,7 +24,7 @@ describe('designation service', () => { }) describe('productSearch', () => { - it('should send a request to API and get results', done => { + it('should send a request to API and get results', () => { self.$httpBackend.expectGET('https://give-stage2.cru.org/search?keyword=steve').respond(200, searchResponse) self.designationsService.productSearch({ keyword: 'steve' @@ -36,12 +36,11 @@ describe('designation service', () => { name: 'John and Jane Doe', type: 'Staff' })]) - done() - }, done) + }) self.$httpBackend.flush() }) - it('should handle undefined fields', done => { + it('should handle undefined fields', () => { self.$httpBackend.expectGET('https://give-stage2.cru.org/search?keyword=steve').respond(200, { hits: [{}] }) self.designationsService.productSearch({ keyword: 'steve' @@ -53,15 +52,14 @@ describe('designation service', () => { name: null, type: null })]) - done() - }, done) + }) self.$httpBackend.flush() }) }) describe('productLookup', () => { const expectedResponse = { - uri: 'carts/items/crugive/a5t4fmspmfpwpqvqli7teksyhu=/form', + uri: 'items/crugive/a5t4fmspmfpwpqvqli7teksyhu=', frequencies: [ { name: 'QUARTERLY', @@ -92,63 +90,58 @@ describe('designation service', () => { frequency: 'NA', displayName: 'Steve Peck', designationType: 'Staff', - orgId: 'STAFF', code: '0354433', designationNumber: '0354433' } - it('should get product details for a designation number', done => { + it('should get product details for a designation number', () => { self.$httpBackend.expectPOST('https://give-stage2.cru.org/cortex/items/crugive/lookups/form?FollowLocation=true&zoom=code,offer:code,definition,definition:options:element:selector:choice,definition:options:element:selector:choice:description,definition:options:element:selector:choice:selectaction,definition:options:element:selector:chosen,definition:options:element:selector:chosen:description', { code: '0354433' }) .respond(200, lookupResponse) self.designationsService.productLookup('0354433') .subscribe((data) => { expect(data).toEqual(expectedResponse) - done() - }, done) + }) self.$httpBackend.flush() }) - it('should get product details for a uri', done => { + it('should get product details for a uri', () => { self.$httpBackend.expectPOST('https://give-stage2.cru.org/cortex/itemselections/crugive/a5t4fmspmhbkez6cwbnd6mrkla74hdgcupbl4xjb=/options/izzgk4lvmvxgg6i=/values/jzaq=/selector?FollowLocation=true&zoom=code,offer:code,definition,definition:options:element:selector:choice,definition:options:element:selector:choice:description,definition:options:element:selector:choice:selectaction,definition:options:element:selector:chosen,definition:options:element:selector:chosen:description') .respond(200, lookupResponse) self.designationsService.productLookup('/itemselections/crugive/a5t4fmspmhbkez6cwbnd6mrkla74hdgcupbl4xjb=/options/izzgk4lvmvxgg6i=/values/jzaq=/selector', true) .subscribe(data => { expect(data).toEqual(expectedResponse) - done() - }, done) + }) self.$httpBackend.flush() }) - it('should handle an empty response', done => { + it('should handle an empty response', () => { self.$httpBackend.expectPOST('https://give-stage2.cru.org/cortex/itemselections/crugive/a5t4fmspmhbkez6cwbnd6mrkla74hdgcupbl4xjb=/options/izzgk4lvmvxgg6i=/values/jzaq=/selector?FollowLocation=true&zoom=code,offer:code,definition,definition:options:element:selector:choice,definition:options:element:selector:choice:description,definition:options:element:selector:choice:selectaction,definition:options:element:selector:chosen,definition:options:element:selector:chosen:description') .respond(200, '') self.designationsService.productLookup('/itemselections/crugive/a5t4fmspmhbkez6cwbnd6mrkla74hdgcupbl4xjb=/options/izzgk4lvmvxgg6i=/values/jzaq=/selector', true) .subscribe(() => { - done('success should not have been called') + fail('success should not have been called') }, error => { - expect(error.message).toEqual('Product lookup response contains no code data') - done() + expect(error).toEqual('Product lookup response contains no code data') }) self.$httpBackend.flush() }) }) describe('bulkLookup', () => { - it('should take an array of designation numbers and return corresponding links for items', done => { + it('should take an array of designation numbers and return corresponding links for items', () => { self.$httpBackend.expectPOST('https://give-stage2.cru.org/cortex/items/crugive/lookups/batches/form?FollowLocation=true', { codes: ['0123456', '1234567'] }) .respond(200, bulkLookupResponse) self.designationsService.bulkLookup(['0123456', '1234567']) .subscribe(data => { expect(data).toEqual(bulkLookupResponse) - done() - }, done) + }) self.$httpBackend.flush() }) }) describe('suggestedAmounts', () => { - it('should load suggested amounts', done => { + it('should load suggested amounts', () => { const itemConfig = { amount: 50, 'campaign-page': 9876 } self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/campaigns/0/1/2/3/4/0123456/9876.infinity.json') .respond(200, campaignResponse) @@ -161,12 +154,11 @@ describe('designation service', () => { expect(itemConfig['default-campaign-code']).toEqual('867EM1') expect(itemConfig['jcr-title']).toEqual('PowerPacksTM for Inner City Children') - done() - }, done) + }) self.$httpBackend.flush() }) - it('should handle an invalid campaign page', done => { + it('should handle an invalid campaign page', () => { const itemConfig = { amount: 50, 'campaign-page': 9876 } self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/campaigns/0/1/2/3/4/0123456/9876.infinity.json') .respond(400, {}) @@ -175,12 +167,11 @@ describe('designation service', () => { expect(suggestedAmounts).toEqual([]) expect(itemConfig['default-campaign-code']).toBeUndefined() expect(itemConfig['jcr-title']).toBeUndefined() - done() - }, done) + }) self.$httpBackend.flush() }) - it('should handle no campaign page', done => { + it('should handle no campaign page', () => { const itemConfig = { amount: 50 } self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/designations/0/1/2/3/4/0123456.infinity.json') .respond(200, designationResponse) @@ -189,27 +180,25 @@ describe('designation service', () => { expect(suggestedAmounts).toEqual([]) expect(itemConfig['default-campaign-code']).toEqual('867EM1') expect(itemConfig['jcr-title']).toEqual('PowerPacksTM for Inner City Children') - done() - }, done) + }) self.$httpBackend.flush() }) }) describe('facebookPixel', () => { - it('should load facebook pixel id from JCR', done => { + it('should load facebook pixel id from JCR', () => { self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/designations/0/1/2/3/4/0123456.infinity.json') .respond(200, designationResponse) self.designationsService.facebookPixel('0123456') .subscribe(pixelId => { expect(pixelId).toEqual('123456') - done() - }, done) + }) self.$httpBackend.flush() }) }) describe('givingLinks', () => { - it('should load givingLinks from JCR', done => { + it('should load givingLinks from JCR', () => { self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/designations/0/1/2/3/4/0123456.infinity.json') .respond(200, designationResponse) self.designationsService.givingLinks('0123456') @@ -217,25 +206,7 @@ describe('designation service', () => { expect(givingLinks).toEqual([ { name: 'Name', url: 'https://example.com', order: 0 } ]) - done() - }, done) - self.$httpBackend.flush() - }) - - it('should ignore givingLinks without names or urls', done => { - const response = angular.copy(designationResponse) - response['jcr:content'].givingLinks.item1 = { 'jcr:primaryType': 'nt:unstructured' } - response['jcr:content'].givingLinks.item2 = { 'jcr:primaryType': 'nt:unstructured', url: 'https://example2.com', name: 'Name 2' } - self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/designations/0/1/2/3/4/0123456.infinity.json') - .respond(200, response) - self.designationsService.givingLinks('0123456') - .subscribe(givingLinks => { - expect(givingLinks).toEqual([ - { name: 'Name', url: 'https://example.com', order: 0 }, - { name: 'Name 2', url: 'https://example2.com', order: 2 } - ]) - done() - }, done) + }) self.$httpBackend.flush() }) }) @@ -273,33 +244,31 @@ describe('designation service', () => { }) describe('ministriesList', () => { - it('should return a list of ministries', done => { + it('should return a list of ministries', () => { jest.spyOn(self.$location, 'protocol').mockImplementationOnce(() => 'https') jest.spyOn(self.$location, 'host').mockImplementationOnce(() => 'give-stage-cloud.cru.org') const pagePath = 'page.html' const ministriesResponse = { - ministries: [ - JSON.stringify({ - name: 'Some Ministry', - designationNumber: '0123456', - path: '/some-vanity', - extra: 'something-else' - }) - ] + ministries: [{ + name: 'Some Ministry', + designationNumber: '0123456', + path: '/some-vanity', + extra: 'something-else' + }] } self.$httpBackend.expectGET(`https://give-stage-cloud.cru.org/${pagePath}/jcr:content/content-parsys/designation_search_r.json`) .respond(200, ministriesResponse) - const expectedResult = [{ - name: 'Some Ministry', - designationNumber: '0123456', - facet: null, - path: '/some-vanity' - }] + const expectedResult = { + ministries: [{ + name: 'Some Ministry', + designationNumber: '0123456', + path: '/some-vanity' + }] + } self.designationsService.ministriesList(pagePath).subscribe(actualResult => { expect(actualResult).toEqual(expectedResult) - done() - }, done) + }) self.$httpBackend.flush() }) })