Skip to content

Commit

Permalink
Upgrading Paypal express (#19)
Browse files Browse the repository at this point in the history
* Upgrading Paypal express from Client-side only integration to a Server-side integration

* Fixing eslint issue
  • Loading branch information
Shagufa92 authored Feb 14, 2024
1 parent 528f858 commit 5b11d8d
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 776 deletions.
33 changes: 27 additions & 6 deletions src/paypal/paypalCreateOrder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
import {CreateOrderActions, CreateOrderData} from '@paypal/paypal-js/types/components/buttons';
import {CreateOrderRequestBody} from '@paypal/paypal-js/types/apis/orders';
import {getPaypalOrder} from 'src/paypal';
import {
IApiSuccessResponse,
IWalletPayCreateOrderRequest, IWalletPayCreateOrderResponse,
walletPayCreateOrder
} from '@boldcommerce/checkout-frontend-library';
import {API_RETRY} from 'src/types';
import {displayError} from 'src/actions';

export async function paypalCreateOrder(data: CreateOrderData, actions: CreateOrderActions): Promise<string> {
const paypalOrder: CreateOrderRequestBody = getPaypalOrder();
return actions.order.create(paypalOrder);
export async function paypalCreateOrder(): Promise<string> {

const payment: IWalletPayCreateOrderRequest = {
gateway_type: 'paypal',
payment_data: {
locale: navigator.language,
payment_type: 'paypal',
}
};

const paymentResult = await walletPayCreateOrder(payment, API_RETRY);
if(paymentResult.success) {
const {data} = paymentResult.response as IApiSuccessResponse;
const {payment_data} = data as IWalletPayCreateOrderResponse;
const orderId = payment_data.id as string;
return orderId;
} else {
displayError('There was an unknown error while loading the payment.', 'payment_gateway', 'unknown_error');
return '';
}
}
121 changes: 35 additions & 86 deletions src/paypal/paypalOnApprove.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,52 @@
import {OnApproveActions, OnApproveData} from '@paypal/paypal-js/types/components/buttons';
import {OrderResponseBody, ShippingInfo} from '@paypal/paypal-js/types/apis/orders';
import {OnApproveData} from '@paypal/paypal-js/types/components/buttons';
import {
callBillingAddressEndpoint,
callGuestCustomerEndpoint,
callShippingAddressEndpoint,
getFirstAndLastName,
getTotals,
isObjectEquals
} from 'src/utils';
import {formatPaypalToApiAddress} from 'src/paypal/formatPaypalToApiAddress';
import {
addPayment,
getCurrency,
IAddPaymentRequest,
setTaxes,
IWalletPayOnApproveRequest,
walletPayOnApprove,
} from '@boldcommerce/checkout-frontend-library';
import {API_RETRY} from 'src/types';
import {getPaypalGatewayPublicId} from 'src/paypal/managePaypalState';
import {orderProcessing, displayError} from 'src/actions';

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

// extract all shipping info
const { name, address: shippingAddress } = purchase_units[0].shipping as ShippingInfo;
const shippingNames = getFirstAndLastName(name?.full_name);

// extract all billing info
const {name: payerName, address: billingAddress} = payer;
const billingNames = {firstName: payerName?.given_name || '', lastName: payerName?.surname || ''};
const phone = payer.phone?.phone_number.national_number || '';
const email = payer.email_address || '';
const isBillingAddressFilled = (
!!billingAddress?.address_line_1
&& !!billingAddress.admin_area_1
&& !!billingAddress.admin_area_2
&& !!billingAddress.country_code
&& !!billingAddress.postal_code
);

// set customer
const customerResult = await callGuestCustomerEndpoint(billingNames.firstName, billingNames.lastName, email);
const success = customerResult.success;
if(!success){
displayError('There was an unknown error while validating your customer information.', 'generic', 'unknown_error');
return;
}

// check if shipping and billing are the same
const isSameNames = isObjectEquals(shippingNames, billingNames);
const isSameAddress = isBillingAddressFilled && isObjectEquals(shippingAddress, billingAddress);
const isBillingSame = isSameNames && isSameAddress;
const formattedShippingAddress = formatPaypalToApiAddress(shippingAddress, shippingNames.firstName, shippingNames.lastName, phone);
const formattedBillingAddress = formatPaypalToApiAddress(isBillingAddressFilled ? billingAddress : shippingAddress, billingNames.firstName, billingNames.lastName, phone);

// check and update shipping address
const shippingAddressResponse = await callShippingAddressEndpoint(formattedShippingAddress, true);
if(!shippingAddressResponse.success){
displayError('There was an unknown error while validating your shipping address.', 'shipping', 'unknown_error');
return;
}


// check and update billing address
const billingAddressToSet = isBillingSame ? formattedShippingAddress : formattedBillingAddress;
const billingAddressResponse = await callBillingAddressEndpoint(billingAddressToSet, (!isBillingSame && isBillingAddressFilled));
if(!billingAddressResponse.success){
displayError('There was an unknown error while validating your billing address.', 'generic', 'unknown_error');
return;
}


// update taxes

const taxResponse = await setTaxes(API_RETRY);
if(!taxResponse.success){
displayError('There was an unknown error while calculating the taxes.', 'payment_gateway', 'no_tax');
return;
}


// add payment
const totals = getTotals();
const payment: IAddPaymentRequest = {
token: `${id}:${payer.payer_id}`,
nonce: `${id}:${payer.payer_id}`, // TODO: Temporarily required - It is not in the API documentation, but required for Paypal Express
gateway_public_id: getPaypalGatewayPublicId(),
currency: currencyCode,
amount: totals.totalAmountDue,
wallet_pay_type: 'paypal',
} as IAddPaymentRequest;
const paymentResult = await addPayment(payment, API_RETRY);
if(!paymentResult.success){
displayError('There was an unknown error while processing your payment.', 'payment_gateway', 'unknown_error');
return;
const body: IWalletPayOnApproveRequest = {
gateway_type: 'paypal',
payment_data: {
locale: navigator.language,
paypal_order_id: data.orderID
}
};

const res = await walletPayOnApprove(body, API_RETRY);

if (!res.success) {
displayError('There was an unknown error while processing your payment.', 'payment_gateway', 'unknown_error');
return;
}

const totals = getTotals();
const payment: IAddPaymentRequest = {
token: `${data.orderID}:${data.payerID}`,
nonce: `${data.orderID}:${data.payerID}`, // TODO: Temporarily required - It is not in the API documentation, but required for Paypal Express
gateway_public_id: getPaypalGatewayPublicId(),
currency: currencyCode,
amount: totals.totalAmountDue,
wallet_pay_type: 'paypal',
} as IAddPaymentRequest;
const paymentResult = await addPayment(payment, API_RETRY);
if (!paymentResult.success) {
displayError('There was an unknown error while processing your payment.', 'payment_gateway', 'unknown_error');
return;
}

// finalize order
orderProcessing();

// finalize order
orderProcessing();
});
}
81 changes: 15 additions & 66 deletions src/paypal/paypalOnShippingChange.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,24 @@
import {OnShippingChangeActions, OnShippingChangeData} from '@paypal/paypal-js/types/components/buttons';
import {API_RETRY,} from 'src';
import {
getPaypalPatchOperations,
API_RETRY,
formatPaypalToApiAddress,
isSimilarStrings,
callShippingAddressEndpoint,
paypalConstants,
getPhoneNumber
} from 'src';
import {
changeShippingLine,
estimateShippingLines,
estimateTaxes,
getOrderInitialData,
getShipping,
getShippingLines,
setTaxes,
IWalletPayOnShippingRequest,
walletPayOnShipping,
} from '@boldcommerce/checkout-frontend-library';
import {OrderResponseBody, UpdateOrderRequestBody} from '@paypal/paypal-js/types/apis/orders';
import {OrderResponseBody} from '@paypal/paypal-js/types/apis/orders';

export async function paypalOnShippingChange(data: OnShippingChangeData, actions: OnShippingChangeActions): Promise<void|OrderResponseBody> {
const {shipping_address: address, selected_shipping_option: selectedOption} = data;
const {reject, order: {patch: patch}} = actions;
const {MAX_STRING_LENGTH: maxStringSize} = paypalConstants;
const {general_settings} = getOrderInitialData();
const rsaEnabled = general_settings.checkout_process.rsa_enabled;

if (address) {
const formattedAddress = formatPaypalToApiAddress(address, undefined, undefined , getPhoneNumber());
let success = false;
if (rsaEnabled) {
const shippingLinesResponse = await estimateShippingLines(formattedAddress, API_RETRY);
if (shippingLinesResponse.success) {
success = true;
}
} else {
const shippingAddressResponse = await callShippingAddressEndpoint(formattedAddress, false);
if (!shippingAddressResponse.success) {
return reject();
}
const shippingLinesResponse = await getShippingLines(API_RETRY);
if (shippingLinesResponse.success) {
success = true;
}
const body: IWalletPayOnShippingRequest = {
gateway_type: 'paypal',
payment_data: {
locale: navigator.language,
paypal_order_id: data.orderID,
shipping_address: data.shipping_address,
shipping_options: data.selected_shipping_option,
}
};


if (success) {
const {selected_shipping: selectedShipping, available_shipping_lines: shippingLines} = getShipping();
if (selectedOption) {
const option = shippingLines.find(line => isSimilarStrings(line.description.substring(0, maxStringSize), selectedOption.label));
option && await changeShippingLine(option.id, API_RETRY);
} else if (!selectedShipping && shippingLines.length > 0) {
await changeShippingLine(shippingLines[0].id, API_RETRY);
}
await getShippingLines(API_RETRY);
}
const res = await walletPayOnShipping(body, API_RETRY);
if (!res.success) {
return actions.reject();
}

let taxResponse;
if (rsaEnabled && address) {
const formattedAddress = formatPaypalToApiAddress(address, undefined, undefined , getPhoneNumber());
taxResponse = await estimateTaxes(formattedAddress, API_RETRY);
} else {
taxResponse = await setTaxes(API_RETRY);
}

if (taxResponse.success) {
const patchOperations = getPaypalPatchOperations(!!selectedOption);
return await patch(patchOperations as UpdateOrderRequestBody);
}

return reject();
}
87 changes: 37 additions & 50 deletions tests/paypal/paypalCreateOrder.test.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,49 @@
import {getPaypalOrder, paypalCreateOrder} from 'src';
import {CreateOrderActions} from '@paypal/paypal-js/types/components/buttons';
import {AmountWithBreakdown, AmountWithCurrencyCode} from '@paypal/paypal-js';
import {CreateOrderRequestBody, PurchaseItem} from '@paypal/paypal-js/types/apis/orders';
import {displayError, paypalCreateOrder} from 'src';
import {mocked} from 'jest-mock';

jest.mock('src/paypal/getPaypalOrder');
const getPaypalOrderMock = mocked(getPaypalOrder, true);
const createOrderActionMock: CreateOrderActions = {order: {create: jest.fn()}};
import {
baseReturnObject,
IWalletPayCreateOrderResponse,
walletPayCreateOrder
} from '@boldcommerce/checkout-frontend-library';
import {applicationStateMock} from '@boldcommerce/checkout-frontend-library/lib/variables/mocks';

jest.mock('@boldcommerce/checkout-frontend-library/lib/walletPay/walletPayCreateOrder');
jest.mock('src/actions/displayError');
const walletPayCreateOrderMock = mocked(walletPayCreateOrder, true);
const displayErrorMock = mocked(displayError, true);

describe('testing paypalCreateOrder function', () => {
const publicOrderId = 'abc123';
const breakdownItemMock: AmountWithCurrencyCode = {
currency_code: 'USD',
value: '0.00',
};
const breakdownItem100Mock: AmountWithCurrencyCode = {
currency_code: 'USD',
value: '100.00',
};

const amountWithBreakdownMock: AmountWithBreakdown = {
currency_code: 'USD',
value: '100.00',
breakdown: {
item_total: breakdownItem100Mock,
shipping: breakdownItemMock,
tax_total: breakdownItemMock,
discount: breakdownItemMock,
shipping_discount: breakdownItemMock,
}
};
const itemsMock: Array<PurchaseItem> = [
{
name: 'Some Name',
quantity: '1',
unit_amount: breakdownItem100Mock
}
];
const paypalOrderMock: CreateOrderRequestBody = {
purchase_units: [{
custom_id: publicOrderId,
amount: amountWithBreakdownMock,
items: itemsMock
}]
};

beforeEach(() => {
jest.clearAllMocks();
getPaypalOrderMock.mockReturnValue(paypalOrderMock);
});

test('testing call paypalCreateOrder success', async () => {
await paypalCreateOrder({paymentSource: 'paypal'}, createOrderActionMock);
test('testing with successful call', async () => {
const response: IWalletPayCreateOrderResponse = {
payment_data: {
id: 'test-order'
},
application_state: applicationStateMock
};

const paymentReturn = {...baseReturnObject};
paymentReturn.success = true;
paymentReturn.response = {data: response};
walletPayCreateOrderMock.mockReturnValue(Promise.resolve(paymentReturn));

const result = await paypalCreateOrder();
expect(result).toBe('test-order');
});


expect(getPaypalOrderMock).toHaveBeenCalledTimes(1);
expect(createOrderActionMock.order.create).toHaveBeenCalledTimes(1);
expect(createOrderActionMock.order.create).toHaveBeenCalledWith(paypalOrderMock);
test('testing with unsuccessful call', async () => {
const paymentReturn = {...baseReturnObject};
paymentReturn.success = false;
walletPayCreateOrderMock.mockReturnValue(Promise.resolve(paymentReturn));

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

});
Loading

0 comments on commit 5b11d8d

Please sign in to comment.