Skip to content

Commit bae946a

Browse files
committed
feat(common): CHECKOUT-2749 Add common custom error types
1 parent b101729 commit bae946a

11 files changed

+198
-18
lines changed

src/core/checkout/checkout-service.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { MissingDataError } from '../common/error/errors';
2+
13
export default class CheckoutService {
24
/**
35
* @constructor
@@ -142,7 +144,7 @@ export default class CheckoutService {
142144
const method = checkout.getPaymentMethod(payment.name, payment.gateway);
143145

144146
if (!method) {
145-
throw new Error('Unable to call this method because the data required for the call is not available. Please refer to the documentation to see what you need to do in order to obtain the required data.');
147+
throw new MissingDataError();
146148
}
147149

148150
return this._paymentStrategyRegistry.getStrategyByMethod(method).execute(payload, options);
@@ -179,7 +181,7 @@ export default class CheckoutService {
179181
const method = checkout.getPaymentMethod(order.payment.id, order.payment.gateway);
180182

181183
if (!method) {
182-
throw new Error('Unable to call this method because the data required for the call is not available. Please refer to the documentation to see what you need to do in order to obtain the required data.');
184+
throw new MissingDataError();
183185
}
184186

185187
return this._paymentStrategyRegistry.getStrategyByMethod(method).finalize(options);
@@ -217,7 +219,7 @@ export default class CheckoutService {
217219
const method = checkout.getPaymentMethod(methodId, gatewayId);
218220

219221
if (!method) {
220-
throw new Error('Unable to call this method because the data required for the call is not available. Please refer to the documentation to see what you need to do in order to obtain the required data.');
222+
throw new MissingDataError();
221223
}
222224

223225
return this._paymentStrategyRegistry.getStrategyByMethod(method).initialize(options);
@@ -233,7 +235,7 @@ export default class CheckoutService {
233235
const method = checkout.getPaymentMethod(methodId, gatewayId);
234236

235237
if (!method) {
236-
throw new Error('Unable to call this method because the data required for the call is not available. Please refer to the documentation to see what you need to do in order to obtain the required data.');
238+
throw new MissingDataError();
237239
}
238240

239241
return this._paymentStrategyRegistry.getStrategyByMethod(method).deinitialize();
@@ -376,7 +378,7 @@ export default class CheckoutService {
376378
const { checkout } = this._store.getState();
377379

378380
if (!checkout.getConfig() || !checkout.getCustomer()) {
379-
throw new Error('Unable to call this method because the data required for the call is not available. Please refer to the documentation to see what you need to do in order to obtain the required data.');
381+
throw new MissingDataError();
380382
}
381383

382384
const { storeId } = checkout.getConfig();
@@ -395,7 +397,7 @@ export default class CheckoutService {
395397
const { checkout } = this._store.getState();
396398

397399
if (!checkout.getConfig() || !checkout.getCustomer()) {
398-
throw new Error('Unable to call this method because the data required for the call is not available. Please refer to the documentation to see what you need to do in order to obtain the required data.');
400+
throw new MissingDataError();
399401
}
400402

401403
const { storeId } = checkout.getConfig();
@@ -414,7 +416,7 @@ export default class CheckoutService {
414416
const { checkout } = this._store.getState();
415417

416418
if (!checkout.getConfig() || !checkout.getCustomer()) {
417-
throw new Error('Unable to call this method because the data required for the call is not available. Please refer to the documentation to see what you need to do in order to obtain the required data.');
419+
throw new MissingDataError();
418420
}
419421

420422
const { storeId } = checkout.getConfig();

src/core/checkout/checkout-service.spec.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { PaymentMethodActionCreator } from '../payment';
1111
import { InstrumentActionCreator } from '../payment/instrument';
1212
import { QuoteActionCreator } from '../quote';
1313
import { ShippingAddressActionCreator, ShippingCountryActionCreator, ShippingOptionActionCreator } from '../shipping';
14+
import { MissingDataError } from '../common/error/errors';
1415
import { getBillingAddress, getBillingAddressResponseBody } from '../billing/billing-address.mock';
1516
import { getCartResponseBody } from '../cart/carts.mock';
1617
import { getCountriesResponseBody } from '../geography/countries.mock';
@@ -226,8 +227,8 @@ describe('CheckoutService', () => {
226227
expect(paymentStrategy.execute).toHaveBeenCalledWith(getOrderRequestBody(), options);
227228
});
228229

229-
it('throws error if payment method is not found or loaded', async () => {
230-
expect(() => checkoutService.submitOrder(getOrderRequestBody())).toThrow();
230+
it('throws error if payment method is not found or loaded', () => {
231+
expect(() => checkoutService.submitOrder(getOrderRequestBody())).toThrow(MissingDataError);
231232
});
232233
});
233234

@@ -370,7 +371,7 @@ describe('CheckoutService', () => {
370371
});
371372

372373
it('throws error if payment method has not been loaded', () => {
373-
expect(() => checkoutService.initializePaymentMethod('braintree')).toThrow();
374+
expect(() => checkoutService.initializePaymentMethod('braintree')).toThrow(MissingDataError);
374375
});
375376
});
376377

@@ -390,7 +391,7 @@ describe('CheckoutService', () => {
390391
});
391392

392393
it('throws error if payment method has not been loaded', () => {
393-
expect(() => checkoutService.deinitializePaymentMethod('braintree')).toThrow();
394+
expect(() => checkoutService.deinitializePaymentMethod('braintree')).toThrow(MissingDataError);
394395
});
395396
});
396397

@@ -543,7 +544,7 @@ describe('CheckoutService', () => {
543544
});
544545

545546
it('throws error if customer data is missing', () => {
546-
expect(() => checkoutService.loadInstruments()).toThrow();
547+
expect(() => checkoutService.loadInstruments()).toThrow(MissingDataError);
547548
});
548549
});
549550

@@ -564,7 +565,7 @@ describe('CheckoutService', () => {
564565
it('throws error if customer data is missing', () => {
565566
const instrument = vaultInstrumentRequestBody();
566567

567-
expect(() => checkoutService.vaultInstrument(instrument)).toThrow();
568+
expect(() => checkoutService.vaultInstrument(instrument)).toThrow(MissingDataError);
568569
});
569570
});
570571

@@ -583,7 +584,7 @@ describe('CheckoutService', () => {
583584
});
584585

585586
it('throws error if customer data is missing', () => {
586-
expect(() => checkoutService.deleteInstrument(456)).toThrow();
587+
expect(() => checkoutService.deleteInstrument(456)).toThrow(MissingDataError);
587588
});
588589
});
589590
});

src/core/common/error/errors/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { default as MissingDataError } from './missing-data-error';
2+
export { default as NotImplementedError } from './not-implemented-error';
3+
export { default as RequestError } from './request-error';
4+
export { default as StandardError } from './standard-error';
5+
export { default as UnrecoverableError } from './unrecoverable-error';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import StandardError from './standard-error';
2+
3+
export default class MissingDataError extends StandardError {
4+
/**
5+
* @constructor
6+
* @param {string} [message]
7+
*/
8+
constructor(message) {
9+
super(message || 'Unable to call this method because the data required for the call is not available. Please refer to the documentation to see what you need to do in order to obtain the required data.');
10+
11+
this.type = 'missing_data';
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import StandardError from './standard-error';
2+
3+
export default class NotImplementedError extends StandardError {
4+
/**
5+
* @constructor
6+
* @param {string} [message]
7+
*/
8+
constructor(message) {
9+
super(message || 'Not implemented.');
10+
11+
this.type = 'not_implemented';
12+
}
13+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import StandardError from './standard-error';
2+
3+
export default class RequestError extends StandardError {
4+
/**
5+
* @constructor
6+
* @param {Response} response
7+
* @param {string} [message] - Fallback message
8+
*/
9+
constructor({ body = {}, headers, status, statusText }, message) {
10+
super(joinErrors(body.errors) || body.detail || body.title || message || 'An unexpected error has occurred.');
11+
12+
this.type = 'request';
13+
this.body = body;
14+
this.headers = headers;
15+
this.status = status;
16+
this.statusText = statusText;
17+
}
18+
}
19+
20+
/**
21+
* @private
22+
* @param {string[] | Array<{ code: string, message: string }>} errors
23+
* @return {?string}
24+
*/
25+
function joinErrors(errors) {
26+
if (!Array.isArray(errors)) {
27+
return;
28+
}
29+
30+
return errors.map(error =>
31+
typeof error === 'object' ? error.message : error
32+
).join(' ');
33+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import RequestError from './request-error';
2+
import { getErrorResponse, getErrorResponseBody } from '../../http-request/responses.mock';
3+
4+
describe('RequestError', () => {
5+
it('sets error type', () => {
6+
const error = new RequestError(getErrorResponse());
7+
8+
expect(error.type).toEqual('request');
9+
});
10+
11+
it('sets error message with errors contained in response', () => {
12+
const response = getErrorResponse();
13+
const error = new RequestError(response);
14+
15+
expect(error.message).toEqual(response.body.errors.join(' '));
16+
});
17+
18+
it('sets error message with error objects contained in response', () => {
19+
const response = getErrorResponse({
20+
...getErrorResponseBody(),
21+
errors: [
22+
{ code: 'invalid_cvv', message: 'Invalid CVV.' },
23+
{ code: 'invalid_account', message: 'Invalid account.' },
24+
],
25+
});
26+
const error = new RequestError(response);
27+
28+
expect(error.message).toEqual('Invalid CVV. Invalid account.');
29+
});
30+
31+
it('sets error message with detail contained in response', () => {
32+
const response = getErrorResponse({
33+
...getErrorResponseBody(),
34+
errors: null,
35+
});
36+
const error = new RequestError(response);
37+
38+
expect(error.message).toEqual(response.body.detail);
39+
});
40+
41+
it('sets error message with title contained in response', () => {
42+
const response = getErrorResponse({
43+
...getErrorResponseBody(),
44+
detail: null,
45+
errors: null,
46+
});
47+
const error = new RequestError(response);
48+
49+
expect(error.message).toEqual(response.body.title);
50+
});
51+
52+
it('sets fallback error message if error message not contained in response', () => {
53+
const message = 'Hello world';
54+
const error = new RequestError(getErrorResponse({}), message);
55+
56+
expect(error.message).toEqual(message);
57+
});
58+
59+
it('sets default error message if none is provided', () => {
60+
const error = new RequestError({});
61+
62+
expect(error.message).toBeDefined();
63+
});
64+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export default class StandardError extends Error {
2+
/**
3+
* @constructor
4+
* @param {string} [message]
5+
*/
6+
constructor(message) {
7+
super(message || 'An unexpected error has occurred.');
8+
9+
Object.setPrototypeOf(this, new.target.prototype);
10+
11+
if (typeof Error.captureStackTrace === 'function') {
12+
Error.captureStackTrace(this, new.target);
13+
} else {
14+
this.stack = (new Error(this.message)).stack;
15+
}
16+
}
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import StandardError from './standard-error';
2+
3+
describe('StandardError', () => {
4+
it('omits error constructor in stack trace', () => {
5+
try {
6+
throw new StandardError();
7+
} catch (error) {
8+
expect(error.stack).not.toContain('StandardError');
9+
}
10+
});
11+
12+
it('sets error message if provided', () => {
13+
const message = 'Hello world';
14+
const error = new StandardError(message);
15+
16+
expect(error.message).toEqual(message);
17+
});
18+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import RequestError from './request-error';
2+
3+
export default class UnrecoverableError extends RequestError {
4+
/**
5+
* @constructor
6+
* @param {Response} response
7+
* @param {string} [message]
8+
*/
9+
constructor(response, message) {
10+
super(response, message || 'An unexpected error has occurred. The checkout process cannot continue as a result.');
11+
12+
this.type = 'unrecoverable';
13+
}
14+
}

0 commit comments

Comments
 (0)