Skip to content

Commit 5b11d8d

Browse files
authored
Upgrading Paypal express (#19)
* Upgrading Paypal express from Client-side only integration to a Server-side integration * Fixing eslint issue
1 parent 528f858 commit 5b11d8d

File tree

6 files changed

+171
-776
lines changed

6 files changed

+171
-776
lines changed

src/paypal/paypalCreateOrder.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
1-
import {CreateOrderActions, CreateOrderData} from '@paypal/paypal-js/types/components/buttons';
2-
import {CreateOrderRequestBody} from '@paypal/paypal-js/types/apis/orders';
3-
import {getPaypalOrder} from 'src/paypal';
1+
import {
2+
IApiSuccessResponse,
3+
IWalletPayCreateOrderRequest, IWalletPayCreateOrderResponse,
4+
walletPayCreateOrder
5+
} from '@boldcommerce/checkout-frontend-library';
6+
import {API_RETRY} from 'src/types';
7+
import {displayError} from 'src/actions';
48

5-
export async function paypalCreateOrder(data: CreateOrderData, actions: CreateOrderActions): Promise<string> {
6-
const paypalOrder: CreateOrderRequestBody = getPaypalOrder();
7-
return actions.order.create(paypalOrder);
9+
export async function paypalCreateOrder(): Promise<string> {
10+
11+
const payment: IWalletPayCreateOrderRequest = {
12+
gateway_type: 'paypal',
13+
payment_data: {
14+
locale: navigator.language,
15+
payment_type: 'paypal',
16+
}
17+
};
18+
19+
const paymentResult = await walletPayCreateOrder(payment, API_RETRY);
20+
if(paymentResult.success) {
21+
const {data} = paymentResult.response as IApiSuccessResponse;
22+
const {payment_data} = data as IWalletPayCreateOrderResponse;
23+
const orderId = payment_data.id as string;
24+
return orderId;
25+
} else {
26+
displayError('There was an unknown error while loading the payment.', 'payment_gateway', 'unknown_error');
27+
return '';
28+
}
829
}

src/paypal/paypalOnApprove.ts

Lines changed: 35 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,52 @@
1-
import {OnApproveActions, OnApproveData} from '@paypal/paypal-js/types/components/buttons';
2-
import {OrderResponseBody, ShippingInfo} from '@paypal/paypal-js/types/apis/orders';
1+
import {OnApproveData} from '@paypal/paypal-js/types/components/buttons';
32
import {
4-
callBillingAddressEndpoint,
5-
callGuestCustomerEndpoint,
6-
callShippingAddressEndpoint,
7-
getFirstAndLastName,
83
getTotals,
9-
isObjectEquals
104
} from 'src/utils';
11-
import {formatPaypalToApiAddress} from 'src/paypal/formatPaypalToApiAddress';
125
import {
136
addPayment,
147
getCurrency,
158
IAddPaymentRequest,
16-
setTaxes,
9+
IWalletPayOnApproveRequest,
10+
walletPayOnApprove,
1711
} from '@boldcommerce/checkout-frontend-library';
1812
import {API_RETRY} from 'src/types';
1913
import {getPaypalGatewayPublicId} from 'src/paypal/managePaypalState';
2014
import {orderProcessing, displayError} from 'src/actions';
2115

22-
export async function paypalOnApprove(data: OnApproveData, actions: OnApproveActions): Promise<void> {
16+
export async function paypalOnApprove(data: OnApproveData): Promise<void> {
2317
const {iso_code: currencyCode} = getCurrency();
24-
return actions.order?.get().then(async ({ id, payer, purchase_units }: OrderResponseBody) => {
2518

26-
// extract all shipping info
27-
const { name, address: shippingAddress } = purchase_units[0].shipping as ShippingInfo;
28-
const shippingNames = getFirstAndLastName(name?.full_name);
29-
30-
// extract all billing info
31-
const {name: payerName, address: billingAddress} = payer;
32-
const billingNames = {firstName: payerName?.given_name || '', lastName: payerName?.surname || ''};
33-
const phone = payer.phone?.phone_number.national_number || '';
34-
const email = payer.email_address || '';
35-
const isBillingAddressFilled = (
36-
!!billingAddress?.address_line_1
37-
&& !!billingAddress.admin_area_1
38-
&& !!billingAddress.admin_area_2
39-
&& !!billingAddress.country_code
40-
&& !!billingAddress.postal_code
41-
);
42-
43-
// set customer
44-
const customerResult = await callGuestCustomerEndpoint(billingNames.firstName, billingNames.lastName, email);
45-
const success = customerResult.success;
46-
if(!success){
47-
displayError('There was an unknown error while validating your customer information.', 'generic', 'unknown_error');
48-
return;
49-
}
50-
51-
// check if shipping and billing are the same
52-
const isSameNames = isObjectEquals(shippingNames, billingNames);
53-
const isSameAddress = isBillingAddressFilled && isObjectEquals(shippingAddress, billingAddress);
54-
const isBillingSame = isSameNames && isSameAddress;
55-
const formattedShippingAddress = formatPaypalToApiAddress(shippingAddress, shippingNames.firstName, shippingNames.lastName, phone);
56-
const formattedBillingAddress = formatPaypalToApiAddress(isBillingAddressFilled ? billingAddress : shippingAddress, billingNames.firstName, billingNames.lastName, phone);
57-
58-
// check and update shipping address
59-
const shippingAddressResponse = await callShippingAddressEndpoint(formattedShippingAddress, true);
60-
if(!shippingAddressResponse.success){
61-
displayError('There was an unknown error while validating your shipping address.', 'shipping', 'unknown_error');
62-
return;
63-
}
64-
65-
66-
// check and update billing address
67-
const billingAddressToSet = isBillingSame ? formattedShippingAddress : formattedBillingAddress;
68-
const billingAddressResponse = await callBillingAddressEndpoint(billingAddressToSet, (!isBillingSame && isBillingAddressFilled));
69-
if(!billingAddressResponse.success){
70-
displayError('There was an unknown error while validating your billing address.', 'generic', 'unknown_error');
71-
return;
72-
}
73-
74-
75-
// update taxes
76-
77-
const taxResponse = await setTaxes(API_RETRY);
78-
if(!taxResponse.success){
79-
displayError('There was an unknown error while calculating the taxes.', 'payment_gateway', 'no_tax');
80-
return;
81-
}
82-
83-
84-
// add payment
85-
const totals = getTotals();
86-
const payment: IAddPaymentRequest = {
87-
token: `${id}:${payer.payer_id}`,
88-
nonce: `${id}:${payer.payer_id}`, // TODO: Temporarily required - It is not in the API documentation, but required for Paypal Express
89-
gateway_public_id: getPaypalGatewayPublicId(),
90-
currency: currencyCode,
91-
amount: totals.totalAmountDue,
92-
wallet_pay_type: 'paypal',
93-
} as IAddPaymentRequest;
94-
const paymentResult = await addPayment(payment, API_RETRY);
95-
if(!paymentResult.success){
96-
displayError('There was an unknown error while processing your payment.', 'payment_gateway', 'unknown_error');
97-
return;
19+
const body: IWalletPayOnApproveRequest = {
20+
gateway_type: 'paypal',
21+
payment_data: {
22+
locale: navigator.language,
23+
paypal_order_id: data.orderID
9824
}
25+
};
26+
27+
const res = await walletPayOnApprove(body, API_RETRY);
28+
29+
if (!res.success) {
30+
displayError('There was an unknown error while processing your payment.', 'payment_gateway', 'unknown_error');
31+
return;
32+
}
33+
34+
const totals = getTotals();
35+
const payment: IAddPaymentRequest = {
36+
token: `${data.orderID}:${data.payerID}`,
37+
nonce: `${data.orderID}:${data.payerID}`, // TODO: Temporarily required - It is not in the API documentation, but required for Paypal Express
38+
gateway_public_id: getPaypalGatewayPublicId(),
39+
currency: currencyCode,
40+
amount: totals.totalAmountDue,
41+
wallet_pay_type: 'paypal',
42+
} as IAddPaymentRequest;
43+
const paymentResult = await addPayment(payment, API_RETRY);
44+
if (!paymentResult.success) {
45+
displayError('There was an unknown error while processing your payment.', 'payment_gateway', 'unknown_error');
46+
return;
47+
}
48+
49+
// finalize order
50+
orderProcessing();
9951

100-
// finalize order
101-
orderProcessing();
102-
});
10352
}

src/paypal/paypalOnShippingChange.ts

Lines changed: 15 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,24 @@
11
import {OnShippingChangeActions, OnShippingChangeData} from '@paypal/paypal-js/types/components/buttons';
2+
import {API_RETRY,} from 'src';
23
import {
3-
getPaypalPatchOperations,
4-
API_RETRY,
5-
formatPaypalToApiAddress,
6-
isSimilarStrings,
7-
callShippingAddressEndpoint,
8-
paypalConstants,
9-
getPhoneNumber
10-
} from 'src';
11-
import {
12-
changeShippingLine,
13-
estimateShippingLines,
14-
estimateTaxes,
15-
getOrderInitialData,
16-
getShipping,
17-
getShippingLines,
18-
setTaxes,
4+
IWalletPayOnShippingRequest,
5+
walletPayOnShipping,
196
} from '@boldcommerce/checkout-frontend-library';
20-
import {OrderResponseBody, UpdateOrderRequestBody} from '@paypal/paypal-js/types/apis/orders';
7+
import {OrderResponseBody} from '@paypal/paypal-js/types/apis/orders';
218

229
export async function paypalOnShippingChange(data: OnShippingChangeData, actions: OnShippingChangeActions): Promise<void|OrderResponseBody> {
23-
const {shipping_address: address, selected_shipping_option: selectedOption} = data;
24-
const {reject, order: {patch: patch}} = actions;
25-
const {MAX_STRING_LENGTH: maxStringSize} = paypalConstants;
26-
const {general_settings} = getOrderInitialData();
27-
const rsaEnabled = general_settings.checkout_process.rsa_enabled;
28-
29-
if (address) {
30-
const formattedAddress = formatPaypalToApiAddress(address, undefined, undefined , getPhoneNumber());
31-
let success = false;
32-
if (rsaEnabled) {
33-
const shippingLinesResponse = await estimateShippingLines(formattedAddress, API_RETRY);
34-
if (shippingLinesResponse.success) {
35-
success = true;
36-
}
37-
} else {
38-
const shippingAddressResponse = await callShippingAddressEndpoint(formattedAddress, false);
39-
if (!shippingAddressResponse.success) {
40-
return reject();
41-
}
42-
const shippingLinesResponse = await getShippingLines(API_RETRY);
43-
if (shippingLinesResponse.success) {
44-
success = true;
45-
}
10+
const body: IWalletPayOnShippingRequest = {
11+
gateway_type: 'paypal',
12+
payment_data: {
13+
locale: navigator.language,
14+
paypal_order_id: data.orderID,
15+
shipping_address: data.shipping_address,
16+
shipping_options: data.selected_shipping_option,
4617
}
18+
};
4719

48-
49-
if (success) {
50-
const {selected_shipping: selectedShipping, available_shipping_lines: shippingLines} = getShipping();
51-
if (selectedOption) {
52-
const option = shippingLines.find(line => isSimilarStrings(line.description.substring(0, maxStringSize), selectedOption.label));
53-
option && await changeShippingLine(option.id, API_RETRY);
54-
} else if (!selectedShipping && shippingLines.length > 0) {
55-
await changeShippingLine(shippingLines[0].id, API_RETRY);
56-
}
57-
await getShippingLines(API_RETRY);
58-
}
20+
const res = await walletPayOnShipping(body, API_RETRY);
21+
if (!res.success) {
22+
return actions.reject();
5923
}
60-
61-
let taxResponse;
62-
if (rsaEnabled && address) {
63-
const formattedAddress = formatPaypalToApiAddress(address, undefined, undefined , getPhoneNumber());
64-
taxResponse = await estimateTaxes(formattedAddress, API_RETRY);
65-
} else {
66-
taxResponse = await setTaxes(API_RETRY);
67-
}
68-
69-
if (taxResponse.success) {
70-
const patchOperations = getPaypalPatchOperations(!!selectedOption);
71-
return await patch(patchOperations as UpdateOrderRequestBody);
72-
}
73-
74-
return reject();
7524
}
Lines changed: 37 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,49 @@
1-
import {getPaypalOrder, paypalCreateOrder} from 'src';
2-
import {CreateOrderActions} from '@paypal/paypal-js/types/components/buttons';
3-
import {AmountWithBreakdown, AmountWithCurrencyCode} from '@paypal/paypal-js';
4-
import {CreateOrderRequestBody, PurchaseItem} from '@paypal/paypal-js/types/apis/orders';
1+
import {displayError, paypalCreateOrder} from 'src';
52
import {mocked} from 'jest-mock';
6-
7-
jest.mock('src/paypal/getPaypalOrder');
8-
const getPaypalOrderMock = mocked(getPaypalOrder, true);
9-
const createOrderActionMock: CreateOrderActions = {order: {create: jest.fn()}};
3+
import {
4+
baseReturnObject,
5+
IWalletPayCreateOrderResponse,
6+
walletPayCreateOrder
7+
} from '@boldcommerce/checkout-frontend-library';
8+
import {applicationStateMock} from '@boldcommerce/checkout-frontend-library/lib/variables/mocks';
9+
10+
jest.mock('@boldcommerce/checkout-frontend-library/lib/walletPay/walletPayCreateOrder');
11+
jest.mock('src/actions/displayError');
12+
const walletPayCreateOrderMock = mocked(walletPayCreateOrder, true);
13+
const displayErrorMock = mocked(displayError, true);
1014

1115
describe('testing paypalCreateOrder function', () => {
12-
const publicOrderId = 'abc123';
13-
const breakdownItemMock: AmountWithCurrencyCode = {
14-
currency_code: 'USD',
15-
value: '0.00',
16-
};
17-
const breakdownItem100Mock: AmountWithCurrencyCode = {
18-
currency_code: 'USD',
19-
value: '100.00',
20-
};
21-
22-
const amountWithBreakdownMock: AmountWithBreakdown = {
23-
currency_code: 'USD',
24-
value: '100.00',
25-
breakdown: {
26-
item_total: breakdownItem100Mock,
27-
shipping: breakdownItemMock,
28-
tax_total: breakdownItemMock,
29-
discount: breakdownItemMock,
30-
shipping_discount: breakdownItemMock,
31-
}
32-
};
33-
const itemsMock: Array<PurchaseItem> = [
34-
{
35-
name: 'Some Name',
36-
quantity: '1',
37-
unit_amount: breakdownItem100Mock
38-
}
39-
];
40-
const paypalOrderMock: CreateOrderRequestBody = {
41-
purchase_units: [{
42-
custom_id: publicOrderId,
43-
amount: amountWithBreakdownMock,
44-
items: itemsMock
45-
}]
46-
};
47-
4816
beforeEach(() => {
4917
jest.clearAllMocks();
50-
getPaypalOrderMock.mockReturnValue(paypalOrderMock);
5118
});
5219

53-
test('testing call paypalCreateOrder success', async () => {
54-
await paypalCreateOrder({paymentSource: 'paypal'}, createOrderActionMock);
20+
test('testing with successful call', async () => {
21+
const response: IWalletPayCreateOrderResponse = {
22+
payment_data: {
23+
id: 'test-order'
24+
},
25+
application_state: applicationStateMock
26+
};
27+
28+
const paymentReturn = {...baseReturnObject};
29+
paymentReturn.success = true;
30+
paymentReturn.response = {data: response};
31+
walletPayCreateOrderMock.mockReturnValue(Promise.resolve(paymentReturn));
32+
33+
const result = await paypalCreateOrder();
34+
expect(result).toBe('test-order');
35+
});
36+
5537

56-
expect(getPaypalOrderMock).toHaveBeenCalledTimes(1);
57-
expect(createOrderActionMock.order.create).toHaveBeenCalledTimes(1);
58-
expect(createOrderActionMock.order.create).toHaveBeenCalledWith(paypalOrderMock);
38+
test('testing with unsuccessful call', async () => {
39+
const paymentReturn = {...baseReturnObject};
40+
paymentReturn.success = false;
41+
walletPayCreateOrderMock.mockReturnValue(Promise.resolve(paymentReturn));
5942

43+
const result = await paypalCreateOrder();
44+
expect(displayErrorMock).toHaveBeenCalledTimes(1);
45+
expect(displayErrorMock).toHaveBeenCalledWith('There was an unknown error while loading the payment.', 'payment_gateway', 'unknown_error');
46+
expect(result).toBe('');
6047
});
6148

6249
});

0 commit comments

Comments
 (0)