diff --git a/packages/medusa-payment-paypal/package.json b/packages/medusa-payment-paypal/package.json index e86d81fe2d..1c9a8ddb78 100644 --- a/packages/medusa-payment-paypal/package.json +++ b/packages/medusa-payment-paypal/package.json @@ -26,12 +26,13 @@ "cross-env": "^5.2.1", "eslint": "^6.8.0", "jest": "^25.5.2", + "medusa-interfaces": "^1.1.10", "medusa-test-utils": "^1.1.12" }, "scripts": { - "build": "babel src -d . --ignore **/__tests__", + "build": "babel src -d . --ignore **/__tests__,**/__mocks__", "prepare": "cross-env NODE_ENV=production npm run build", - "watch": "babel -w src --out-dir . --ignore **/__tests__", + "watch": "babel -w src --out-dir . --ignore **/__tests__,**/__mocks__", "test": "jest" }, "peerDependencies": { diff --git a/packages/medusa-payment-paypal/src/__mocks__/@paypal/checkout-server-sdk.js b/packages/medusa-payment-paypal/src/__mocks__/@paypal/checkout-server-sdk.js new file mode 100644 index 0000000000..8c394649bb --- /dev/null +++ b/packages/medusa-payment-paypal/src/__mocks__/@paypal/checkout-server-sdk.js @@ -0,0 +1,87 @@ +export const PayPalClientMock = { + execute: jest.fn().mockImplementation((r) => { + return { + result: r.result, + } + }), +} + +export const PayPalMock = { + core: { + SandboxEnvironment: function () { + this.env = { + sandbox: true, + live: false, + } + }, + LiveEnvironment: function () { + this.env = { + sandbox: false, + live: true, + } + }, + PayPalHttpClient: function () { + return PayPalClientMock + }, + }, + + payments: { + AuthorizationsGetRequest: jest.fn().mockImplementation(() => {}), + AuthorizationsVoidRequest: jest.fn().mockImplementation(() => {}), + AuthorizationsCaptureRequest: jest.fn().mockImplementation(() => { + return { + result: { + id: "test", + }, + capture: true, + } + }), + CapturesRefundRequest: jest.fn().mockImplementation(() => { + return { + result: { + id: "test", + }, + body: null, + requestBody: function (d) { + this.body = d + }, + } + }), + }, + + orders: { + OrdersCreateRequest: jest.fn().mockImplementation(() => { + return { + result: { + id: "test", + }, + order: true, + body: null, + requestBody: function (d) { + this.body = d + }, + } + }), + OrdersPatchRequest: jest.fn().mockImplementation(() => { + return { + result: { + id: "test", + }, + order: true, + body: null, + requestBody: function (d) { + this.body = d + }, + } + }), + OrdersGetRequest: jest.fn().mockImplementation(() => { + return { + result: { + id: "test", + }, + } + }), + }, +} + +export default PayPalMock diff --git a/packages/medusa-payment-paypal/src/services/__tests__/paypal-provider.js b/packages/medusa-payment-paypal/src/services/__tests__/paypal-provider.js new file mode 100644 index 0000000000..a57368a757 --- /dev/null +++ b/packages/medusa-payment-paypal/src/services/__tests__/paypal-provider.js @@ -0,0 +1,346 @@ +import PayPalProviderService from "../paypal-provider" +import { + PayPalMock, + PayPalClientMock, +} from "../../__mocks__/@paypal/checkout-server-sdk" + +const TotalsServiceMock = { + getTotal: jest.fn().mockImplementation((c) => c.total), +} + +const RegionServiceMock = { + retrieve: jest.fn().mockImplementation((id) => + Promise.resolve({ + currency_code: "eur", + }) + ), +} + +describe("PaypalProviderService", () => { + describe("createPayment", () => { + let result + const paypalProviderService = new PayPalProviderService( + { + regionService: RegionServiceMock, + totalsService: TotalsServiceMock, + }, + { + api_key: "test", + } + ) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("creates paypal order", async () => { + result = await paypalProviderService.createPayment({ + id: "test_cart", + region_id: "fr", + total: 1000, + }) + + expect(PayPalMock.orders.OrdersCreateRequest).toHaveBeenCalledTimes(1) + expect(PayPalClientMock.execute).toHaveBeenCalledTimes(1) + expect(PayPalClientMock.execute).toHaveBeenCalledWith( + expect.objectContaining({ + order: true, + body: { + intent: "AUTHORIZE", + application_context: { + shipping_preference: "NO_SHIPPING", + }, + purchase_units: [ + { + custom_id: "test_cart", + amount: { + currency_code: "EUR", + value: "10.00", + }, + }, + ], + }, + }) + ) + + expect(result.id).toEqual("test") + }) + }) + + describe("retrievePayment", () => { + let result + const paypalProviderService = new PayPalProviderService( + { + regionService: RegionServiceMock, + totalsService: TotalsServiceMock, + }, + { + api_key: "test", + } + ) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("retrieves paypal order", async () => { + result = await paypalProviderService.retrievePayment({ id: "test" }) + expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledTimes(1) + expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith("test") + expect(PayPalClientMock.execute).toHaveBeenCalledTimes(1) + + expect(result.id).toEqual("test") + }) + }) + + describe("updatePayment", () => { + let result + const paypalProviderService = new PayPalProviderService( + { + regionService: RegionServiceMock, + totalsService: TotalsServiceMock, + }, + { + api_key: "test", + } + ) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("updates paypal order", async () => { + result = await paypalProviderService.updatePayment( + { id: "test" }, + { + id: "test_cart", + region_id: "fr", + total: 1000, + } + ) + + expect(PayPalMock.orders.OrdersPatchRequest).toHaveBeenCalledTimes(1) + expect(PayPalMock.orders.OrdersPatchRequest).toHaveBeenCalledWith("test") + expect(PayPalClientMock.execute).toHaveBeenCalledTimes(1) + expect(PayPalClientMock.execute).toHaveBeenCalledWith( + expect.objectContaining({ + order: true, + body: [ + { + op: "replace", + path: "/purchase_units/@reference_id=='default'", + value: { + amount: { + currency_code: "EUR", + value: "10.00", + }, + }, + }, + ], + }) + ) + + expect(result.id).toEqual("test") + }) + }) + + describe("capturePayment", () => { + let result + const paypalProviderService = new PayPalProviderService( + { + regionService: RegionServiceMock, + totalsService: TotalsServiceMock, + }, + { + api_key: "test", + } + ) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("updates paypal order", async () => { + result = await paypalProviderService.capturePayment({ + data: { + id: "test", + purchase_units: [ + { + payments: { + authorizations: [ + { + id: "test_auth", + }, + ], + }, + }, + ], + }, + }) + + expect( + PayPalMock.payments.AuthorizationsCaptureRequest + ).toHaveBeenCalledTimes(1) + expect( + PayPalMock.payments.AuthorizationsCaptureRequest + ).toHaveBeenCalledWith("test_auth") + expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith("test") + expect(PayPalClientMock.execute).toHaveBeenCalledTimes(2) + + expect(result.id).toEqual("test") + }) + }) + + describe("refundPayment", () => { + let result + const paypalProviderService = new PayPalProviderService( + { + regionService: RegionServiceMock, + totalsService: TotalsServiceMock, + }, + { + api_key: "test", + } + ) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("refunds payment", async () => { + result = await paypalProviderService.refundPayment( + { + currency_code: "eur", + data: { + id: "test", + purchase_units: [ + { + payments: { + captures: [ + { + id: "test_cap", + }, + ], + }, + }, + ], + }, + }, + 2000 + ) + + expect(PayPalMock.payments.CapturesRefundRequest).toHaveBeenCalledWith( + "test_cap" + ) + expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith("test") + expect(PayPalClientMock.execute).toHaveBeenCalledTimes(2) + expect(PayPalClientMock.execute).toHaveBeenCalledWith( + expect.objectContaining({ + body: { + amount: { + currency_code: "EUR", + value: "20.00", + }, + }, + }) + ) + + expect(result.id).toEqual("test") + }) + + it("doesn't refund without captures", async () => { + await expect( + paypalProviderService.refundPayment( + { + currency_code: "eur", + data: { + id: "test", + purchase_units: [ + { + payments: { + captures: [], + }, + }, + ], + }, + }, + 2000 + ) + ).rejects.toThrow("Order not yet captured") + }) + }) + + describe("cancelPayment", () => { + let result + const paypalProviderService = new PayPalProviderService( + { + regionService: RegionServiceMock, + totalsService: TotalsServiceMock, + }, + { + api_key: "test", + } + ) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("refunds if captured", async () => { + result = await paypalProviderService.cancelPayment({ + captured_at: "true", + currency_code: "eur", + data: { + id: "test", + purchase_units: [ + { + payments: { + captures: [ + { + id: "test_cap", + }, + ], + }, + }, + ], + }, + }) + + expect(PayPalMock.payments.CapturesRefundRequest).toHaveBeenCalledWith( + "test_cap" + ) + expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith("test") + expect(PayPalClientMock.execute).toHaveBeenCalledTimes(2) + + expect(result.id).toEqual("test") + }) + + it("voids if not captured", async () => { + result = await paypalProviderService.cancelPayment({ + currency_code: "eur", + data: { + id: "test", + purchase_units: [ + { + payments: { + authorizations: [ + { + id: "test_auth", + }, + ], + }, + }, + ], + }, + }) + + expect( + PayPalMock.payments.AuthorizationsVoidRequest + ).toHaveBeenCalledWith("test_auth") + expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith("test") + expect(PayPalClientMock.execute).toHaveBeenCalledTimes(2) + + expect(result.id).toEqual("test") + }) + }) +}) diff --git a/packages/medusa-payment-paypal/src/services/paypal-provider.js b/packages/medusa-payment-paypal/src/services/paypal-provider.js index ed868da3c8..dc2b2bcdf4 100644 --- a/packages/medusa-payment-paypal/src/services/paypal-provider.js +++ b/packages/medusa-payment-paypal/src/services/paypal-provider.js @@ -5,7 +5,7 @@ import { PaymentService } from "medusa-interfaces" class PayPalProviderService extends PaymentService { static identifier = "paypal" - constructor({ customerService, totalsService, regionService }, options) { + constructor({ totalsService, regionService }, options) { super() /** @@ -35,9 +35,6 @@ class PayPalProviderService extends PaymentService { /** @private @const {PayPalHttpClient} */ this.paypal_ = new PayPal.core.PayPalHttpClient(environment) - /** @private @const {CustomerService} */ - this.customerService_ = customerService - /** @private @const {RegionService} */ this.regionService_ = regionService @@ -196,7 +193,7 @@ class PayPalProviderService extends PaymentService { value: { amount: { currency_code: currency_code.toUpperCase(), - value: (cart.total / 100).toFixed(), + value: (cart.total / 100).toFixed(2), }, }, }, @@ -240,7 +237,7 @@ class PayPalProviderService extends PaymentService { * Refunds a given amount. * @param {object} payment - payment to refund * @param {number} amountToRefund - amount to refund - * @returns {string} the resulting PayPal order + * @returns {Promise} the resulting PayPal order */ async refundPayment(payment, amountToRefund) { const { purchase_units } = payment.data @@ -257,13 +254,13 @@ class PayPalProviderService extends PaymentService { request.requestBody({ amount: { currency_code: payment.currency_code.toUpperCase(), - value: (amountToRefund / 100).toFixed(), + value: (amountToRefund / 100).toFixed(2), }, }) await this.paypal_.execute(request) - return this.retrievePayment(payment.id) + return this.retrievePayment(payment.data) } catch (error) { throw error } @@ -272,7 +269,7 @@ class PayPalProviderService extends PaymentService { /** * Cancels payment for Stripe payment intent. * @param {object} paymentData - payment method data from cart - * @returns {object} canceled payment intent + * @returns {Promise} canceled payment intent */ async cancelPayment(payment) { try {