Skip to content
107 changes: 107 additions & 0 deletions packages/core/src/cart/cart-action-creator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { createRequestSender, RequestSender } from '@bigcommerce/request-sender';
import { from, of } from 'rxjs';
import { catchError, toArray } from 'rxjs/operators';

import { Cart } from '../cart';
import CheckoutStore from '../checkout/checkout-store';
import { getCheckoutStoreState } from '../checkout/checkouts.mock';
import createCheckoutStore from '../checkout/create-checkout-store';
import { getErrorResponse, getResponse } from '../common/http-request/responses.mock';

import CartActionCreator from './cart-action-creator';
import { CartActionType } from './cart-actions';
import CartRequestSender from './cart-request-sender';
import { getCart } from './carts.mock';
import { getGQLCartResponse, getGQLCurrencyResponse } from './gql-cart/mocks/gql-cart.mock';

describe('CartActionCreator', () => {
let cartActionCreator: CartActionCreator;
let requestSender: RequestSender;
let cartRequestSender: CartRequestSender;
let store: CheckoutStore;
let cart: Cart;

beforeEach(() => {
cart = getCart();
requestSender = createRequestSender();

cartRequestSender = new CartRequestSender(requestSender);

store = createCheckoutStore(getCheckoutStoreState());

jest.spyOn(cartRequestSender, 'loadCart').mockReturnValue(
Promise.resolve(getResponse(getGQLCartResponse())),
);

jest.spyOn(cartRequestSender, 'loadCartCurrency').mockReturnValue(
Promise.resolve(getResponse(getGQLCurrencyResponse())),
);

cartActionCreator = new CartActionCreator(cartRequestSender);
});

it('emits action to notify loading progress', async () => {
const actions = await from(cartActionCreator.loadCart(cart.id)(store))
.pipe(toArray())
.toPromise();

expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined, undefined);

expect(actions).toEqual(
expect.arrayContaining([
{ type: CartActionType.LoadCartRequested },
{
type: CartActionType.LoadCartSucceeded,
payload: expect.objectContaining({
id: cart.id,
currency: {
code: cart.currency.code,
name: cart.currency.name,
symbol: cart.currency.symbol,
decimalPlaces: cart.currency.decimalPlaces,
},
lineItems: expect.objectContaining({
physicalItems: cart.lineItems.physicalItems.map((item) =>
expect.objectContaining({
id: item.id,
variantId: item.variantId,
productId: item.productId,
sku: item.sku,
name: item.name,
url: item.url,
quantity: item.quantity,
isShippingRequired: item.isShippingRequired,
}),
),
}),
}),
},
]),
);
});

it('emits error action if unable to load cart', async () => {
jest.spyOn(cartRequestSender, 'loadCart').mockReturnValue(
Promise.reject(getErrorResponse()),
);

const errorHandler = jest.fn((action) => of(action));

const actions = await from(cartActionCreator.loadCart(cart.id)(store))
.pipe(catchError(errorHandler), toArray())
.toPromise();

expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined, undefined);

expect(actions).toEqual(
expect.arrayContaining([
{ type: CartActionType.LoadCartRequested },
{
type: CartActionType.LoadCartFailed,
error: true,
payload: getErrorResponse(),
},
]),
);
});
});
71 changes: 71 additions & 0 deletions packages/core/src/cart/cart-action-creator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { createAction, createErrorAction, ThunkAction } from '@bigcommerce/data-store';
import { Response } from '@bigcommerce/request-sender';
import { merge } from 'lodash';
import { Observable, Observer } from 'rxjs';

import { RequestOptions } from '@bigcommerce/checkout-sdk/payment-integration-api';

import { InternalCheckoutSelectors } from '../checkout';
import { cachableAction } from '../common/data-store';
import ActionOptions from '../common/data-store/action-options';

import Cart from './cart';
import { CartActionType, LoadCartAction } from './cart-actions';
import CartRequestSender from './cart-request-sender';
import { GQLCartResponse, GQLCurrencyResponse, GQLRequestResponse, mapToCart } from './gql-cart';

export default class CartActionCreator {
constructor(private _cartRequestSender: CartRequestSender) {}

@cachableAction
loadCart(
cartId: string,
options?: RequestOptions & ActionOptions,
): ThunkAction<LoadCartAction, InternalCheckoutSelectors> {
return (store) => {
return new Observable((observer: Observer<LoadCartAction>) => {
const state = store.getState();
const gqlUrl = state.config.getGQLRequestUrl();

observer.next(createAction(CartActionType.LoadCartRequested, undefined));

this._cartRequestSender
.loadCart(cartId, gqlUrl, options)
.then((cartResponse) => {
return this._cartRequestSender
.loadCartCurrency(
cartResponse.body.data.site.cart.currencyCode,
gqlUrl,
options,
)
.then((currencyResponse) => {
observer.next(
createAction(
CartActionType.LoadCartSucceeded,
this.transformToCartResponse(
merge(cartResponse, currencyResponse),
),
),
);
observer.complete();
});
})
.catch((response) => {
observer.error(createErrorAction(CartActionType.LoadCartFailed, response));
});
});
};
}

private transformToCartResponse(
response: Response<GQLRequestResponse<GQLCartResponse & GQLCurrencyResponse>>,
): Cart {
const {
body: {
data: { site },
},
} = response;

return mapToCart(site);
}
}
26 changes: 26 additions & 0 deletions packages/core/src/cart/cart-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Action } from '@bigcommerce/data-store';

import Cart from './cart';

export enum CartActionType {
LoadCartRequested = 'LOAD_CART_REQUESTED',
LoadCartSucceeded = 'LOAD_CART_SUCCEEDED',
LoadCartFailed = 'LOAD_CART_FAILED',
}

export type LoadCartAction =
| LoadCartRequestedAction
| LoadCartSucceededAction
| LoadCartFailedAction;

export interface LoadCartRequestedAction extends Action {
type: CartActionType.LoadCartRequested;
}

export interface LoadCartSucceededAction extends Action<Cart> {
type: CartActionType.LoadCartSucceeded;
}

export interface LoadCartFailedAction extends Action<Error> {
type: CartActionType.LoadCartFailed;
}
7 changes: 6 additions & 1 deletion packages/core/src/cart/cart-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { ConsignmentAction, ConsignmentActionType } from '../shipping';

import Cart from './cart';
import { CartActionType, LoadCartAction } from './cart-actions';
import CartState, { CartErrorsState, CartStatusesState, DEFAULT_STATE } from './cart-state';

export default function cartReducer(state: CartState = DEFAULT_STATE, action: Action): CartState {
Expand All @@ -32,7 +33,8 @@ function dataReducer(
| CheckoutAction
| ConsignmentAction
| CouponAction
| GiftCertificateAction,
| GiftCertificateAction
| LoadCartAction,
): Cart | undefined {
switch (action.type) {
case BillingAddressActionType.UpdateBillingAddressSucceeded:
Expand All @@ -48,6 +50,9 @@ function dataReducer(
case GiftCertificateActionType.RemoveGiftCertificateSucceeded:
return objectMerge(data, action.payload && action.payload.cart);

case CartActionType.LoadCartSucceeded:
return objectMerge(data, action.payload && action.payload);

default:
return data;
}
Expand Down
69 changes: 69 additions & 0 deletions packages/core/src/cart/cart-request-sender.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@ import {

import { CartSource } from '@bigcommerce/checkout-sdk/payment-integration-api';

import { GQL_REQUEST_URL } from '../common/gql-request';
import { ContentType, SDK_VERSION_HEADERS } from '../common/http-request';
import { getResponse } from '../common/http-request/responses.mock';

import BuyNowCartRequestBody from './buy-now-cart-request-body';
import Cart from './cart';
import CartRequestSender from './cart-request-sender';
import { getCart } from './carts.mock';
import { GQLCartResponse, GQLCurrencyResponse, GQLRequestResponse } from './gql-cart';
import getCartCurrencyQuery from './gql-cart/get-cart-currency-query';
import getCartQuery from './gql-cart/get-cart-query';
import { getGQLCartResponse, getGQLCurrencyResponse } from './gql-cart/mocks/gql-cart.mock';

describe('CartRequestSender', () => {
let cart: Cart;
let cartRequestSender: CartRequestSender;
let requestSender: RequestSender;
let response: Response<Cart>;
let gqlResponse: Response<GQLRequestResponse<GQLCartResponse>>;
let gqlCurrencyResponse: Response<GQLRequestResponse<GQLCurrencyResponse>>;

beforeEach(() => {
requestSender = createRequestSender();
Expand Down Expand Up @@ -75,4 +82,66 @@ describe('CartRequestSender', () => {
});
});
});

describe('#loadCart', () => {
const cartId = '123123';
const gqlUrl = 'https://test.com/graphql';

beforeEach(() => {
gqlResponse = getResponse(getGQLCartResponse());

jest.spyOn(requestSender, 'post').mockResolvedValue(gqlResponse);
});

it('get gql cart', async () => {
await cartRequestSender.loadCart(cartId);

expect(requestSender.post).toHaveBeenCalledWith(GQL_REQUEST_URL, {
body: {
query: getCartQuery(cartId),
},
});
});

it('get gql cart with graphql url', async () => {
await cartRequestSender.loadCart(cartId, gqlUrl);

expect(requestSender.post).toHaveBeenCalledWith('https://test.com/graphql', {
body: {
query: getCartQuery(cartId),
},
});
});
});

describe('#loadCartCurrency', () => {
const currencyCode = 'USD';
const gqlUrl = 'https://test.com/graphql';

beforeEach(() => {
gqlCurrencyResponse = getResponse(getGQLCurrencyResponse());

jest.spyOn(requestSender, 'post').mockResolvedValue(gqlCurrencyResponse);
});

it('get gql cart currency', async () => {
await cartRequestSender.loadCartCurrency(currencyCode);

expect(requestSender.post).toHaveBeenCalledWith(GQL_REQUEST_URL, {
body: {
query: getCartCurrencyQuery(currencyCode),
},
});
});

it('get gql cart currency with host url', async () => {
await cartRequestSender.loadCartCurrency(currencyCode, gqlUrl);

expect(requestSender.post).toHaveBeenCalledWith('https://test.com/graphql', {
body: {
query: getCartCurrencyQuery(currencyCode),
},
});
});
});
});
Loading