Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrading Paypal express #19

Merged
merged 2 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading