Skip to content

feat(payment): PAYPAL-4936 Update PayPalCommerceIntegrationService by adding a proxy request functionality for order creation process #2803

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,82 @@ describe('PayPalCommerceIntegrationService', () => {
});
});

describe('#createPaymentOrderIntent', () => {
const providerId = 'paypalcommerce.paypal';
const paymentOrderIntentResponse = {
orderId: '10',
approveUrl: 'test-url',
};

beforeEach(() => {
jest.spyOn(paypalCommerceRequestSender, 'createPaymentOrderIntent').mockResolvedValue(
paymentOrderIntentResponse,
);
});

it('throws an error if cart does not exist', async () => {
const err = new Error('cart does not exist');

jest.spyOn(paymentIntegrationService.getState(), 'getCartOrThrow').mockImplementation(
() => {
throw err;
},
);

try {
await subject.createPaymentOrderIntent(providerId);
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe('cart does not exist');
}
});

it('successfully creation of payment order intent', async () => {
const orderId = await subject.createPaymentOrderIntent(providerId);

expect(paypalCommerceRequestSender.createPaymentOrderIntent).toHaveBeenCalledWith(
providerId,
cart.id,
undefined,
);

expect(orderId).toEqual(paymentOrderIntentResponse.orderId);
});
});

describe('#proxyTokenizationPayment', () => {
const redirectUrl = 'redirect-url';

beforeEach(() => {
jest.spyOn(paypalCommerceRequestSender, 'getRedirectToCheckoutUrl').mockResolvedValue(
redirectUrl,
);

Object.defineProperty(window, 'location', {
value: {
assign: jest.fn(),
},
});
});

it('throws an error if orderId is not provided', async () => {
try {
await subject.proxyTokenizationPayment();
} catch (error) {
expect(error).toBeInstanceOf(MissingDataError);
}
});

it('successfully tokenization payment', async () => {
await subject.proxyTokenizationPayment('10');

expect(paypalCommerceRequestSender.getRedirectToCheckoutUrl).toHaveBeenCalledWith(
'/redirect-to-checkout',
);
expect(window.location.assign).toHaveBeenCalledWith(redirectUrl);
});
});

describe('#submitPayment', () => {
it('successfully submits payment', async () => {
jest.spyOn(paymentIntegrationService, 'submitPayment').mockImplementation(jest.fn());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import PayPalCommerceRequestSender from './paypal-commerce-request-sender';
import PayPalCommerceScriptLoader from './paypal-commerce-script-loader';
import {
CreatePaymentOrderIntentOptions,
PayPalButtonStyleOptions,
PayPalBuyNowInitializeOptions,
PayPalCommerceInitializationData,
Expand Down Expand Up @@ -202,6 +203,38 @@ export default class PayPalCommerceIntegrationService {
});
}

async proxyTokenizationPayment(orderId?: string): Promise<void> {
const state = this.paymentIntegrationService.getState();

if (!orderId) {
throw new MissingDataError(MissingDataErrorType.MissingOrderId);
}

const host = state.getHost();
const path = 'redirect-to-checkout';

const redirectUrl = await this.paypalCommerceRequestSender.getRedirectToCheckoutUrl(
host ? `${host}/${path}` : `/${path}`,
);

window.location.assign(redirectUrl);
}

async createPaymentOrderIntent(
providerId: string,
options?: CreatePaymentOrderIntentOptions,
): Promise<string> {
const cartId = this.paymentIntegrationService.getState().getCartOrThrow().id;

const { orderId } = await this.paypalCommerceRequestSender.createPaymentOrderIntent(
providerId,
cartId,
options,
);

return orderId;
}

/**
*
* Shipping options methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,115 @@ describe('PayPalCommerceRequestSender', () => {
}),
);
});

describe('#createPaymentOrderIntent', () => {
const requestBody = {
walletEntityId: 'paypalcommerce.paypal',
cartId: '12341234',
};

const mockRequest = ({
orderId = '10',
errors = [],
}: {
orderId?: string;
errors?: Array<{ message: string }>;
} = {}) => {
const requestResponseMock = getResponse({
data: {
payment: {
paymentWallet: {
createPaymentWalletIntent: {
paymentWalletIntentData: { orderId },
errors,
},
},
},
},
});

jest.spyOn(requestSender, 'post').mockReturnValue(Promise.resolve(requestResponseMock));
};

beforeEach(() => {
mockRequest();
});

it('should throw an error', async () => {
mockRequest({ errors: [{ message: 'error message' }] });

try {
await paypalCommerceRequestSender.createPaymentOrderIntent(
requestBody.walletEntityId,
requestBody.cartId,
);
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe('error message');
}
});

describe('create order', () => {
it('with provided data', async () => {
await paypalCommerceRequestSender.createPaymentOrderIntent(
requestBody.walletEntityId,
requestBody.cartId,
);

expect(requestSender.post).toHaveBeenCalledWith(
'http://localhost/api/wallet-buttons/create-payment-wallet-intent',
expect.objectContaining({
body: requestBody,
}),
);
});
});
});

describe('#getRedirectToCheckoutUrl', () => {
const url = 'https://example.com';
const redirectedCheckoutUrl = 'https://redirect-to-checkout.com';

const mockRequest = ({
createCartRedirectUrls = { redirectUrls: { redirectedCheckoutUrl } },
}: {
createCartRedirectUrls?: { redirectUrls: { redirectedCheckoutUrl: string } | null };
} = {}) => {
const requestResponseMock = getResponse({
data: {
cart: {
createCartRedirectUrls,
},
},
});

jest.spyOn(requestSender, 'get').mockReturnValue(Promise.resolve(requestResponseMock));
};

it('get redirect to checkout url', async () => {
mockRequest();

const redirectToCheckoutUrl =
await paypalCommerceRequestSender.getRedirectToCheckoutUrl(url);

expect(requestSender.get).toHaveBeenCalledWith(url, undefined);

expect(redirectToCheckoutUrl).toEqual(redirectedCheckoutUrl);
});

it('should throw an error if there is no redirect url', async () => {
mockRequest({
createCartRedirectUrls: {
redirectUrls: null,
},
});

try {
await paypalCommerceRequestSender.getRedirectToCheckoutUrl(url);
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe('Failed to redirection to checkout page');
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
} from '@bigcommerce/checkout-sdk/payment-integration-api';

import {
CreatePaymentOrderIntentOptions,
CreatePaymentOrderIntentResponse,
CreateRedirectToCheckoutResponse,
PayPalCreateOrderRequestBody,
PayPalOrderData,
PayPalOrderStatusData,
Expand Down Expand Up @@ -69,4 +72,67 @@ export default class PayPalCommerceRequestSender {

return res.body;
}

async createPaymentOrderIntent(
walletEntityId: string,
cartId: string,
options?: CreatePaymentOrderIntentOptions,
): Promise<PayPalOrderData> {
const url = `${window.location.origin}/api/wallet-buttons/create-payment-wallet-intent`;

const requestOptions: CreatePaymentOrderIntentOptions = {
body: {
...options?.body,
walletEntityId,
cartId,
},
};

const res = await this.requestSender.post<CreatePaymentOrderIntentResponse>(
url,
requestOptions,
);

const {
data: {
payment: {
paymentWallet: {
createPaymentWalletIntent: { paymentWalletIntentData, errors },
},
},
},
} = res.body;

const errorMessage = errors[0]?.message;

if (errorMessage) {
throw new Error(errorMessage);
}

return {
orderId: paymentWalletIntentData.orderId,
approveUrl: paymentWalletIntentData.approvalUrl,
};
}

async getRedirectToCheckoutUrl(
url: string,
options?: CreatePaymentOrderIntentOptions,
): Promise<string> {
const res = await this.requestSender.get<CreateRedirectToCheckoutResponse>(url, options);

const {
data: {
cart: {
createCartRedirectUrls: { redirectUrls },
},
},
} = res.body;

if (!redirectUrls?.redirectedCheckoutUrl) {
throw new Error('Failed to redirection to checkout page');
}

return redirectUrls.redirectedCheckoutUrl;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BuyNowCartRequestBody,
HostedInstrument,
RequestOptions,
ShippingOption,
VaultedInstrument,
} from '@bigcommerce/checkout-sdk/payment-integration-api';
Expand Down Expand Up @@ -616,3 +617,35 @@ export interface PayPalCreateOrderCardFieldsResponse {
orderId: string;
setupToken?: string;
}

export interface CreatePaymentOrderIntentOptions extends RequestOptions {
body?: { walletEntityId: string; cartId: string };
}

export interface CreatePaymentOrderIntentResponse {
data: {
payment: {
paymentWallet: {
createPaymentWalletIntent: {
errors: Array<{
location: Array<{ line: string; column: string }>;
message: string;
}>;
paymentWalletIntentData: {
__typename: string;
approvalUrl: string;
orderId: string;
};
};
};
};
};
}

export interface CreateRedirectToCheckoutResponse {
data: {
cart: {
createCartRedirectUrls: { redirectUrls: { redirectedCheckoutUrl: string } | null };
};
};
}