From a6ff26d60afa11b974a44ef5782b349581bb16af Mon Sep 17 00:00:00 2001 From: bc-nick Date: Mon, 24 Mar 2025 22:09:58 +0100 Subject: [PATCH] feat(payment): PAYPAL-4936 PayPalCommerceIntegrationService updates --- ...aypal-commerce-integration-service.spec.ts | 76 ++++++++++++ .../paypal-commerce-integration-service.ts | 33 ++++++ .../paypal-commerce-request-sender.spec.ts | 111 ++++++++++++++++++ .../src/paypal-commerce-request-sender.ts | 66 +++++++++++ .../src/paypal-commerce-types.ts | 33 ++++++ 5 files changed, 319 insertions(+) diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-integration-service.spec.ts b/packages/paypal-commerce-integration/src/paypal-commerce-integration-service.spec.ts index 36384dfb3e..ad150c4cf2 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-integration-service.spec.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-integration-service.spec.ts @@ -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()); diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-integration-service.ts b/packages/paypal-commerce-integration/src/paypal-commerce-integration-service.ts index 63dabe31e1..0e2eaf2a03 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-integration-service.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-integration-service.ts @@ -18,6 +18,7 @@ import { import PayPalCommerceRequestSender from './paypal-commerce-request-sender'; import PayPalCommerceScriptLoader from './paypal-commerce-script-loader'; import { + CreatePaymentOrderIntentOptions, PayPalButtonStyleOptions, PayPalBuyNowInitializeOptions, PayPalCommerceInitializationData, @@ -202,6 +203,38 @@ export default class PayPalCommerceIntegrationService { }); } + async proxyTokenizationPayment(orderId?: string): Promise { + 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 { + const cartId = this.paymentIntegrationService.getState().getCartOrThrow().id; + + const { orderId } = await this.paypalCommerceRequestSender.createPaymentOrderIntent( + providerId, + cartId, + options, + ); + + return orderId; + } + /** * * Shipping options methods diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-request-sender.spec.ts b/packages/paypal-commerce-integration/src/paypal-commerce-request-sender.spec.ts index f827eb6563..04add3a834 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-request-sender.spec.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-request-sender.spec.ts @@ -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'); + } + }); + }); }); diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-request-sender.ts b/packages/paypal-commerce-integration/src/paypal-commerce-request-sender.ts index 0d12815b62..35897b987b 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-request-sender.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-request-sender.ts @@ -8,6 +8,9 @@ import { } from '@bigcommerce/checkout-sdk/payment-integration-api'; import { + CreatePaymentOrderIntentOptions, + CreatePaymentOrderIntentResponse, + CreateRedirectToCheckoutResponse, PayPalCreateOrderRequestBody, PayPalOrderData, PayPalOrderStatusData, @@ -69,4 +72,67 @@ export default class PayPalCommerceRequestSender { return res.body; } + + async createPaymentOrderIntent( + walletEntityId: string, + cartId: string, + options?: CreatePaymentOrderIntentOptions, + ): Promise { + 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( + 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 { + const res = await this.requestSender.get(url, options); + + const { + data: { + cart: { + createCartRedirectUrls: { redirectUrls }, + }, + }, + } = res.body; + + if (!redirectUrls?.redirectedCheckoutUrl) { + throw new Error('Failed to redirection to checkout page'); + } + + return redirectUrls.redirectedCheckoutUrl; + } } diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-types.ts b/packages/paypal-commerce-integration/src/paypal-commerce-types.ts index d76d0eb7e3..a8dc8f2964 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-types.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-types.ts @@ -1,6 +1,7 @@ import { BuyNowCartRequestBody, HostedInstrument, + RequestOptions, ShippingOption, VaultedInstrument, } from '@bigcommerce/checkout-sdk/payment-integration-api'; @@ -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 }; + }; + }; +}