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 3d04bea commit 6a2f32f
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 50 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 6a2f32f

Please sign in to comment.