diff --git a/src/common/components/nav/navCart/navCart.component.js b/src/common/components/nav/navCart/navCart.component.js
index 42f30aedf..c5a5f2f57 100644
--- a/src/common/components/nav/navCart/navCart.component.js
+++ b/src/common/components/nav/navCart/navCart.component.js
@@ -7,9 +7,7 @@ import orderService from 'common/services/api/order.service'
import analyticsFactory from 'app/analytics/analytics.factory'
import template from './navCart.tpl.html'
-
-export const giftAddedEvent = 'giftAddedToCart'
-export const cartUpdatedEvent = 'cartUpdatedEvent'
+import { giftAddedEvent, cartUpdatedEvent } from 'common/lib/cartEvents'
const componentName = 'navCart'
diff --git a/src/common/components/nav/navCart/navCart.component.spec.js b/src/common/components/nav/navCart/navCart.component.spec.js
index 3ea72ba33..8423fcf85 100644
--- a/src/common/components/nav/navCart/navCart.component.spec.js
+++ b/src/common/components/nav/navCart/navCart.component.spec.js
@@ -4,7 +4,8 @@ import { Observable } from 'rxjs/Observable'
import { Subject } from 'rxjs/Subject'
import 'rxjs/add/observable/of'
import 'rxjs/add/observable/throw'
-import module, { giftAddedEvent, cartUpdatedEvent } from './navCart.component'
+import module from './navCart.component'
+import { giftAddedEvent, cartUpdatedEvent } from 'common/lib/cartEvents'
describe('navCart', () => {
beforeEach(angular.mock.module(module.name))
diff --git a/src/common/components/nav/navCartIcon.component.js b/src/common/components/nav/navCartIcon.component.js
index 12bdd894c..32ca30663 100644
--- a/src/common/components/nav/navCartIcon.component.js
+++ b/src/common/components/nav/navCartIcon.component.js
@@ -1,6 +1,7 @@
import angular from 'angular'
-import navCart, { giftAddedEvent, cartUpdatedEvent } from 'common/components/nav/navCart/navCart.component'
+import navCart from 'common/components/nav/navCart/navCart.component'
+import { giftAddedEvent, cartUpdatedEvent } from 'common/lib/cartEvents'
import uibDropdown from 'angular-ui-bootstrap/src/dropdown'
import analyticsFactory from 'app/analytics/analytics.factory'
diff --git a/src/common/components/nav/navCartIcon.component.spec.js b/src/common/components/nav/navCartIcon.component.spec.js
index 517c6c5be..7814dc0f0 100644
--- a/src/common/components/nav/navCartIcon.component.spec.js
+++ b/src/common/components/nav/navCartIcon.component.spec.js
@@ -2,7 +2,7 @@ import angular from 'angular'
import 'angular-mocks'
import module from './navCartIcon.component'
-import { giftAddedEvent, cartUpdatedEvent } from 'common/components/nav/navCart/navCart.component'
+import { giftAddedEvent,cartUpdatedEvent } from 'common/lib/cartEvents'
describe('nav cart icon', function () {
beforeEach(angular.mock.module(module.name))
diff --git a/src/common/lib/cartEvents.js b/src/common/lib/cartEvents.js
new file mode 100644
index 000000000..d4b9d0c42
--- /dev/null
+++ b/src/common/lib/cartEvents.js
@@ -0,0 +1,2 @@
+export const giftAddedEvent = 'giftAddedToCart'
+export const cartUpdatedEvent = 'cartUpdatedEvent'
diff --git a/src/common/services/api/order.service.js b/src/common/services/api/order.service.js
index fcbb2ad41..2ea0b4830 100644
--- a/src/common/services/api/order.service.js
+++ b/src/common/services/api/order.service.js
@@ -8,8 +8,10 @@ import 'rxjs/add/observable/of'
import 'rxjs/add/observable/throw'
import map from 'lodash/map'
import omit from 'lodash/omit'
+import isString from 'lodash/isString'
import sortPaymentMethods from 'common/services/paymentHelpers/paymentMethodSort'
import extractPaymentAttributes from 'common/services/paymentHelpers/extractPaymentAttributes'
+import { cartUpdatedEvent } from 'common/lib/cartEvents'
import cortexApiService from '../cortexApi.service'
import cartService from './cart.service'
@@ -35,6 +37,7 @@ class Order {
this.analyticsFactory = analyticsFactory
this.sessionStorage = $window.sessionStorage
this.localStorage = $window.localStorage
+ this.$window = $window
this.$log = $log
this.$filter = $filter
}
@@ -399,6 +402,53 @@ class Order {
return startedOrderWithoutSpouse
}
}
+
+ submitOrder (controller) {
+ delete controller.submissionError
+ delete controller.submissionErrorStatus
+ // Prevent multiple submissions
+ if (controller.submittingOrder) return Observable.empty()
+ controller.submittingOrder = true
+ controller.onSubmittingOrder({ value: true })
+
+ let submitRequest
+ if (controller.bankAccountPaymentDetails) {
+ submitRequest = this.submit()
+ } else if (controller.creditCardPaymentDetails) {
+ const cvv = this.retrieveCardSecurityCode()
+ submitRequest = this.submit(cvv)
+ } else {
+ submitRequest = Observable.throw({ data: 'Current payment type is unknown' })
+ }
+ return submitRequest
+ .do(() => {
+ controller.submittingOrder = false
+ controller.onSubmittingOrder({ value: false })
+ this.clearCardSecurityCodes()
+ this.clearCoverFees()
+ controller.onSubmitted()
+ controller.$scope.$emit(cartUpdatedEvent)
+ })
+ .catch((error) => {
+ // Handle the error side effects when the observable errors
+ this.analyticsFactory.checkoutFieldError('submitOrder', 'failed')
+ controller.submittingOrder = false
+ controller.onSubmittingOrder({ value: false })
+
+ controller.loadCart()
+
+ if (error.config && error.config.data && error.config.data['security-code']) {
+ error.config.data['security-code'] = error.config.data['security-code'].replace(/./g, 'X') // Mask security-code
+ }
+ this.$log.error('Error submitting purchase:', error)
+ controller.onSubmitted()
+ controller.submissionErrorStatus = error.status
+ controller.submissionError = isString(error && error.data) ? (error && error.data).replace(/[:].*$/, '') : 'generic error' // Keep prefix before first colon for easier ng-switch matching
+ this.$window.scrollTo(0, 0)
+
+ return Observable.throw(error) // Return the error as an observable
+ })
+ }
}
export default angular
diff --git a/src/common/services/api/order.service.spec.js b/src/common/services/api/order.service.spec.js
index 8be9e6019..ca068775f 100644
--- a/src/common/services/api/order.service.spec.js
+++ b/src/common/services/api/order.service.spec.js
@@ -2,7 +2,7 @@ import angular from 'angular'
import 'angular-mocks'
import omit from 'lodash/omit'
import cloneDeep from 'lodash/cloneDeep'
-import { Observable } from 'rxjs/Observable'
+import { Observable } from 'rxjs'
import 'rxjs/add/observable/of'
import formatAddressForTemplate from '../addressHelpers/formatAddressForTemplate'
import { Roles } from 'common/services/session/session.service'
@@ -17,6 +17,7 @@ import purchaseFormResponse from 'common/services/api/fixtures/cortex-order-purc
import donorDetailsResponse from 'common/services/api/fixtures/cortex-donordetails.fixture.js'
import needInfoResponse from 'common/services/api/fixtures/cortex-order-needinfo.fixture.js'
import purchaseResponse from 'common/services/api/fixtures/cortex-purchase.fixture.js'
+import { cartUpdatedEvent } from 'common/lib/cartEvents'
describe('order service', () => {
beforeEach(angular.mock.module(module.name))
@@ -1206,4 +1207,161 @@ describe('order service', () => {
expect(self.$window.localStorage.getItem('currentOrder')).toEqual('order id 2')
})
})
+
+ describe('submitOrder', () => {
+ let mockController;
+
+ beforeEach(() => {
+ mockController = {
+ submittingOrder: false,
+ onSubmittingOrder: jest.fn(),
+ onSubmitted: jest.fn(),
+ $scope: {
+ $emit: jest.fn(),
+ },
+ };
+
+ // Mock the submit() method to return a resolved observable
+ self.orderService.submit = jest.fn().mockReturnValue(Observable.of({}));
+ })
+
+ describe('another order submission in progress', () => {
+ it('should not submit the order twice', () => {
+ mockController.submittingOrder = true;
+ // Call submitOrder
+ const result = self.orderService.submitOrder(mockController)
+
+ // The submit method should not be called
+ expect(self.orderService.submit).not.toHaveBeenCalled();
+
+ // It should return an empty observable
+ expect(result).toEqual(Observable.empty());
+ })
+ })
+
+ describe('submit single order', () => {
+ beforeEach(() => {
+ self.orderService.clearCardSecurityCodes = jest.fn();
+ self.orderService.retrieveCardSecurityCode = jest.fn();
+ self.orderService.clearCoverFees = jest.fn();
+ mockController.loadCart = jest.fn();
+ self.$window.scrollTo = jest.fn();
+ })
+
+ afterEach(() => {
+ expect(mockController.onSubmittingOrder).toHaveBeenCalledWith({ value: true })
+ expect(mockController.onSubmittingOrder).toHaveBeenCalledWith({ value: false })
+ expect(mockController.onSubmitted).toHaveBeenCalled()
+ })
+
+ it('should submit the order normally if paying with a bank account', (done) => {
+ mockController.bankAccountPaymentDetails = {}
+ self.orderService.submitOrder(mockController).subscribe(
+ () => {
+ expect(self.orderService.submit).toHaveBeenCalled()
+ expect(self.orderService.clearCardSecurityCodes).toHaveBeenCalled()
+
+ expect(mockController.$scope.$emit).toHaveBeenCalledWith(cartUpdatedEvent)
+ done()
+ })
+ })
+
+ it('should handle an error submitting an order with a bank account', (done) => {
+ self.orderService.submit.mockImplementation(() => Observable.throw({ data: 'error saving bank account' }))
+
+ mockController.bankAccountPaymentDetails = {}
+ self.orderService.submitOrder(mockController).subscribe(
+ ()=>{
+ fail()
+ },
+ () => {
+ // Handle the error and continue with assertions
+ expect(self.orderService.submit).toHaveBeenCalled();
+ expect(mockController.loadCart).toHaveBeenCalled();
+ expect(self.orderService.clearCardSecurityCodes).not.toHaveBeenCalled();
+ expect(self.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'error saving bank account' }]);
+ expect(mockController.submissionError).toEqual('error saving bank account');
+ expect(self.$window.scrollTo).toHaveBeenCalledWith(0, 0);
+
+ done();
+ })
+ })
+
+ it('should submit the order with a CVV if paying with a credit card', (done) => {
+ self.orderService.retrieveCardSecurityCode = jest.fn().mockReturnValue('1234');
+ mockController.creditCardPaymentDetails = {}
+ mockController.coverFeeDecision = true
+ self.orderService.submitOrder(mockController).subscribe(() => {
+ expect(self.orderService.submit).toHaveBeenCalledWith('1234')
+ expect(self.orderService.clearCardSecurityCodes).toHaveBeenCalled()
+ expect(mockController.$scope.$emit).toHaveBeenCalledWith(cartUpdatedEvent)
+ done()
+ })
+ })
+
+ it('should submit the order without a CVV if paying with an existing credit card or the cvv in session storage is missing', (done) => {
+ mockController.creditCardPaymentDetails = {}
+ self.orderService.retrieveCardSecurityCode = jest.fn().mockReturnValue(undefined);
+ mockController.coverFeeDecision = true
+ self.orderService.submitOrder(mockController).subscribe(() => {
+ expect(self.orderService.submit).toHaveBeenCalledWith(undefined)
+ expect(self.orderService.clearCardSecurityCodes).toHaveBeenCalled()
+ expect(mockController.$scope.$emit).toHaveBeenCalledWith(cartUpdatedEvent)
+ done()
+ })
+
+
+ })
+
+ it('should handle an error submitting an order with a credit card', (done) => {
+ self.orderService.submit.mockImplementation(() => Observable.throw({ data: 'CardErrorException: Invalid Card Number: some details' }))
+ mockController.creditCardPaymentDetails = {}
+ self.orderService.retrieveCardSecurityCode = jest.fn().mockReturnValue('1234');
+ self.orderService.submitOrder(mockController).subscribe(
+ () => {},
+ () => { // error handler
+ expect(self.orderService.submit).toHaveBeenCalledWith('1234')
+ expect(self.orderService.clearCardSecurityCodes).not.toHaveBeenCalled()
+ expect(self.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'CardErrorException: Invalid Card Number: some details' }])
+ expect(mockController.submissionError).toEqual('CardErrorException')
+ expect(self.$window.scrollTo).toHaveBeenCalledWith(0, 0);
+ done();
+ })
+ })
+
+ it('should mask the security code on a credit card error', (done) => {
+ self.orderService.submit.mockReturnValue(Observable.throw({ data: 'some error', config: { data: { 'security-code': '1234' } } }))
+ self.orderService.retrieveCardSecurityCode = jest.fn().mockReturnValue('1234');
+ mockController.creditCardPaymentDetails = {}
+ self.orderService.submitOrder(mockController).subscribe(
+ () => {},
+ () => { // error handler
+ expect(self.orderService.submit).toHaveBeenCalledWith('1234')
+ expect(self.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'some error', config: { data: { 'security-code': 'XXXX' } } }])
+ done();
+ })
+ })
+
+ it('should throw an error if neither bank account or credit card details are loaded', (done) => {
+ self.orderService.submitOrder(mockController).subscribe(
+ () => {},
+ () => { // error handler
+ expect(self.orderService.submit).not.toHaveBeenCalled()
+ expect(self.orderService.clearCardSecurityCodes).not.toHaveBeenCalled()
+ expect(self.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'Current payment type is unknown' }])
+ expect(mockController.submissionError).toEqual('Current payment type is unknown')
+ expect(self.$window.scrollTo).toHaveBeenCalledWith(0, 0);
+ done();
+ })
+ })
+
+ it('should clear out cover fee data', (done) => {
+ mockController.creditCardPaymentDetails = {}
+ self.orderService.submitOrder(mockController).subscribe(() => {
+ done()
+ })
+ expect(self.orderService.clearCoverFees).toHaveBeenCalled()
+ })
+ })
+ })
})