Skip to content

Commit

Permalink
CVV validation on all transactions.
Browse files Browse the repository at this point in the history
  • Loading branch information
wjames111 committed Dec 12, 2024
1 parent a31af6e commit c8f9d81
Show file tree
Hide file tree
Showing 13 changed files with 336 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,38 @@ 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
this.cartService = cartService
this.$uibModal = $uibModal
this.paymentFormResolve = {}
this.validPaymentMethod = validPaymentMethod
this.sessionStorage = $window.sessionStorage

this.$scope.$on(SignInEvent, () => {
this.$onInit()
})
}

$onInit () {
this.enableContinue({ $event: false })
this.loadPaymentMethods()
this.waitForFormInitialization()
}

$onChanges (changes) {
Expand All @@ -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) => {
Expand Down Expand Up @@ -80,6 +105,7 @@ class ExistingPaymentMethodsController {
// Select the first payment method
this.selectedPaymentMethod = paymentMethods[0]
}
this.shouldRecoverCvv = true
this.switchPayment()
}

Expand Down Expand Up @@ -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)
Expand All @@ -144,7 +177,8 @@ export default angular
paymentMethodFormModal.name,
coverFees.name,
orderService.name,
cartService.name
cartService.name,
creditCardCvv.name
])
.component(componentName, {
controller: ExistingPaymentMethodsController,
Expand All @@ -159,6 +193,7 @@ export default angular
brandedCheckoutItem: '<',
onPaymentFormStateChange: '&',
onPaymentChange: '&',
onLoad: '&'
onLoad: '&',
enableContinue: '&'
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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', () => {
Expand Down Expand Up @@ -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 })
})
})
})
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
<div class="panel panel-default tab-toggle mb0">
<div class="panel panel-default tab-toggle mb0 existing-payment-method">
<div class="panel-title panel-heading">
<span translate>Your Payment Methods</span>
<i class="fas fa-lock u-floatRight mt--"></i>
</div>
<div class="panel-body">
<div class="radio radio-method" ng-repeat="paymentMethod in $ctrl.paymentMethods" ng-init="expired = !$ctrl.validPaymentMethod(paymentMethod)">
<label>
<input type="radio" name="paymentMethod" ng-model="$ctrl.selectedPaymentMethod" ng-value="paymentMethod" ng-disabled="expired" required ng-change="$ctrl.switchPayment()">
<payment-method-display payment-method="paymentMethod" expired="expired"></payment-method-display>
<button class="btn btn-xs btn-link" ng-click="$ctrl.openPaymentMethodFormModal(paymentMethod)" ng-if="paymentMethod['card-type']" translate>edit</button>
</label>
</div>

<form novalidate name="$ctrl.creditCardPaymentForm">
<div class="radio radio-method" ng-repeat="paymentMethod in $ctrl.paymentMethods" ng-init="expired = !$ctrl.validPaymentMethod(paymentMethod)">
<div class="col-sm-8">
<label>
<input type="radio" name="paymentMethod" ng-model="$ctrl.selectedPaymentMethod" ng-value="paymentMethod" ng-disabled="expired" required ng-change="$ctrl.switchPayment()">
<payment-method-display payment-method="paymentMethod" expired="expired"></payment-method-display>
<button class="btn btn-xs btn-link" ng-click="$ctrl.openPaymentMethodFormModal(paymentMethod)" ng-if="paymentMethod['card-type']" translate>edit</button>
</label>
</div>
<div ng-if="$ctrl.selectedPaymentMethod['card-type'] && $ctrl.selectedPaymentMethod === paymentMethod" class="col-sm-4">
<credit-card-cvv ></credit-card-cvv>
</div>
</div>
</form>
</div>
<div class="panel panel-default tab-toggle mb0 mt"
ng-if="(($ctrl.cartData && $ctrl.cartData.items) || $ctrl.brandedCheckoutItem) && $ctrl.selectedPaymentMethod && $ctrl.selectedPaymentMethod['card-type']">
<div class="panel-title panel-heading" translate>{{'OPTIONAL'}}</div>
Expand Down
12 changes: 11 additions & 1 deletion src/app/checkout/step-2/step-2.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -109,14 +110,19 @@ 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') {
this.onStateChange({ state: 'errorSubmitting' })
}
}

getContinueDisabled () {
isContinueDisabled () {
if (this.selectedPaymentMethod?.['card-type'] && !this.isCvvValid) {
return true
}
if (this.loadingPaymentMethods) {
return true
}
Expand All @@ -129,6 +135,10 @@ class Step2Controller {
}
return false
}

enableContinue (isCvvValid) {
this.isCvvValid = isCvvValid
}
}

export default angular
Expand Down
Loading

0 comments on commit c8f9d81

Please sign in to comment.