Skip to content

Commit c6f09ae

Browse files
committed
feat(payment): PAYPAL-4935 updates related to new requirements
1 parent c723c0c commit c6f09ae

File tree

8 files changed

+342
-59
lines changed

8 files changed

+342
-59
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { createRequestSender, RequestSender } from '@bigcommerce/request-sender';
2+
import { from, of } from 'rxjs';
3+
import { catchError, toArray } from 'rxjs/operators';
4+
5+
import { Cart } from '../cart';
6+
import CheckoutStore from '../checkout/checkout-store';
7+
import { getCheckoutStoreState } from '../checkout/checkouts.mock';
8+
import createCheckoutStore from '../checkout/create-checkout-store';
9+
import { getErrorResponse, getResponse } from '../common/http-request/responses.mock';
10+
11+
import CartActionCreator from './cart-action-creator';
12+
import { CartActionType } from './cart-actions';
13+
import CartRequestSender from './cart-request-sender';
14+
import { getCart } from './carts.mock';
15+
16+
describe('CartActionCreator', () => {
17+
let cartActionCreator: CartActionCreator;
18+
let requestSender: RequestSender;
19+
let cartRequestSender: CartRequestSender;
20+
let store: CheckoutStore;
21+
let cart: Cart;
22+
23+
beforeEach(() => {
24+
cart = getCart();
25+
requestSender = createRequestSender();
26+
27+
cartRequestSender = new CartRequestSender(requestSender);
28+
29+
store = createCheckoutStore(getCheckoutStoreState());
30+
31+
jest.spyOn(cartRequestSender, 'loadCard').mockReturnValue(
32+
Promise.resolve(getResponse(getCart())),
33+
);
34+
35+
cartActionCreator = new CartActionCreator(cartRequestSender);
36+
});
37+
38+
it('emits action to notify loading progress', async () => {
39+
const actions = await from(cartActionCreator.loadCard(cart.id)(store))
40+
.pipe(toArray())
41+
.toPromise();
42+
43+
expect(cartRequestSender.loadCard).toHaveBeenCalledWith(cart.id, undefined, undefined);
44+
45+
expect(actions).toEqual(
46+
expect.arrayContaining([
47+
{ type: CartActionType.LoadCartRequested },
48+
{
49+
type: CartActionType.LoadCartSucceeded,
50+
payload: getCart(),
51+
},
52+
]),
53+
);
54+
});
55+
56+
it('emits error action if unable to load cart', async () => {
57+
jest.spyOn(cartRequestSender, 'loadCard').mockReturnValue(
58+
Promise.reject(getErrorResponse()),
59+
);
60+
61+
const errorHandler = jest.fn((action) => of(action));
62+
63+
const actions = await from(cartActionCreator.loadCard(cart.id)(store))
64+
.pipe(catchError(errorHandler), toArray())
65+
.toPromise();
66+
67+
expect(cartRequestSender.loadCard).toHaveBeenCalledWith(cart.id, undefined, undefined);
68+
69+
expect(actions).toEqual(
70+
expect.arrayContaining([
71+
{ type: CartActionType.LoadCartRequested },
72+
{
73+
type: CartActionType.LoadCartFailed,
74+
error: true,
75+
payload: getErrorResponse(),
76+
},
77+
]),
78+
);
79+
});
80+
});

packages/core/src/cart/cart-request-sender.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ import BuyNowCartRequestBody from './buy-now-cart-request-body';
1414
import Cart from './cart';
1515
import CartRequestSender from './cart-request-sender';
1616
import { getCart } from './carts.mock';
17+
import { HeadlessCartRequestResponse } from './headless-cart';
18+
import { getHeadlessCartResponse } from './headless-cart/mocks/headless-cart.mock';
1719

1820
describe('CartRequestSender', () => {
1921
let cart: Cart;
2022
let cartRequestSender: CartRequestSender;
2123
let requestSender: RequestSender;
2224
let response: Response<Cart>;
25+
let headlessResponse: Response<HeadlessCartRequestResponse>;
2326

2427
beforeEach(() => {
2528
requestSender = createRequestSender();
@@ -75,4 +78,35 @@ describe('CartRequestSender', () => {
7578
});
7679
});
7780
});
81+
82+
describe('#loadCard', () => {
83+
const cartId = '123123';
84+
const host = 'https://test.com';
85+
86+
beforeEach(() => {
87+
headlessResponse = getResponse(getHeadlessCartResponse());
88+
89+
jest.spyOn(requestSender, 'get').mockResolvedValue(headlessResponse);
90+
});
91+
92+
it('get headless cart', async () => {
93+
await cartRequestSender.loadCard(cartId);
94+
95+
expect(requestSender.get).toHaveBeenCalledWith('/cart-information', {
96+
params: {
97+
cartId,
98+
},
99+
});
100+
});
101+
102+
it('get headless cart with host url', async () => {
103+
await cartRequestSender.loadCard(cartId, host);
104+
105+
expect(requestSender.get).toHaveBeenCalledWith('https://test.com/cart-information', {
106+
params: {
107+
cartId,
108+
},
109+
});
110+
});
111+
});
78112
});

packages/core/src/cart/headless-cart/headless-cart.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,12 @@ export default interface HeadlessCartResponse {
130130
};
131131
}>;
132132
};
133+
currency?: {
134+
display: {
135+
decimalPlaces: number;
136+
symbol: string;
137+
};
138+
name: string;
139+
code: string;
140+
};
133141
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { omit } from 'lodash';
2+
3+
import { getCart } from '../carts.mock';
4+
import { LineItem } from '../line-item';
5+
6+
import mapToLineItem from './map-to-cart-line-item';
7+
import { headlessLineItem } from './mocks/headless-cart.mock';
8+
9+
describe('mapToLineItem', () => {
10+
let headlessLineItemResponse: LineItem | undefined;
11+
12+
beforeEach(() => {
13+
headlessLineItemResponse = mapToLineItem(headlessLineItem());
14+
});
15+
16+
it('maps to line item', () => {
17+
const {
18+
lineItems: {
19+
physicalItems: [firstPhysicalItem],
20+
},
21+
} = getCart();
22+
23+
// TODO:: data is not yet fully compatible due to lack of information
24+
expect(headlessLineItemResponse).toEqual({
25+
// omits fields that do not exist to retrieve information via GQL
26+
...omit(firstPhysicalItem, ['categoryNames', 'categories', 'isShippingRequired']),
27+
// default props that are set due lack of information
28+
addedByPromotion: false,
29+
comparisonPrice: 0,
30+
extendedComparisonPrice: 0,
31+
retailPrice: 0,
32+
});
33+
});
34+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { omit } from 'lodash';
2+
3+
import { getCart } from '../carts.mock';
4+
import { LineItemMap } from '../index';
5+
6+
import mapToCartLineItems from './map-to-cart-line-items';
7+
import { getHeadlessCartResponse } from './mocks/headless-cart.mock';
8+
9+
describe('mapToCartLinesItem', () => {
10+
let headlessCartLineItemsResponse: LineItemMap | undefined;
11+
12+
beforeEach(() => {
13+
const {
14+
data: {
15+
site: { cart },
16+
},
17+
} = getHeadlessCartResponse();
18+
19+
headlessCartLineItemsResponse = mapToCartLineItems(
20+
cart?.lineItems ?? {
21+
physicalItems: [],
22+
digitalItems: [],
23+
giftCertificates: [],
24+
customItems: [],
25+
},
26+
);
27+
});
28+
29+
it('maps to cart line items', () => {
30+
const {
31+
lineItems: {
32+
physicalItems: [firstPhysicalItem],
33+
},
34+
} = getCart();
35+
36+
// TODO:: data is not yet fully compatible due to lack of information
37+
const physicalItem = {
38+
// omits fields that do not exist to retrieve information via GQL
39+
...omit(firstPhysicalItem, ['categoryNames', 'categories']),
40+
// default props that are set due lack of information
41+
addedByPromotion: false,
42+
comparisonPrice: 0,
43+
extendedComparisonPrice: 0,
44+
retailPrice: 0,
45+
};
46+
47+
expect(headlessCartLineItemsResponse).toEqual({
48+
physicalItems: [physicalItem],
49+
digitalItems: [],
50+
giftCertificates: [],
51+
customItems: [],
52+
});
53+
});
54+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Cart } from '@bigcommerce/checkout-sdk/payment-integration-api';
2+
3+
import { getCart } from '../carts.mock';
4+
5+
import mapToCart from './map-to-cart';
6+
import { getHeadlessCartResponse } from './mocks/headless-cart.mock';
7+
8+
describe('mapToCart', () => {
9+
let headlessCartResponse: Cart | undefined;
10+
11+
beforeEach(() => {
12+
headlessCartResponse = mapToCart(getHeadlessCartResponse().data.site);
13+
});
14+
15+
it('maps to internal cart', () => {
16+
const cart = getCart();
17+
18+
// TODO:: data is not yet fully compatible due to lack of information
19+
expect(headlessCartResponse).toEqual(
20+
expect.objectContaining({
21+
id: cart.id,
22+
isTaxIncluded: cart.isTaxIncluded,
23+
discountAmount: cart.discountAmount,
24+
baseAmount: cart.baseAmount,
25+
cartAmount: cart.cartAmount,
26+
createdTime: cart.createdTime,
27+
updatedTime: cart.updatedTime,
28+
coupons: cart.coupons.map((item) =>
29+
expect.objectContaining({
30+
id: item.id,
31+
code: item.code,
32+
couponType: item.couponType,
33+
discountedAmount: item.discountedAmount,
34+
}),
35+
),
36+
lineItems: expect.objectContaining({
37+
physicalItems: cart.lineItems.physicalItems.map((item) =>
38+
expect.objectContaining({
39+
id: item.id,
40+
variantId: item.variantId,
41+
productId: item.productId,
42+
sku: item.sku,
43+
name: item.name,
44+
url: item.url,
45+
quantity: item.quantity,
46+
brand: item.brand,
47+
isTaxable: item.isTaxable,
48+
imageUrl: item.imageUrl,
49+
discounts: item.discounts,
50+
discountAmount: item.discountAmount,
51+
couponAmount: item.couponAmount,
52+
listPrice: item.listPrice,
53+
salePrice: item.salePrice,
54+
extendedListPrice: item.extendedListPrice,
55+
extendedSalePrice: item.extendedSalePrice,
56+
isShippingRequired: item.isShippingRequired,
57+
options: expect.arrayContaining(item.options || []),
58+
// retailPrice, comparisonPrice, extendedComparisonPrice, addedByPromotion are not exist
59+
}),
60+
),
61+
}),
62+
discounts: cart.discounts,
63+
currency: cart.currency,
64+
// customerId is not exist
65+
// email is not exist
66+
}),
67+
);
68+
});
69+
});

packages/core/src/cart/headless-cart/map-to-cart.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import mapToCartLineItems from './map-to-cart-line-items';
55
import { HeadlessCartResponse } from './';
66

77
export default function mapToCart(headlessCartResponse: HeadlessCartResponse): Cart | undefined {
8-
const { cart, checkout } = headlessCartResponse;
8+
const { cart, checkout, currency } = headlessCartResponse;
99

10-
if (!cart || !checkout) {
10+
if (!cart || !checkout || !currency) {
1111
return;
1212
}
1313

@@ -22,11 +22,10 @@ export default function mapToCart(headlessCartResponse: HeadlessCartResponse): C
2222
isTaxIncluded: cart.isTaxIncluded,
2323
lineItems: mapToCartLineItems(cart.lineItems),
2424
currency: {
25-
code: cart.currencyCode,
26-
// TODO:: we do not have any information regarding to fields below (name, symbol, decimalPlaces) in the GraphQL Storefront doc (https://developer.bigcommerce.com/docs/storefront/cart-checkout/guide/graphql-storefront)
27-
name: '',
28-
symbol: '',
29-
decimalPlaces: 2,
25+
code: currency.code,
26+
name: currency.name,
27+
symbol: currency.display.symbol,
28+
decimalPlaces: currency.display.decimalPlaces,
3029
},
3130
createdTime: cart.createdAt.utc,
3231
updatedTime: cart.updatedAt.utc,

0 commit comments

Comments
 (0)