hotfix(paypal): correct payment retrieval on refunds

This commit is contained in:
Sebastian Rindom
2021-04-29 15:37:27 +02:00
parent 7cd9fd487e
commit cf34f8e404
4 changed files with 442 additions and 11 deletions

View File

@@ -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": {

View File

@@ -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

View File

@@ -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")
})
})
})

View File

@@ -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<Object>} 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<object>} canceled payment intent
*/
async cancelPayment(payment) {
try {