feat(medusa-payment-paypal): Migrate to the new payment processor API (#3414)
* feat(medusa-payment-paypal): Migrate to the new payment processor API * WIP * WIP * WIP unit tests * WIP * unit tests * fix package.json * yarn * cleanup * address feedback 1/2 * Start to implement a new Paypal SDK * cleanup * finalise sdk * cleanup * fix push missing file * rename sdk methods * unit test the http client * WIP * fix http client * Create .changeset/empty-melons-eat.md * refactor tests * fix quote * fix options * cleanup * do not retry auth * WIP * retry mechanism max attempts * use both old and new options * fix capture * remove totals fields * add missing method * cleanup * fix current tests * authorize should update the data with the fresh order * remove comments * fix tests * Update packages/medusa-payment-paypal/src/core/paypal-http-client.ts Co-authored-by: Riqwan Thamir <rmthamir@gmail.com> * fix unit tests * update changeset --------- Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
44 files changed
+2271
-1036
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"medusa-payment-paypal": patch
|
||||||
|
"medusa-payment-stripe": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(medusa-payment-paypal): Migrate to the new payment processor API
|
||||||
@@ -10,6 +10,7 @@ packages/*
|
|||||||
!packages/admin-ui
|
!packages/admin-ui
|
||||||
!packages/admin
|
!packages/admin
|
||||||
!packages/medusa-payment-stripe
|
!packages/medusa-payment-stripe
|
||||||
|
!packages/medusa-payment-paypal
|
||||||
!packages/event-bus-redis
|
!packages/event-bus-redis
|
||||||
!packages/event-bus-local
|
!packages/event-bus-local
|
||||||
!packages/medusa-plugin-meilisearch
|
!packages/medusa-plugin-meilisearch
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ module.exports = {
|
|||||||
project: [
|
project: [
|
||||||
"./packages/medusa/tsconfig.json",
|
"./packages/medusa/tsconfig.json",
|
||||||
"./packages/medusa-payment-stripe/tsconfig.spec.json",
|
"./packages/medusa-payment-stripe/tsconfig.spec.json",
|
||||||
|
"./packages/medusa-payment-paypal/tsconfig.spec.json",
|
||||||
|
"./packages/admin-ui/tsconfig.json",
|
||||||
"./packages/event-bus-local/tsconfig.spec.json",
|
"./packages/event-bus-local/tsconfig.spec.json",
|
||||||
"./packages/event-bus-redis/tsconfig.spec.json",
|
"./packages/event-bus-redis/tsconfig.spec.json",
|
||||||
"./packages/medusa-plugin-meilisearch/tsconfig.spec.json",
|
"./packages/medusa-plugin-meilisearch/tsconfig.spec.json",
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-proposal-optional-chaining",
|
|
||||||
"@babel/plugin-proposal-class-properties",
|
|
||||||
"@babel/plugin-transform-instanceof",
|
|
||||||
"@babel/plugin-transform-classes"
|
|
||||||
],
|
|
||||||
"presets": ["@babel/preset-env"],
|
|
||||||
"env": {
|
|
||||||
"test": {
|
|
||||||
"plugins": ["@babel/plugin-transform-runtime"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,4 @@
|
|||||||
/lib
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
.DS_store
|
.DS_store
|
||||||
.env*
|
|
||||||
/*.js
|
|
||||||
!index.js
|
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
/dist
|
|
||||||
|
|
||||||
/api
|
|
||||||
/services
|
|
||||||
/models
|
|
||||||
/subscribers
|
|
||||||
/loaders
|
|
||||||
/__mocks__
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.DS_store
|
|
||||||
src
|
|
||||||
dist
|
|
||||||
yarn.lock
|
|
||||||
.babelrc
|
|
||||||
|
|
||||||
.turbo
|
|
||||||
.yarn
|
|
||||||
@@ -9,8 +9,12 @@ Learn more about how you can use this plugin in the [documentaion](https://docs.
|
|||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
sandbox: true, //default false
|
sandbox: true, //default false
|
||||||
client_id: "CLIENT_ID", // REQUIRED
|
clientId: "CLIENT_ID", // REQUIRED
|
||||||
client_secret: "CLIENT_SECRET", // REQUIRED
|
clientSecret: "CLIENT_SECRET", // REQUIRED
|
||||||
auth_webhook_id: "WEBHOOK_ID" //REQUIRED for webhook to work
|
authWebhookId: "WEBHOOK_ID", //REQUIRED for webhook to work
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Deprecation
|
||||||
|
|
||||||
|
The paypal plugin version `>=1.3.x` requires medusa `>=1.8.x`
|
||||||
@@ -1 +0,0 @@
|
|||||||
// noop
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
globals: {
|
||||||
|
"ts-jest": {
|
||||||
|
tsconfig: "tsconfig.spec.json",
|
||||||
|
isolatedModules: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
"^.+\\.[jt]s?$": "ts-jest",
|
||||||
|
},
|
||||||
|
transformIgnorePatterns: [
|
||||||
|
"/node_modules/(?!(axios)/).*"
|
||||||
|
],
|
||||||
|
testEnvironment: `node`,
|
||||||
|
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
||||||
|
}
|
||||||
@@ -2,44 +2,35 @@
|
|||||||
"name": "medusa-payment-paypal",
|
"name": "medusa-payment-paypal",
|
||||||
"version": "1.3.0-rc.0",
|
"version": "1.3.0-rc.0",
|
||||||
"description": "Paypal Payment provider for Medusa Commerce",
|
"description": "Paypal Payment provider for Medusa Commerce",
|
||||||
"main": "index.js",
|
"main": "dist/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/medusajs/medusa",
|
"url": "https://github.com/medusajs/medusa",
|
||||||
"directory": "packages/medusa-payment-paypal"
|
"directory": "packages/medusa-payment-paypal"
|
||||||
},
|
},
|
||||||
"author": "Sebastian Rindom",
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"author": "Medusa",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
|
||||||
"@babel/cli": "^7.7.5",
|
|
||||||
"@babel/core": "^7.7.5",
|
|
||||||
"@babel/node": "^7.7.4",
|
|
||||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.12.7",
|
|
||||||
"@babel/plugin-transform-classes": "^7.9.5",
|
|
||||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
|
||||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
|
||||||
"@babel/preset-env": "^7.7.5",
|
|
||||||
"@babel/register": "^7.7.4",
|
|
||||||
"@babel/runtime": "^7.9.6",
|
|
||||||
"client-sessions": "^0.8.0",
|
|
||||||
"cross-env": "^5.2.1",
|
|
||||||
"jest": "^25.5.4",
|
|
||||||
"jest-environment-node": "25.5.0",
|
|
||||||
"medusa-interfaces": "^1.3.7-rc.0",
|
|
||||||
"medusa-test-utils": "^1.1.40-rc.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||||
"test": "jest --passWithNoTests src",
|
"test": "jest --passWithNoTests src",
|
||||||
"build": "babel src --out-dir . --ignore '**/__tests__','**/__mocks__'",
|
"build": "tsc",
|
||||||
"watch": "babel -w src --out-dir . --ignore '**/__tests__','**/__mocks__'"
|
"watch": "tsc --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@medusajs/medusa": "^1.8.0-rc.0",
|
||||||
|
"@types/stripe": "^8.0.417",
|
||||||
|
"cross-env": "^5.2.1",
|
||||||
|
"jest": "^25.5.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"medusa-interfaces": "1.3.7-rc.0"
|
"@medusajs/medusa": "^1.7.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@paypal/checkout-server-sdk": "^1.0.3",
|
"@paypal/checkout-server-sdk": "^1.0.3",
|
||||||
|
"axios": "^1.3.4",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"medusa-core-utils": "^1.2.0-rc.0"
|
"medusa-core-utils": "^1.2.0-rc.0"
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { INVOICE_ID } from "../__mocks__/@paypal/checkout-server-sdk"
|
||||||
|
|
||||||
|
export const PaymentIntentDataByStatus = {
|
||||||
|
CREATED: {
|
||||||
|
id: "CREATED",
|
||||||
|
status: "CREATED",
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
},
|
||||||
|
COMPLETED: {
|
||||||
|
id: "COMPLETED",
|
||||||
|
status: "COMPLETED",
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
},
|
||||||
|
SAVED: {
|
||||||
|
id: "SAVED",
|
||||||
|
status: "SAVED",
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
},
|
||||||
|
APPROVED: {
|
||||||
|
id: "APPROVED",
|
||||||
|
status: "APPROVED",
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
},
|
||||||
|
PAYER_ACTION_REQUIRED: {
|
||||||
|
id: "PAYER_ACTION_REQUIRED",
|
||||||
|
status: "PAYER_ACTION_REQUIRED",
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
},
|
||||||
|
VOIDED: {
|
||||||
|
id: "VOIDED",
|
||||||
|
status: "VOIDED",
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
+36
-27
@@ -1,3 +1,9 @@
|
|||||||
|
import { PaymentIntentDataByStatus } from "../../__fixtures__/data";
|
||||||
|
|
||||||
|
export const SUCCESS_INTENT = "right@test.fr"
|
||||||
|
export const FAIL_INTENT_ID = "unknown"
|
||||||
|
export const INVOICE_ID = "invoice_id"
|
||||||
|
|
||||||
export const PayPalClientMock = {
|
export const PayPalClientMock = {
|
||||||
execute: jest.fn().mockImplementation((r) => {
|
execute: jest.fn().mockImplementation((r) => {
|
||||||
return {
|
return {
|
||||||
@@ -8,6 +14,7 @@ export const PayPalClientMock = {
|
|||||||
|
|
||||||
export const PayPalMock = {
|
export const PayPalMock = {
|
||||||
core: {
|
core: {
|
||||||
|
env: {},
|
||||||
SandboxEnvironment: function () {
|
SandboxEnvironment: function () {
|
||||||
this.env = {
|
this.env = {
|
||||||
sandbox: true,
|
sandbox: true,
|
||||||
@@ -32,7 +39,11 @@ export const PayPalMock = {
|
|||||||
status: "VOIDED"
|
status: "VOIDED"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
AuthorizationsCaptureRequest: jest.fn().mockImplementation(() => {
|
AuthorizationsCaptureRequest: jest.fn().mockImplementation((id) => {
|
||||||
|
if (id === FAIL_INTENT_ID) {
|
||||||
|
throw new Error("Error.")
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: {
|
result: {
|
||||||
id: "test",
|
id: "test",
|
||||||
@@ -40,13 +51,17 @@ export const PayPalMock = {
|
|||||||
capture: true,
|
capture: true,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
CapturesRefundRequest: jest.fn().mockImplementation(() => {
|
CapturesRefundRequest: jest.fn().mockImplementation((paymentId) => {
|
||||||
|
if (paymentId === FAIL_INTENT_ID) {
|
||||||
|
throw new Error("Error")
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: {
|
result: {
|
||||||
id: "test",
|
id: "test",
|
||||||
},
|
},
|
||||||
status: "COMPLETED",
|
status: "COMPLETED",
|
||||||
invoice_id: 'invoice_id',
|
invoice_id: INVOICE_ID,
|
||||||
body: null,
|
body: null,
|
||||||
requestBody: function (d) {
|
requestBody: function (d) {
|
||||||
this.body = d
|
this.body = d
|
||||||
@@ -64,11 +79,19 @@ export const PayPalMock = {
|
|||||||
order: true,
|
order: true,
|
||||||
body: null,
|
body: null,
|
||||||
requestBody: function (d) {
|
requestBody: function (d) {
|
||||||
|
if (d.purchase_units[0].custom_id === FAIL_INTENT_ID) {
|
||||||
|
throw new Error("Error.")
|
||||||
|
}
|
||||||
|
|
||||||
this.body = d
|
this.body = d
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
OrdersPatchRequest: jest.fn().mockImplementation(() => {
|
OrdersPatchRequest: jest.fn().mockImplementation((id) => {
|
||||||
|
if (id === FAIL_INTENT_ID) {
|
||||||
|
throw new Error("Error.")
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: {
|
result: {
|
||||||
id: "test",
|
id: "test",
|
||||||
@@ -80,29 +103,15 @@ export const PayPalMock = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
OrdersGetRequest: jest.fn().mockImplementation((id) => {
|
OrdersGetRequest: jest.fn().mockImplementation((paymentId) => {
|
||||||
switch (id) {
|
if (paymentId === FAIL_INTENT_ID) {
|
||||||
case "test-refund":
|
throw new Error("Error")
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: {
|
result: Object.values(PaymentIntentDataByStatus).find(value => {
|
||||||
id: "test-refund",
|
return value.id === paymentId
|
||||||
status: "COMPLETED",
|
}) ?? {}
|
||||||
invoice_id: "invoice_id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "test-voided":
|
|
||||||
return {
|
|
||||||
result: {
|
|
||||||
id: "test-voided",
|
|
||||||
status: "VOIDED"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
result: {
|
|
||||||
id: "test",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
import hooks from "./routes/hooks"
|
import hooks from "./routes/hooks"
|
||||||
|
|
||||||
export default (container) => {
|
export default () => {
|
||||||
const app = Router()
|
const app = Router()
|
||||||
|
|
||||||
hooks(app)
|
hooks(app)
|
||||||
@@ -1 +0,0 @@
|
|||||||
export default (fn) => (...args) => fn(...args).catch(args[2])
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { default as wrap } from "./await-middleware"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
wrap,
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Router } from "express"
|
|
||||||
import bodyParser from "body-parser"
|
|
||||||
import middlewares from "../../middlewares"
|
|
||||||
|
|
||||||
const route = Router()
|
|
||||||
|
|
||||||
export default (app) => {
|
|
||||||
app.use("/paypal", route)
|
|
||||||
|
|
||||||
route.use(bodyParser.json())
|
|
||||||
route.post("/hooks", middlewares.wrap(require("./paypal").default))
|
|
||||||
return app
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Router } from "express"
|
||||||
|
import bodyParser from "body-parser"
|
||||||
|
import { wrapHandler } from "@medusajs/medusa"
|
||||||
|
import paypalWebhookHandler from "./paypal"
|
||||||
|
|
||||||
|
const route = Router()
|
||||||
|
|
||||||
|
export default (app) => {
|
||||||
|
app.use("/paypal/hooks", route)
|
||||||
|
|
||||||
|
route.use(bodyParser.json())
|
||||||
|
route.post("/", wrapHandler(paypalWebhookHandler))
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
+10
-1
@@ -1,3 +1,5 @@
|
|||||||
|
import PaypalProvider from "../../../services/paypal-provider"
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const auth_algo = req.headers["paypal-auth-algo"]
|
const auth_algo = req.headers["paypal-auth-algo"]
|
||||||
const cert_url = req.headers["paypal-cert-url"]
|
const cert_url = req.headers["paypal-cert-url"]
|
||||||
@@ -5,7 +7,9 @@ export default async (req, res) => {
|
|||||||
const transmission_sig = req.headers["paypal-transmission-sig"]
|
const transmission_sig = req.headers["paypal-transmission-sig"]
|
||||||
const transmission_time = req.headers["paypal-transmission-time"]
|
const transmission_time = req.headers["paypal-transmission-time"]
|
||||||
|
|
||||||
const paypalService = req.scope.resolve("paypalProviderService")
|
const paypalService: PaypalProvider = req.scope.resolve(
|
||||||
|
"paypalProviderService"
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await paypalService.verifyWebhook({
|
await paypalService.verifyWebhook({
|
||||||
@@ -88,6 +92,11 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const order = await paypalService.retrieveOrderFromAuth(auth)
|
const order = await paypalService.retrieveOrderFromAuth(auth)
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
res.sendStatus(200)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const purchaseUnit = order.purchase_units[0]
|
const purchaseUnit = order.purchase_units[0]
|
||||||
const customId = purchaseUnit.custom_id
|
const customId = purchaseUnit.custom_id
|
||||||
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { PaymentIntentDataByStatus } from "../../__fixtures__/data"
|
||||||
|
|
||||||
|
export const SUCCESS_INTENT = "right@test.fr"
|
||||||
|
export const FAIL_INTENT_ID = "unknown"
|
||||||
|
export const INVOICE_ID = "invoice_id"
|
||||||
|
|
||||||
|
export const PayPalMock = {
|
||||||
|
cancelAuthorizedPayment: jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
status: "VOIDED"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
captureAuthorizedPayment: jest.fn().mockImplementation((id) => {
|
||||||
|
if (id === FAIL_INTENT_ID) {
|
||||||
|
throw new Error("Error.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: "test",
|
||||||
|
capture: true,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
refundPayment: jest.fn().mockImplementation((paymentId) => {
|
||||||
|
if (paymentId === FAIL_INTENT_ID) {
|
||||||
|
throw new Error("Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}),
|
||||||
|
createOrder: jest.fn().mockImplementation((d) => {
|
||||||
|
if (d.purchase_units[0].custom_id === FAIL_INTENT_ID) {
|
||||||
|
throw new Error("Error.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}),
|
||||||
|
patchOrder: jest.fn().mockImplementation((id) => {
|
||||||
|
if (id === FAIL_INTENT_ID) {
|
||||||
|
throw new Error("Error.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: "test",
|
||||||
|
order: true,
|
||||||
|
body: null,
|
||||||
|
requestBody: function (d) {
|
||||||
|
this.body = d
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getOrder: jest.fn().mockImplementation((paymentId) => {
|
||||||
|
if (paymentId === FAIL_INTENT_ID) {
|
||||||
|
throw new Error("Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(PaymentIntentDataByStatus).find(value => {
|
||||||
|
return value.id === paymentId
|
||||||
|
}) ?? {}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PayPalMock
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
import { PaypalHttpClient } from "../paypal-http-client"
|
||||||
|
import { PaypalApiPath } from "../types"
|
||||||
|
|
||||||
|
jest.mock("axios")
|
||||||
|
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||||
|
|
||||||
|
const accessToken = "accessToken"
|
||||||
|
|
||||||
|
const responseData = { test: "test" }
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
clientId: "fake",
|
||||||
|
clientSecret: "fake",
|
||||||
|
logger: {
|
||||||
|
error: jest.fn(),
|
||||||
|
} as any,
|
||||||
|
sandbox: true,
|
||||||
|
} as any
|
||||||
|
|
||||||
|
describe("PaypalHttpClient", function () {
|
||||||
|
let paypalHttpClient: PaypalHttpClient
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockedAxios.create.mockReturnThis()
|
||||||
|
paypalHttpClient = new PaypalHttpClient(options)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should succeed", async () => {
|
||||||
|
mockedAxios.request.mockResolvedValue(Promise.resolve("resolve"))
|
||||||
|
|
||||||
|
const argument = { url: PaypalApiPath.CREATE_ORDER }
|
||||||
|
await paypalHttpClient.request(argument)
|
||||||
|
|
||||||
|
expect(mockedAxios.request).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
expect(mockedAxios.request).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: argument.url,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer undefined`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail and retry after authentication until reaches the maximum number of attempts", async () => {
|
||||||
|
mockedAxios.request.mockImplementation((async (
|
||||||
|
config,
|
||||||
|
originalConfig,
|
||||||
|
retryCount = 0
|
||||||
|
) => {
|
||||||
|
if (retryCount <= 2) {
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
return Promise.reject({ response: { status: 401 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 200, data: responseData }
|
||||||
|
}) as any)
|
||||||
|
|
||||||
|
const argument = { url: PaypalApiPath.CREATE_ORDER }
|
||||||
|
await paypalHttpClient.request(argument).catch((e) => e)
|
||||||
|
|
||||||
|
expect(mockedAxios.request).toHaveBeenCalledTimes(3)
|
||||||
|
|
||||||
|
expect(mockedAxios.request).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: argument.url,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer undefined`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedAxios.request).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: PaypalApiPath.AUTH,
|
||||||
|
auth: { password: options.clientId, username: options.clientSecret },
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: argument.url,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer undefined`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedAxios.request).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: PaypalApiPath.AUTH,
|
||||||
|
auth: { password: options.clientId, username: options.clientSecret },
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: argument.url,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer undefined`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail and retry after authentication and then succeed", async () => {
|
||||||
|
mockedAxios.request.mockImplementation((async (
|
||||||
|
config,
|
||||||
|
originalConfig,
|
||||||
|
retryCount = 0
|
||||||
|
) => {
|
||||||
|
if (retryCount >= 2 && config.url === PaypalApiPath.AUTH) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
access_token: accessToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryCount < 2) {
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
return Promise.reject({ response: { status: 401 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 200, data: responseData }
|
||||||
|
}) as any)
|
||||||
|
|
||||||
|
const argument = { url: PaypalApiPath.CREATE_ORDER }
|
||||||
|
await paypalHttpClient.request(argument).catch((e) => e)
|
||||||
|
|
||||||
|
expect(mockedAxios.request).toHaveBeenCalledTimes(4)
|
||||||
|
|
||||||
|
expect(mockedAxios.request).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: argument.url,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer undefined`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedAxios.request).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: PaypalApiPath.AUTH,
|
||||||
|
auth: { password: options.clientId, username: options.clientSecret },
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: argument.url,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer undefined`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedAxios.request).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: PaypalApiPath.AUTH,
|
||||||
|
auth: { password: options.clientId, username: options.clientSecret },
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: argument.url,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer undefined`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedAxios.request).toHaveBeenNthCalledWith(
|
||||||
|
4,
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
url: argument.url,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./paypal-sdk"
|
||||||
|
export * from "./types"
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import axios, { AxiosInstance, AxiosRequestConfig, Method } from "axios"
|
||||||
|
import {
|
||||||
|
PaypalApiPath,
|
||||||
|
PaypalEnvironmentPaths,
|
||||||
|
PaypalSdkOptions,
|
||||||
|
} from "./types"
|
||||||
|
import { Logger } from "@medusajs/medusa"
|
||||||
|
|
||||||
|
const MAX_ATTEMPTS = 2
|
||||||
|
|
||||||
|
export class PaypalHttpClient {
|
||||||
|
protected readonly baseUrl_: string = PaypalEnvironmentPaths.LIVE
|
||||||
|
protected readonly httpClient_: AxiosInstance
|
||||||
|
protected readonly options_: PaypalSdkOptions
|
||||||
|
protected readonly logger_?: Logger
|
||||||
|
protected accessToken_: string
|
||||||
|
|
||||||
|
constructor(options: PaypalSdkOptions) {
|
||||||
|
this.options_ = options
|
||||||
|
|
||||||
|
this.logger_ = options.logger
|
||||||
|
|
||||||
|
if (options.sandbox) {
|
||||||
|
this.baseUrl_ = PaypalEnvironmentPaths.SANDBOX
|
||||||
|
}
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: this.baseUrl_,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.httpClient_ = new Proxy(axiosInstance, {
|
||||||
|
// Handle automatic retry mechanism
|
||||||
|
get: (target, prop) => {
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
const original = Reflect.get(...arguments)
|
||||||
|
|
||||||
|
if ("request" === (prop as string)) {
|
||||||
|
return this.retryIfNecessary(original)
|
||||||
|
}
|
||||||
|
|
||||||
|
return original
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a request and return the result
|
||||||
|
* @param url
|
||||||
|
* @param data
|
||||||
|
* @param method
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
async request<T, TResponse>({
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
method,
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
data?: T
|
||||||
|
method?: Method
|
||||||
|
}): Promise<TResponse> {
|
||||||
|
return await this.httpClient_.request({
|
||||||
|
method: method ?? "POST",
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${this.accessToken_}`,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will run the original method and retry it if an unauthorized error is returned
|
||||||
|
* @param originalMethod
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected retryIfNecessary<T = unknown>(originalMethod: Function) {
|
||||||
|
return async (config, originalConfig, retryCount = 0) => {
|
||||||
|
if (retryCount > MAX_ATTEMPTS) {
|
||||||
|
throw new Error(
|
||||||
|
`An error occurred while requesting Paypal API after ${MAX_ATTEMPTS} attempts`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await originalMethod
|
||||||
|
.apply(this.httpClient_, [config, originalConfig, retryCount])
|
||||||
|
.then((res) => res.data)
|
||||||
|
.catch(async (err) => {
|
||||||
|
if (err.response?.status === 401) {
|
||||||
|
++retryCount
|
||||||
|
|
||||||
|
if (!originalConfig) {
|
||||||
|
originalConfig = config
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.authenticate(originalConfig, retryCount)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
...(originalConfig ?? {}),
|
||||||
|
headers: {
|
||||||
|
...(originalConfig?.headers ?? {}),
|
||||||
|
Authorization: `Bearer ${this.accessToken_}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return await originalMethod
|
||||||
|
.apply(this.httpClient_, [config])
|
||||||
|
.then((res) => res.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger_?.error(err.response.message)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate and store the access token
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async authenticate(
|
||||||
|
originalConfig?: AxiosRequestConfig,
|
||||||
|
retryCount = 0
|
||||||
|
) {
|
||||||
|
const res: { access_token: string } = await (
|
||||||
|
this.httpClient_.request as any
|
||||||
|
)(
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
url: PaypalApiPath.AUTH,
|
||||||
|
auth: {
|
||||||
|
username: this.options_.clientId ?? this.options_.client_id,
|
||||||
|
password: this.options_.clientSecret ?? this.options_.client_secret,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
originalConfig,
|
||||||
|
retryCount
|
||||||
|
)
|
||||||
|
|
||||||
|
this.accessToken_ = res.access_token
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import {
|
||||||
|
CreateOrder,
|
||||||
|
CreateOrderResponse,
|
||||||
|
GetOrderResponse,
|
||||||
|
PatchOrder,
|
||||||
|
PaypalApiPath,
|
||||||
|
PaypalSdkOptions,
|
||||||
|
} from "./types"
|
||||||
|
import {
|
||||||
|
CaptureAuthorizedPayment,
|
||||||
|
CapturesAuthorizationResponse,
|
||||||
|
CapturesRefundResponse,
|
||||||
|
GetAuthorizationPaymentResponse,
|
||||||
|
RefundPayment,
|
||||||
|
} from "./types/payment"
|
||||||
|
import { PaypalHttpClient } from "./paypal-http-client"
|
||||||
|
import { VerifyWebhookSignature } from "./types/webhook"
|
||||||
|
|
||||||
|
export class PaypalSdk {
|
||||||
|
protected readonly httpClient_: PaypalHttpClient
|
||||||
|
|
||||||
|
constructor(options: PaypalSdkOptions) {
|
||||||
|
this.httpClient_ = new PaypalHttpClient(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new order.
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
async createOrder(data: CreateOrder): Promise<CreateOrderResponse> {
|
||||||
|
const url = PaypalApiPath.CREATE_ORDER
|
||||||
|
return await this.httpClient_.request({ url, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve an order.
|
||||||
|
* @param orderId
|
||||||
|
*/
|
||||||
|
async getOrder(orderId: string): Promise<GetOrderResponse> {
|
||||||
|
const url = PaypalApiPath.GET_ORDER.replace("{id}", orderId)
|
||||||
|
return await this.httpClient_.request({ url, method: "GET" })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch an order.
|
||||||
|
* @param orderId
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
async patchOrder(orderId: string, data?: PatchOrder[]): Promise<void> {
|
||||||
|
const url = PaypalApiPath.PATCH_ORDER.replace("{id}", orderId)
|
||||||
|
return await this.httpClient_.request({ url, method: "PATCH" })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorizes payment for an order. To successfully authorize payment for an order,
|
||||||
|
* the buyer must first approve the order or a valid payment_source must be provided in the request.
|
||||||
|
* A buyer can approve the order upon being redirected to the rel:approve URL that was returned in the HATEOAS links in the create order response.
|
||||||
|
* @param orderId
|
||||||
|
*/
|
||||||
|
async authorizeOrder(orderId: string): Promise<CreateOrderResponse> {
|
||||||
|
const url = PaypalApiPath.AUTHORIZE_ORDER.replace("{id}", orderId)
|
||||||
|
|
||||||
|
return await this.httpClient_.request({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refunds a captured payment, by ID. For a full refund, include an empty
|
||||||
|
* payload in the JSON request body. For a partial refund, include an amount
|
||||||
|
* object in the JSON request body.
|
||||||
|
* @param paymentId
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
async refundPayment(
|
||||||
|
paymentId: string,
|
||||||
|
data?: RefundPayment
|
||||||
|
): Promise<CapturesRefundResponse> {
|
||||||
|
const url = PaypalApiPath.CAPTURE_REFUND.replace("{id}", paymentId)
|
||||||
|
return await this.httpClient_.request({ url, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Voids, or cancels, an authorized payment, by ID. You cannot void an authorized payment that has been fully captured.
|
||||||
|
* @param authorizationId
|
||||||
|
*/
|
||||||
|
async cancelAuthorizedPayment(authorizationId: string): Promise<void> {
|
||||||
|
const url = PaypalApiPath.AUTHORIZATION_VOID.replace(
|
||||||
|
"{id}",
|
||||||
|
authorizationId
|
||||||
|
)
|
||||||
|
|
||||||
|
return await this.httpClient_.request({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures an authorized payment, by ID.
|
||||||
|
* @param authorizationId
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
async captureAuthorizedPayment(
|
||||||
|
authorizationId: string,
|
||||||
|
data?: CaptureAuthorizedPayment
|
||||||
|
): Promise<CapturesAuthorizationResponse> {
|
||||||
|
const url = PaypalApiPath.AUTHORIZATION_CAPTURE.replace(
|
||||||
|
"{id}",
|
||||||
|
authorizationId
|
||||||
|
)
|
||||||
|
|
||||||
|
return await this.httpClient_.request({ url, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures an authorized payment, by ID.
|
||||||
|
* @param authorizationId
|
||||||
|
*/
|
||||||
|
async getAuthorizationPayment(
|
||||||
|
authorizationId: string
|
||||||
|
): Promise<GetAuthorizationPaymentResponse> {
|
||||||
|
const url = PaypalApiPath.AUTHORIZATION_GET.replace("{id}", authorizationId)
|
||||||
|
|
||||||
|
return await this.httpClient_.request({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyWebhook(data: VerifyWebhookSignature) {
|
||||||
|
const url = PaypalApiPath.VERIFY_WEBHOOK_SIGNATURE
|
||||||
|
return await this.httpClient_.request({ url, data })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
export type Links = { href: string; rel: string; method: string }[]
|
||||||
|
|
||||||
|
export interface Address {
|
||||||
|
country_code: string
|
||||||
|
address_line_1?: string
|
||||||
|
address_line_2?: string
|
||||||
|
admin_area_1?: string
|
||||||
|
admin_area_2?: string
|
||||||
|
postal_code?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoneyAmount {
|
||||||
|
value: string | number
|
||||||
|
currency_code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoneyBreakdown {
|
||||||
|
discount: MoneyAmount
|
||||||
|
handling: MoneyAmount
|
||||||
|
insurance: MoneyAmount
|
||||||
|
item_total: MoneyAmount
|
||||||
|
shipping: MoneyAmount
|
||||||
|
shipping_discount: MoneyAmount
|
||||||
|
tax_total: MoneyAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentInstruction {
|
||||||
|
disbursement_mode: "INSTANT" | "DELAYED"
|
||||||
|
payee_pricing_tier_id: string
|
||||||
|
payee_receivable_fx_rate_id: string
|
||||||
|
platform_fees: Array<PlatformFee>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Payee {
|
||||||
|
email_address: string
|
||||||
|
merchant_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlatformFee {
|
||||||
|
amount: MoneyAmount
|
||||||
|
payee: Payee
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseUnitItem {
|
||||||
|
name: string
|
||||||
|
quantity: number
|
||||||
|
unit_amount: MoneyAmount
|
||||||
|
category?: "DIGITAL_GOODS" | "PHYSICAL_GOODS" | "DONATION"
|
||||||
|
description?: string
|
||||||
|
sku?: string
|
||||||
|
tax?: MoneyAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseUnit {
|
||||||
|
amount: MoneyAmount | MoneyBreakdown
|
||||||
|
custom_id?: string
|
||||||
|
description?: string
|
||||||
|
invoice_id?: string
|
||||||
|
items?: Array<PurchaseUnitItem>
|
||||||
|
payee?: Payee
|
||||||
|
payment_instruction?: PaymentInstruction
|
||||||
|
reference_id?: string
|
||||||
|
shipping?: {
|
||||||
|
address?: Address
|
||||||
|
name?: { full_name: string }
|
||||||
|
type?: "SHIPPING" | "PICKUP_IN_PERSON"
|
||||||
|
}
|
||||||
|
soft_descriptor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperienceContext {
|
||||||
|
brand_name?: string
|
||||||
|
cancel_url?: string
|
||||||
|
locale?: string
|
||||||
|
return_url?: string
|
||||||
|
shipping_preference?: "GET_FROM_FILE" | "NO_SHIPPING" | "SET_PROVIDED_ADDRESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentSourceBase {
|
||||||
|
name: string
|
||||||
|
country_name: string
|
||||||
|
experience_context?: ExperienceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint @typescript-eslint/no-empty-interface: "off" */
|
||||||
|
export interface Bancontact extends PaymentSourceBase {}
|
||||||
|
|
||||||
|
export interface Blik extends PaymentSourceBase {
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Card {
|
||||||
|
billing_address?: Address
|
||||||
|
expiry?: string
|
||||||
|
name?: string
|
||||||
|
number?: string
|
||||||
|
security_code?: string
|
||||||
|
stored_credential?: {
|
||||||
|
payment_initiator: "CUSTOMER" | "MERCHANT"
|
||||||
|
payment_type: "ONE_TIME" | "RECURRING" | "UNSCHEDULED"
|
||||||
|
previous_network_transaction_reference?: {
|
||||||
|
id: string
|
||||||
|
network:
|
||||||
|
| "VISA"
|
||||||
|
| "MASTERCARD"
|
||||||
|
| "DISCOVER"
|
||||||
|
| "AMEX"
|
||||||
|
| "SOLO"
|
||||||
|
| "JCB"
|
||||||
|
| "STAR"
|
||||||
|
| "DELTA"
|
||||||
|
| "SWITCH"
|
||||||
|
| "MAESTRO"
|
||||||
|
| "CB_NATIONALE"
|
||||||
|
| "CONFIGOGA"
|
||||||
|
| "CONFIDIS"
|
||||||
|
| "ELECTRON"
|
||||||
|
| "CETELEM"
|
||||||
|
| "CHINA_UNION_PAY"
|
||||||
|
date?: string
|
||||||
|
}
|
||||||
|
usageenum?: "FIRST" | "SUBSEQUENT" | "DERIVED"
|
||||||
|
}
|
||||||
|
vault_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint @typescript-eslint/no-empty-interface: "off" */
|
||||||
|
export interface EPS extends PaymentSourceBase {}
|
||||||
|
|
||||||
|
/* eslint @typescript-eslint/no-empty-interface: "off" */
|
||||||
|
export interface Giropay extends PaymentSourceBase {}
|
||||||
|
|
||||||
|
export interface Ideal extends PaymentSourceBase {
|
||||||
|
bic?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint @typescript-eslint/no-empty-interface: "off" */
|
||||||
|
export interface MyBank extends PaymentSourceBase {}
|
||||||
|
|
||||||
|
export interface P24 extends PaymentSourceBase {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Paypal {
|
||||||
|
address?: Address
|
||||||
|
birth_date?: string
|
||||||
|
email_address?: string
|
||||||
|
experience_context?: ExperienceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint @typescript-eslint/no-empty-interface: "off" */
|
||||||
|
export interface Sofort extends PaymentSourceBase {}
|
||||||
|
|
||||||
|
export interface Token {
|
||||||
|
id: string
|
||||||
|
type: "BILLING_AGREEMENT"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Trustly {
|
||||||
|
bic?: string
|
||||||
|
country_code?: string
|
||||||
|
iban_last_chars?: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentSource {
|
||||||
|
bancontact?: Bancontact
|
||||||
|
blik?: Blik
|
||||||
|
card?: Card
|
||||||
|
eps?: EPS
|
||||||
|
ideal?: Ideal
|
||||||
|
myBank?: MyBank
|
||||||
|
p24?: P24
|
||||||
|
paypal?: Paypal
|
||||||
|
sofort?: Sofort
|
||||||
|
token?: Token
|
||||||
|
trustly?: Trustly
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export const PaypalEnvironmentPaths = {
|
||||||
|
SANDBOX: "https://api-m.sandbox.paypal.com",
|
||||||
|
LIVE: "https://api-m.paypal.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaypalApiPath = {
|
||||||
|
AUTH: "/v1/oauth2/token",
|
||||||
|
GET_ORDER: "/v2/checkout/orders/{id}",
|
||||||
|
PATCH_ORDER: "/v2/checkout/orders/{id}",
|
||||||
|
CREATE_ORDER: "/v2/checkout/orders",
|
||||||
|
AUTHORIZE_ORDER: "/v2/checkout/orders/{id}/authorize",
|
||||||
|
CAPTURE_REFUND: "/v2/payments/captures/{id}/refund",
|
||||||
|
AUTHORIZATION_GET: "/v2/payments/authorizations/{id}",
|
||||||
|
AUTHORIZATION_CAPTURE: "/v2/payments/authorizations/{id}/capture",
|
||||||
|
AUTHORIZATION_VOID: "/v2/payments/authorizations/{id}/void",
|
||||||
|
VERIFY_WEBHOOK_SIGNATURE: "/v1/notifications/verify-webhook-signature",
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Logger } from "@medusajs/medusa"
|
||||||
|
import { PaypalOptions } from "../../types"
|
||||||
|
|
||||||
|
export type PaypalSdkOptions = PaypalOptions & {
|
||||||
|
logger?: Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./common"
|
||||||
|
export * from "./order"
|
||||||
|
export * from "./constant"
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Links, PaymentSource, PurchaseUnit } from "./common"
|
||||||
|
|
||||||
|
export interface CreateOrder {
|
||||||
|
intent: "CAPTURE" | "AUTHORIZE"
|
||||||
|
purchase_units: Array<PurchaseUnit>
|
||||||
|
payment_source?: PaymentSource
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateOrderResponse {
|
||||||
|
id: string
|
||||||
|
status:
|
||||||
|
| "CREATED"
|
||||||
|
| "SAVED"
|
||||||
|
| "APPROVED"
|
||||||
|
| "VOIDED"
|
||||||
|
| "COMPLETED"
|
||||||
|
| "PAYER_ACTION_REQUIRED"
|
||||||
|
payment_source?: PaymentSource
|
||||||
|
links?: Links
|
||||||
|
intent?: CreateOrder["intent"]
|
||||||
|
processing_instruction?:
|
||||||
|
| "ORDER_COMPLETE_ON_PAYMENT_APPROVAL"
|
||||||
|
| "NO_INSTRUCTION"
|
||||||
|
purchase_units: Array<PurchaseUnit>
|
||||||
|
create_time?: string
|
||||||
|
update_time?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint @typescript-eslint/no-empty-interface: "off" */
|
||||||
|
export interface GetOrderResponse extends CreateOrderResponse {}
|
||||||
|
|
||||||
|
export interface PatchOrder {
|
||||||
|
op: "replace" | "add" | "remove"
|
||||||
|
path: string
|
||||||
|
value:
|
||||||
|
| CreateOrder["intent"]
|
||||||
|
| PurchaseUnit
|
||||||
|
| { client_configuration?: any }
|
||||||
|
| Record<string, unknown>
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Links, MoneyAmount, PaymentInstruction } from "./common"
|
||||||
|
|
||||||
|
export interface RefundPayment {
|
||||||
|
amount?: MoneyAmount
|
||||||
|
invoice_id?: string
|
||||||
|
note_to_payer?: string
|
||||||
|
payment_instruction?: PaymentInstruction
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CapturesRefundResponse {
|
||||||
|
id: string
|
||||||
|
status: "CANCELLED" | "FAILED" | "PENDING" | "COMPLETED"
|
||||||
|
status_details?: any
|
||||||
|
amount?: MoneyAmount
|
||||||
|
note_to_payer?: string
|
||||||
|
seller_payable_breakdown?: any
|
||||||
|
invoice_id?: string
|
||||||
|
create_time?: string
|
||||||
|
update_time?: string
|
||||||
|
links?: Links
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CaptureAuthorizedPayment {
|
||||||
|
amount?: MoneyAmount
|
||||||
|
final_capture?: boolean
|
||||||
|
invoice_id?: string
|
||||||
|
note_to_payer?: string
|
||||||
|
payment_instruction?: PaymentInstruction
|
||||||
|
soft_descriptor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CapturesAuthorizationResponse {
|
||||||
|
id: string
|
||||||
|
status:
|
||||||
|
| "COMPLETED"
|
||||||
|
| "DECLINED"
|
||||||
|
| "PARTIALLY_REFUNDED"
|
||||||
|
| "PENDING"
|
||||||
|
| "REFUNDED"
|
||||||
|
| "FAILED"
|
||||||
|
status_details?: any
|
||||||
|
amount?: MoneyAmount
|
||||||
|
created_time?: string
|
||||||
|
update_time?: string
|
||||||
|
custom_id?: string
|
||||||
|
disbursement_mode?: "INSTANT" | "DELAYED"
|
||||||
|
final_capture?: boolean
|
||||||
|
invoice_id?: string
|
||||||
|
links?: Links
|
||||||
|
processor_response?: any
|
||||||
|
seller_protection?: any
|
||||||
|
seller_receivable_breakdown?: any
|
||||||
|
supplementary_data?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetAuthorizationPaymentResponse {
|
||||||
|
amount?: MoneyAmount
|
||||||
|
create_time?: string
|
||||||
|
custom_id?: string
|
||||||
|
expiration_time?: string
|
||||||
|
id?: string
|
||||||
|
invoice_id?: string
|
||||||
|
links?: Links
|
||||||
|
seller_protection?: any
|
||||||
|
status?:
|
||||||
|
| "CREATED"
|
||||||
|
| "DENIED"
|
||||||
|
| "CAPTURED"
|
||||||
|
| "VOIDED"
|
||||||
|
| "EXPIRED"
|
||||||
|
| "PARTIALLY_CAPTURED"
|
||||||
|
| "PENDING"
|
||||||
|
status_details?: any
|
||||||
|
supplementary_data?: any
|
||||||
|
update_time?: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Links } from "./common"
|
||||||
|
|
||||||
|
export interface WebhookEvent {
|
||||||
|
id?: string
|
||||||
|
create_time?: string
|
||||||
|
event_version?: string
|
||||||
|
links?: Links
|
||||||
|
resource?: any
|
||||||
|
resource_type?: string
|
||||||
|
resource_version?: string
|
||||||
|
summary?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifyWebhookSignature {
|
||||||
|
auth_algo: string
|
||||||
|
cert_url: string
|
||||||
|
transmission_id: string
|
||||||
|
transmission_sig: string
|
||||||
|
transmission_time: string
|
||||||
|
webhook_event: WebhookEvent
|
||||||
|
webhook_id: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./types"
|
||||||
|
export * from "./services/paypal-provider"
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
import {
|
||||||
|
FAIL_INTENT_ID,
|
||||||
|
SUCCESS_INTENT,
|
||||||
|
} from "../../__mocks__/@paypal/checkout-server-sdk"
|
||||||
|
import { PaymentIntentDataByStatus } from "../../__fixtures__/data"
|
||||||
|
|
||||||
|
// INITIATE PAYMENT DATA
|
||||||
|
|
||||||
|
export const initiatePaymentContextSuccess = {
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 1000,
|
||||||
|
resource_id: SUCCESS_INTENT,
|
||||||
|
customer: {},
|
||||||
|
context: {},
|
||||||
|
paymentSessionData: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initiatePaymentContextFail = {
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 1000,
|
||||||
|
resource_id: FAIL_INTENT_ID,
|
||||||
|
customer: {
|
||||||
|
metadata: {
|
||||||
|
stripe_id: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {},
|
||||||
|
paymentSessionData: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// AUTHORIZE PAYMENT DATA
|
||||||
|
|
||||||
|
export const authorizePaymentSuccessData = {
|
||||||
|
id: PaymentIntentDataByStatus.COMPLETED.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
// CANCEL PAYMENT DATA
|
||||||
|
|
||||||
|
export const cancelPaymentSuccessData = {
|
||||||
|
id: PaymentIntentDataByStatus.APPROVED.id,
|
||||||
|
purchase_units: [
|
||||||
|
{
|
||||||
|
payments: {
|
||||||
|
authorizations: [
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cancelPaymentRefundAlreadyCaptureSuccessData = {
|
||||||
|
id: PaymentIntentDataByStatus.APPROVED.id,
|
||||||
|
purchase_units: [
|
||||||
|
{
|
||||||
|
payments: {
|
||||||
|
captures: [
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
authorizations: [
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cancelPaymentRefundAlreadyCanceledSuccessData = {
|
||||||
|
id: PaymentIntentDataByStatus.VOIDED.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cancelPaymentFailData = {
|
||||||
|
id: FAIL_INTENT_ID,
|
||||||
|
purchase_units: [
|
||||||
|
{
|
||||||
|
payments: {
|
||||||
|
captures: [
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
authorizations: [
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAPTURE PAYMENT DATA
|
||||||
|
|
||||||
|
export const capturePaymentContextSuccessData = {
|
||||||
|
paymentSessionData: {
|
||||||
|
id: PaymentIntentDataByStatus.APPROVED.id,
|
||||||
|
purchase_units: [
|
||||||
|
{
|
||||||
|
payments: {
|
||||||
|
authorizations: [
|
||||||
|
{
|
||||||
|
id: SUCCESS_INTENT,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const capturePaymentContextFailData = {
|
||||||
|
paymentSessionData: {
|
||||||
|
id: PaymentIntentDataByStatus.APPROVED.id,
|
||||||
|
purchase_units: [
|
||||||
|
{
|
||||||
|
payments: {
|
||||||
|
authorizations: [
|
||||||
|
{
|
||||||
|
id: FAIL_INTENT_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// REFUND PAYMENT DATA
|
||||||
|
|
||||||
|
export const refundPaymentSuccessData = {
|
||||||
|
id: PaymentIntentDataByStatus.APPROVED.id,
|
||||||
|
purchase_units: [
|
||||||
|
{
|
||||||
|
amount: {
|
||||||
|
currency_code: "USD",
|
||||||
|
value: "100.00",
|
||||||
|
},
|
||||||
|
payments: {
|
||||||
|
captures: [
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
authorizations: [
|
||||||
|
{
|
||||||
|
id: FAIL_INTENT_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const refundPaymentFailNotYetCapturedData = {
|
||||||
|
id: PaymentIntentDataByStatus.APPROVED.id,
|
||||||
|
purchase_units: [
|
||||||
|
{
|
||||||
|
payments: {
|
||||||
|
captures: [],
|
||||||
|
authorizations: [
|
||||||
|
{
|
||||||
|
id: FAIL_INTENT_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const refundPaymentFailData = {
|
||||||
|
id: FAIL_INTENT_ID,
|
||||||
|
purchase_units: [
|
||||||
|
{
|
||||||
|
amount: {
|
||||||
|
currency_code: "USD",
|
||||||
|
value: "100.00",
|
||||||
|
},
|
||||||
|
payments: {
|
||||||
|
captures: [
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
authorizations: [
|
||||||
|
{
|
||||||
|
id: FAIL_INTENT_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// RETRIEVE PAYMENT DATA
|
||||||
|
|
||||||
|
export const retrievePaymentSuccessData = {
|
||||||
|
id: PaymentIntentDataByStatus.APPROVED.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const retrievePaymentFailData = {
|
||||||
|
id: FAIL_INTENT_ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE PAYMENT DATA
|
||||||
|
|
||||||
|
export const updatePaymentSuccessData = {
|
||||||
|
paymentSessionData: {
|
||||||
|
id: PaymentIntentDataByStatus.APPROVED.id,
|
||||||
|
},
|
||||||
|
currency_code: "USD",
|
||||||
|
amount: 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updatePaymentFailData = {
|
||||||
|
currency_code: "USD",
|
||||||
|
amount: 1000,
|
||||||
|
resource_id: FAIL_INTENT_ID,
|
||||||
|
customer: {
|
||||||
|
metadata: {
|
||||||
|
stripe_id: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {},
|
||||||
|
paymentSessionData: {
|
||||||
|
id: FAIL_INTENT_ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,447 +0,0 @@
|
|||||||
import PayPalProviderService from "../paypal-provider"
|
|
||||||
import { PayPalClientMock, PayPalMock, } 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")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("creates paypal order using new API", async () => {
|
|
||||||
result = await paypalProviderService.createPayment({
|
|
||||||
id: "",
|
|
||||||
region_id: "fr",
|
|
||||||
total: 1000,
|
|
||||||
resource_id: "resource_id"
|
|
||||||
})
|
|
||||||
|
|
||||||
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: "resource_id",
|
|
||||||
amount: {
|
|
||||||
currency_code: "EUR",
|
|
||||||
value: "10.00",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.id).toEqual("test")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("retrievePayment", () => {
|
|
||||||
let result
|
|
||||||
const paypalProviderService = new PayPalProviderService(
|
|
||||||
{
|
|
||||||
regionService: RegionServiceMock,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("updates paypal order using new API", async () => {
|
|
||||||
result = await paypalProviderService.updatePayment(
|
|
||||||
{ id: "test" },
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
region_id: "fr",
|
|
||||||
total: 1000,
|
|
||||||
resource_id: "resource_id"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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(3)
|
|
||||||
|
|
||||||
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(3)
|
|
||||||
|
|
||||||
expect(result.id).toEqual("test")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return the order if already canceled", async () => {
|
|
||||||
result = await paypalProviderService.cancelPayment({
|
|
||||||
currency_code: "eur",
|
|
||||||
data: { id: "test-voided" },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(
|
|
||||||
PayPalMock.payments.AuthorizationsVoidRequest
|
|
||||||
).not.toHaveBeenCalled()
|
|
||||||
expect(PayPalMock.payments.CapturesRefundRequest).not.toHaveBeenCalled()
|
|
||||||
expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith(
|
|
||||||
"test-voided"
|
|
||||||
)
|
|
||||||
expect(PayPalClientMock.execute).toHaveBeenCalledTimes(1)
|
|
||||||
|
|
||||||
expect(result.id).toEqual("test-voided")
|
|
||||||
expect(result.status).toEqual("VOIDED")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return the order if already fully refund", async () => {
|
|
||||||
result = await paypalProviderService.cancelPayment({
|
|
||||||
currency_code: "eur",
|
|
||||||
data: {
|
|
||||||
id: "test-refund",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(
|
|
||||||
PayPalMock.payments.AuthorizationsVoidRequest
|
|
||||||
).not.toHaveBeenCalled()
|
|
||||||
expect(PayPalMock.payments.CapturesRefundRequest).not.toHaveBeenCalled()
|
|
||||||
expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith(
|
|
||||||
"test-refund"
|
|
||||||
)
|
|
||||||
expect(PayPalClientMock.execute).toHaveBeenCalledTimes(1)
|
|
||||||
|
|
||||||
expect(result.id).toEqual("test-refund")
|
|
||||||
expect(result.status).toEqual("COMPLETED")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
import { PaymentIntentDataByStatus } from "../../__fixtures__/data"
|
||||||
|
import { PaymentProcessorContext, PaymentSessionStatus } from "@medusajs/medusa"
|
||||||
|
import PaypalProvider from "../paypal-provider"
|
||||||
|
import {
|
||||||
|
authorizePaymentSuccessData,
|
||||||
|
cancelPaymentFailData,
|
||||||
|
cancelPaymentRefundAlreadyCanceledSuccessData,
|
||||||
|
cancelPaymentRefundAlreadyCaptureSuccessData,
|
||||||
|
cancelPaymentSuccessData,
|
||||||
|
capturePaymentContextFailData,
|
||||||
|
capturePaymentContextSuccessData,
|
||||||
|
initiatePaymentContextFail,
|
||||||
|
initiatePaymentContextSuccess,
|
||||||
|
refundPaymentFailData,
|
||||||
|
refundPaymentFailNotYetCapturedData,
|
||||||
|
refundPaymentSuccessData,
|
||||||
|
retrievePaymentFailData,
|
||||||
|
retrievePaymentSuccessData,
|
||||||
|
updatePaymentFailData,
|
||||||
|
updatePaymentSuccessData,
|
||||||
|
} from "../__fixtures__/data"
|
||||||
|
import axios from "axios"
|
||||||
|
import { INVOICE_ID, PayPalMock } from "../../core/__mocks__/paypal-sdk"
|
||||||
|
import { roundToTwo } from "../utils/utils"
|
||||||
|
import { humanizeAmount } from "medusa-core-utils"
|
||||||
|
|
||||||
|
jest.mock("axios")
|
||||||
|
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||||
|
|
||||||
|
jest.mock("../../core", () => {
|
||||||
|
return {
|
||||||
|
PaypalSdk: jest.fn().mockImplementation(() => PayPalMock),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const container = {
|
||||||
|
logger: {
|
||||||
|
error: jest.fn(),
|
||||||
|
} as any,
|
||||||
|
}
|
||||||
|
|
||||||
|
const paypalConfig = {
|
||||||
|
sandbox: true,
|
||||||
|
client_id: "fake",
|
||||||
|
client_secret: "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("PaypalProvider", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
mockedAxios.create.mockReturnThis()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getPaymentStatus", function () {
|
||||||
|
let paypalProvider: PaypalProvider
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const scopedContainer = { ...container }
|
||||||
|
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return the correct status", async () => {
|
||||||
|
let status: PaymentSessionStatus
|
||||||
|
|
||||||
|
status = await paypalProvider.getPaymentStatus({
|
||||||
|
id: PaymentIntentDataByStatus.CREATED.id,
|
||||||
|
})
|
||||||
|
expect(status).toBe(PaymentSessionStatus.PENDING)
|
||||||
|
|
||||||
|
status = await paypalProvider.getPaymentStatus({
|
||||||
|
id: PaymentIntentDataByStatus.SAVED.id,
|
||||||
|
})
|
||||||
|
expect(status).toBe(PaymentSessionStatus.REQUIRES_MORE)
|
||||||
|
|
||||||
|
status = await paypalProvider.getPaymentStatus({
|
||||||
|
id: PaymentIntentDataByStatus.APPROVED.id,
|
||||||
|
})
|
||||||
|
expect(status).toBe(PaymentSessionStatus.REQUIRES_MORE)
|
||||||
|
|
||||||
|
status = await paypalProvider.getPaymentStatus({
|
||||||
|
id: PaymentIntentDataByStatus.PAYER_ACTION_REQUIRED.id,
|
||||||
|
})
|
||||||
|
expect(status).toBe(PaymentSessionStatus.REQUIRES_MORE)
|
||||||
|
|
||||||
|
status = await paypalProvider.getPaymentStatus({
|
||||||
|
id: PaymentIntentDataByStatus.VOIDED.id,
|
||||||
|
})
|
||||||
|
expect(status).toBe(PaymentSessionStatus.CANCELED)
|
||||||
|
|
||||||
|
status = await paypalProvider.getPaymentStatus({
|
||||||
|
id: PaymentIntentDataByStatus.COMPLETED.id,
|
||||||
|
})
|
||||||
|
expect(status).toBe(PaymentSessionStatus.AUTHORIZED)
|
||||||
|
|
||||||
|
status = await paypalProvider.getPaymentStatus({
|
||||||
|
id: "unknown-id",
|
||||||
|
})
|
||||||
|
expect(status).toBe(PaymentSessionStatus.PENDING)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("initiatePayment", function () {
|
||||||
|
let paypalProvider: PaypalProvider
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const scopedContainer = { ...container }
|
||||||
|
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should succeed with an existing customer but no stripe id", async () => {
|
||||||
|
const result = await paypalProvider.initiatePayment(
|
||||||
|
initiatePaymentContextSuccess as PaymentProcessorContext
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(PayPalMock.createOrder).toHaveBeenCalled()
|
||||||
|
expect(PayPalMock.createOrder).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
intent: "AUTHORIZE",
|
||||||
|
purchase_units: [
|
||||||
|
{
|
||||||
|
custom_id: initiatePaymentContextSuccess.resource_id,
|
||||||
|
amount: {
|
||||||
|
currency_code:
|
||||||
|
initiatePaymentContextSuccess.currency_code.toUpperCase(),
|
||||||
|
value: roundToTwo(
|
||||||
|
humanizeAmount(
|
||||||
|
initiatePaymentContextSuccess.amount,
|
||||||
|
initiatePaymentContextSuccess.currency_code
|
||||||
|
),
|
||||||
|
initiatePaymentContextSuccess.currency_code
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
session_data: expect.any(Object),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail", async () => {
|
||||||
|
const result = await paypalProvider.initiatePayment(
|
||||||
|
initiatePaymentContextFail as unknown as PaymentProcessorContext
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(PayPalMock.createOrder).toHaveBeenCalled()
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
error: "An error occurred in initiatePayment",
|
||||||
|
code: "",
|
||||||
|
detail: "Error.",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("authorizePayment", function () {
|
||||||
|
let paypalProvider: PaypalProvider
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const scopedContainer = { ...container }
|
||||||
|
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should succeed", async () => {
|
||||||
|
const result = await paypalProvider.authorizePayment(
|
||||||
|
authorizePaymentSuccessData as Record<string, unknown>,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
data: {
|
||||||
|
id: authorizePaymentSuccessData.id,
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
status:
|
||||||
|
PaymentIntentDataByStatus[authorizePaymentSuccessData.id].status,
|
||||||
|
},
|
||||||
|
status: "authorized",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("cancelPayment", function () {
|
||||||
|
let paypalProvider: PaypalProvider
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const scopedContainer = { ...container }
|
||||||
|
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should succeed to void the payment authorization", async () => {
|
||||||
|
const result = await paypalProvider.cancelPayment(
|
||||||
|
cancelPaymentSuccessData
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(PayPalMock.cancelAuthorizedPayment).toHaveBeenCalledTimes(1)
|
||||||
|
expect(PayPalMock.cancelAuthorizedPayment).toHaveBeenCalledWith(
|
||||||
|
cancelPaymentSuccessData.purchase_units[0].payments.authorizations[0].id
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: cancelPaymentSuccessData.id,
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
status: PaymentIntentDataByStatus[cancelPaymentSuccessData.id].status,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should succeed to refund an already captured payment", async () => {
|
||||||
|
const result = await paypalProvider.cancelPayment(
|
||||||
|
cancelPaymentRefundAlreadyCaptureSuccessData
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(PayPalMock.refundPayment).toHaveBeenCalledTimes(1)
|
||||||
|
expect(PayPalMock.refundPayment).toHaveBeenCalledWith(
|
||||||
|
cancelPaymentRefundAlreadyCaptureSuccessData.purchase_units[0].payments
|
||||||
|
.captures[0].id
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: cancelPaymentRefundAlreadyCaptureSuccessData.id,
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
status:
|
||||||
|
PaymentIntentDataByStatus[
|
||||||
|
cancelPaymentRefundAlreadyCaptureSuccessData.id
|
||||||
|
].status,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should succeed to do nothing if already canceled or already fully refund", async () => {
|
||||||
|
const result = await paypalProvider.cancelPayment(
|
||||||
|
cancelPaymentRefundAlreadyCanceledSuccessData
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(PayPalMock.captureAuthorizedPayment).not.toHaveBeenCalled()
|
||||||
|
expect(PayPalMock.cancelAuthorizedPayment).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: cancelPaymentRefundAlreadyCanceledSuccessData.id,
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
status:
|
||||||
|
PaymentIntentDataByStatus[
|
||||||
|
cancelPaymentRefundAlreadyCanceledSuccessData.id
|
||||||
|
].status,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail", async () => {
|
||||||
|
const result = await paypalProvider.cancelPayment(cancelPaymentFailData)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
code: "",
|
||||||
|
detail: "Error",
|
||||||
|
error: "An error occurred in retrievePayment",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("capturePayment", function () {
|
||||||
|
let paypalProvider: PaypalProvider
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const scopedContainer = { ...container }
|
||||||
|
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should succeed", async () => {
|
||||||
|
const result = await paypalProvider.capturePayment(
|
||||||
|
capturePaymentContextSuccessData.paymentSessionData
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: capturePaymentContextSuccessData.paymentSessionData.id,
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
status:
|
||||||
|
PaymentIntentDataByStatus[
|
||||||
|
capturePaymentContextSuccessData.paymentSessionData.id
|
||||||
|
].status,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail", async () => {
|
||||||
|
const result = await paypalProvider.capturePayment(
|
||||||
|
capturePaymentContextFailData.paymentSessionData
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
error: "An error occurred in capturePayment",
|
||||||
|
code: "",
|
||||||
|
detail: "Error.",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("refundPayment", function () {
|
||||||
|
let paypalProvider: PaypalProvider
|
||||||
|
const refundAmount = 500
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const scopedContainer = { ...container }
|
||||||
|
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should succeed", async () => {
|
||||||
|
const result = await paypalProvider.refundPayment(
|
||||||
|
refundPaymentSuccessData,
|
||||||
|
refundAmount
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(PayPalMock.refundPayment).toHaveBeenCalled()
|
||||||
|
expect(PayPalMock.refundPayment).toHaveBeenCalledWith(
|
||||||
|
refundPaymentSuccessData.purchase_units[0].payments.captures[0].id,
|
||||||
|
{
|
||||||
|
amount: {
|
||||||
|
currency_code:
|
||||||
|
refundPaymentSuccessData.purchase_units[0].amount.currency_code,
|
||||||
|
value: "5.00",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: refundPaymentSuccessData.id,
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
status: PaymentIntentDataByStatus[refundPaymentSuccessData.id].status,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail if not already captured", async () => {
|
||||||
|
const result = await paypalProvider.refundPayment(
|
||||||
|
refundPaymentFailNotYetCapturedData,
|
||||||
|
refundAmount
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(PayPalMock.refundPayment).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
code: "",
|
||||||
|
detail: "Cannot refund an uncaptured payment",
|
||||||
|
error: "An error occurred in refundPayment",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail", async () => {
|
||||||
|
const result = await paypalProvider.refundPayment(
|
||||||
|
refundPaymentFailData,
|
||||||
|
refundAmount
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(PayPalMock.refundPayment).toHaveBeenCalled()
|
||||||
|
expect(PayPalMock.refundPayment).toHaveBeenCalledWith(
|
||||||
|
refundPaymentFailData.purchase_units[0].payments.captures[0].id,
|
||||||
|
{
|
||||||
|
amount: {
|
||||||
|
currency_code:
|
||||||
|
refundPaymentFailData.purchase_units[0].amount.currency_code,
|
||||||
|
value: "5.00",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
code: "",
|
||||||
|
detail: "Error",
|
||||||
|
error: "An error occurred in retrievePayment",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("retrievePayment", function () {
|
||||||
|
let paypalProvider
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const scopedContainer = { ...container }
|
||||||
|
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should succeed", async () => {
|
||||||
|
const result = await paypalProvider.retrievePayment(
|
||||||
|
retrievePaymentSuccessData
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: retrievePaymentSuccessData.id,
|
||||||
|
invoice_id: INVOICE_ID,
|
||||||
|
status: PaymentIntentDataByStatus[retrievePaymentSuccessData.id].status,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail on refund creation", async () => {
|
||||||
|
const result = await paypalProvider.retrievePayment(
|
||||||
|
retrievePaymentFailData
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
error: "An error occurred in retrievePayment",
|
||||||
|
code: "",
|
||||||
|
detail: "Error",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("updatePayment", function () {
|
||||||
|
let paypalProvider: PaypalProvider
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const scopedContainer = { ...container }
|
||||||
|
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should succeed", async () => {
|
||||||
|
const result = await paypalProvider.updatePayment(
|
||||||
|
updatePaymentSuccessData as unknown as PaymentProcessorContext
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(PayPalMock.patchOrder).toHaveBeenCalled()
|
||||||
|
expect(PayPalMock.patchOrder).toHaveBeenCalledWith(
|
||||||
|
updatePaymentSuccessData.paymentSessionData.id,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
op: "replace",
|
||||||
|
path: "/purchase_units/@reference_id=='default'",
|
||||||
|
value: {
|
||||||
|
amount: {
|
||||||
|
currency_code: updatePaymentSuccessData.currency_code,
|
||||||
|
value: "10.00",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
session_data: expect.any(Object),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail", async () => {
|
||||||
|
const result = await paypalProvider.updatePayment(
|
||||||
|
updatePaymentFailData as unknown as PaymentProcessorContext
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(PayPalMock.patchOrder).toHaveBeenCalled()
|
||||||
|
expect(PayPalMock.patchOrder).toHaveBeenCalledWith(
|
||||||
|
updatePaymentFailData.paymentSessionData.id,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
op: "replace",
|
||||||
|
path: "/purchase_units/@reference_id=='default'",
|
||||||
|
value: {
|
||||||
|
amount: {
|
||||||
|
currency_code: updatePaymentFailData.currency_code,
|
||||||
|
value: "10.00",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
code: "",
|
||||||
|
detail: "Error.",
|
||||||
|
error: "An error occurred in initiatePayment",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
import { humanizeAmount, zeroDecimalCurrencies } from "medusa-core-utils"
|
|
||||||
import PayPal from "@paypal/checkout-server-sdk"
|
|
||||||
import { PaymentService } from "medusa-interfaces"
|
|
||||||
|
|
||||||
function roundToTwo(num, currency) {
|
|
||||||
if (zeroDecimalCurrencies.includes(currency.toLowerCase())) {
|
|
||||||
return `${num}`
|
|
||||||
}
|
|
||||||
return num.toFixed(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
class PayPalProviderService extends PaymentService {
|
|
||||||
static identifier = "paypal"
|
|
||||||
|
|
||||||
constructor({ regionService }, options) {
|
|
||||||
super()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Required PayPal options:
|
|
||||||
* {
|
|
||||||
* sandbox: [default: false],
|
|
||||||
* client_id: "CLIENT_ID", REQUIRED
|
|
||||||
* client_secret: "CLIENT_SECRET", REQUIRED
|
|
||||||
* auth_webhook_id: REQUIRED for webhook to work
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
this.options_ = options
|
|
||||||
|
|
||||||
let environment
|
|
||||||
if (this.options_.sandbox) {
|
|
||||||
environment = new PayPal.core.SandboxEnvironment(
|
|
||||||
options.client_id,
|
|
||||||
options.client_secret
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
environment = new PayPal.core.LiveEnvironment(
|
|
||||||
options.client_id,
|
|
||||||
options.client_secret
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @private @const {PayPalHttpClient} */
|
|
||||||
this.paypal_ = new PayPal.core.PayPalHttpClient(environment)
|
|
||||||
|
|
||||||
/** @private @const {RegionService} */
|
|
||||||
this.regionService_ = regionService
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches an open PayPal order and maps its status to Medusa payment
|
|
||||||
* statuses.
|
|
||||||
* @param {object} paymentData - the data stored with the payment
|
|
||||||
* @returns {Promise<string>} the status of the order
|
|
||||||
*/
|
|
||||||
async getStatus(paymentData) {
|
|
||||||
const order = await this.retrievePayment(paymentData)
|
|
||||||
|
|
||||||
let status = "pending"
|
|
||||||
|
|
||||||
switch (order.status) {
|
|
||||||
case "CREATED":
|
|
||||||
return "pending"
|
|
||||||
case "COMPLETED":
|
|
||||||
return "authorized"
|
|
||||||
case "SAVED":
|
|
||||||
case "APPROVED":
|
|
||||||
case "PAYER_ACTION_REQUIRED":
|
|
||||||
return "requires_more"
|
|
||||||
case "VOIDED":
|
|
||||||
return "canceled"
|
|
||||||
default:
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not supported
|
|
||||||
*/
|
|
||||||
async retrieveSavedMethods(customer) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a PayPal order, with an Authorize intent. The plugin doesn't
|
|
||||||
* support shipping details at the moment.
|
|
||||||
* Reference docs: https://developer.paypal.com/docs/api/orders/v2/
|
|
||||||
* @param {object} cart - cart to create a payment for
|
|
||||||
* @returns {object} the data to be stored with the payment session.
|
|
||||||
*/
|
|
||||||
async createPayment(cart) {
|
|
||||||
let { id, region_id, resource_id, currency_code, total } = cart
|
|
||||||
|
|
||||||
if (!currency_code) {
|
|
||||||
const region = await this.regionService_.retrieve(region_id)
|
|
||||||
currency_code = region.currency_code
|
|
||||||
}
|
|
||||||
|
|
||||||
const amount = total
|
|
||||||
|
|
||||||
const request = new PayPal.orders.OrdersCreateRequest()
|
|
||||||
request.requestBody({
|
|
||||||
intent: "AUTHORIZE",
|
|
||||||
application_context: {
|
|
||||||
shipping_preference: "NO_SHIPPING",
|
|
||||||
},
|
|
||||||
purchase_units: [
|
|
||||||
{
|
|
||||||
custom_id: resource_id ?? id,
|
|
||||||
amount: {
|
|
||||||
currency_code: currency_code.toUpperCase(),
|
|
||||||
value: roundToTwo(
|
|
||||||
humanizeAmount(amount, currency_code),
|
|
||||||
currency_code
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await this.paypal_.execute(request)
|
|
||||||
|
|
||||||
return { id: res.result.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a PayPal order.
|
|
||||||
* @param {object} data - the data stored with the payment
|
|
||||||
* @returns {Promise<object>} PayPal order
|
|
||||||
*/
|
|
||||||
async retrievePayment(data) {
|
|
||||||
try {
|
|
||||||
const request = new PayPal.orders.OrdersGetRequest(data.id)
|
|
||||||
const res = await this.paypal_.execute(request)
|
|
||||||
return res.result
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the payment data from a payment session
|
|
||||||
* @param {object} session - the session to fetch payment data for.
|
|
||||||
* @returns {Promise<object>} the PayPal order object
|
|
||||||
*/
|
|
||||||
async getPaymentData(session) {
|
|
||||||
try {
|
|
||||||
return this.retrievePayment(session.data)
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method does not call the PayPal authorize function, but fetches the
|
|
||||||
* status of the payment as it is expected to have been authorized client side.
|
|
||||||
* @param {object} session - payment session
|
|
||||||
* @param {object} context - properties relevant to current context
|
|
||||||
* @returns {Promise<{ status: string, data: object }>} result with data and status
|
|
||||||
*/
|
|
||||||
async authorizePayment(session, context = {}) {
|
|
||||||
const stat = await this.getStatus(session.data)
|
|
||||||
|
|
||||||
try {
|
|
||||||
return { data: session.data, status: stat }
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the data stored with the payment session.
|
|
||||||
* @param {object} data - the currently stored data.
|
|
||||||
* @param {object} update - the update data to store.
|
|
||||||
* @returns {object} the merged data of the two arguments.
|
|
||||||
*/
|
|
||||||
async updatePaymentData(data, update) {
|
|
||||||
try {
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
...update.data,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the PayPal order.
|
|
||||||
* @param {object} sessionData - payment session data.
|
|
||||||
* @param {object} cart - the cart to update by.
|
|
||||||
* @returns {object} the resulting order object.
|
|
||||||
*/
|
|
||||||
async updatePayment(sessionData, cart) {
|
|
||||||
try {
|
|
||||||
let { currency_code, total, region_id } = cart
|
|
||||||
|
|
||||||
if (!currency_code) {
|
|
||||||
const region = await this.regionService_.retrieve(region_id)
|
|
||||||
currency_code = region.currency_code
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = new PayPal.orders.OrdersPatchRequest(sessionData.id)
|
|
||||||
request.requestBody([
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "/purchase_units/@reference_id=='default'",
|
|
||||||
value: {
|
|
||||||
amount: {
|
|
||||||
currency_code: currency_code.toUpperCase(),
|
|
||||||
value: roundToTwo(
|
|
||||||
humanizeAmount(total, currency_code),
|
|
||||||
currency_code
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
await this.paypal_.execute(request)
|
|
||||||
|
|
||||||
return sessionData
|
|
||||||
} catch (error) {
|
|
||||||
return this.createPayment(cart)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not suported
|
|
||||||
*/
|
|
||||||
async deletePayment(payment) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Captures a previously authorized order.
|
|
||||||
* @param {object} payment - the payment to capture
|
|
||||||
* @returns {object} the PayPal order
|
|
||||||
*/
|
|
||||||
async capturePayment(payment) {
|
|
||||||
const { purchase_units } = payment.data
|
|
||||||
|
|
||||||
const id = purchase_units[0].payments.authorizations[0].id
|
|
||||||
|
|
||||||
try {
|
|
||||||
const request = new PayPal.payments.AuthorizationsCaptureRequest(id)
|
|
||||||
await this.paypal_.execute(request)
|
|
||||||
return this.retrievePayment(payment.data)
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refunds a given amount.
|
|
||||||
* @param {object} payment - payment to refund
|
|
||||||
* @param {number} amountToRefund - amount to refund
|
|
||||||
* @returns {Promise<Object>} the resulting PayPal order
|
|
||||||
*/
|
|
||||||
async refundPayment(payment, amountToRefund) {
|
|
||||||
const { purchase_units } = payment.data
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payments = purchase_units[0].payments
|
|
||||||
if (!(payments && payments.captures.length)) {
|
|
||||||
throw new Error("Order not yet captured")
|
|
||||||
}
|
|
||||||
|
|
||||||
const payId = payments.captures[0].id
|
|
||||||
const request = new PayPal.payments.CapturesRefundRequest(payId)
|
|
||||||
|
|
||||||
request.requestBody({
|
|
||||||
amount: {
|
|
||||||
currency_code: payment.currency_code.toUpperCase(),
|
|
||||||
value: roundToTwo(
|
|
||||||
humanizeAmount(amountToRefund, payment.currency_code),
|
|
||||||
payment.currency_code
|
|
||||||
),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.paypal_.execute(request)
|
|
||||||
|
|
||||||
return this.retrievePayment(payment.data)
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels payment for paypal payment.
|
|
||||||
* @param {Payment} payment - payment object
|
|
||||||
* @returns {Promise<object>} canceled payment intent
|
|
||||||
*/
|
|
||||||
async cancelPayment(payment) {
|
|
||||||
const order = await this.retrievePayment(payment.data)
|
|
||||||
const isAlreadyCanceled = order.status === "VOIDED"
|
|
||||||
const isCanceledAndFullyRefund =
|
|
||||||
order.status === "COMPLETED" && !!order.invoice_id
|
|
||||||
if (isAlreadyCanceled || isCanceledAndFullyRefund) {
|
|
||||||
return order
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { purchase_units } = payment.data
|
|
||||||
if (payment.captured_at) {
|
|
||||||
const payments = purchase_units[0].payments
|
|
||||||
|
|
||||||
const payId = payments.captures[0].id
|
|
||||||
const request = new PayPal.payments.CapturesRefundRequest(payId)
|
|
||||||
await this.paypal_.execute(request)
|
|
||||||
} else {
|
|
||||||
const id = purchase_units[0].payments.authorizations[0].id
|
|
||||||
const request = new PayPal.payments.AuthorizationsVoidRequest(id)
|
|
||||||
await this.paypal_.execute(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.retrievePayment(payment.data)
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a PayPal authorization object the method will find the order that
|
|
||||||
* created the authorization, by following the HATEOAS link to the order.
|
|
||||||
* @param {object} auth - the authorization object.
|
|
||||||
* @returns {Promise<object>} the PayPal order object
|
|
||||||
*/
|
|
||||||
async retrieveOrderFromAuth(auth) {
|
|
||||||
const link = auth.links.find((l) => l.rel === "up")
|
|
||||||
const parts = link.href.split("/")
|
|
||||||
const orderId = parts[parts.length - 1]
|
|
||||||
const orderReq = new PayPal.orders.OrdersGetRequest(orderId)
|
|
||||||
const res = await this.paypal_.execute(orderReq)
|
|
||||||
if (res.result) {
|
|
||||||
return res.result
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a PayPal authorization.
|
|
||||||
* @param {string} id - the id of the authorization.
|
|
||||||
* @returns {Promise<object>} the authorization.
|
|
||||||
*/
|
|
||||||
async retrieveAuthorization(id) {
|
|
||||||
const authReq = new PayPal.payments.AuthorizationsGetRequest(id)
|
|
||||||
const res = await this.paypal_.execute(authReq)
|
|
||||||
if (res.result) {
|
|
||||||
return res.result
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a webhook is verified.
|
|
||||||
* @param {object} data - the verficiation data.
|
|
||||||
* @returns {Promise<object>} the response of the verification request.
|
|
||||||
*/
|
|
||||||
async verifyWebhook(data) {
|
|
||||||
const verifyReq = {
|
|
||||||
verb: "POST",
|
|
||||||
path: "/v1/notifications/verify-webhook-signature",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
webhook_id: this.options_.auth_webhook_id,
|
|
||||||
...data,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.paypal_.execute(verifyReq)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upserts a webhook that listens for order authorizations.
|
|
||||||
*/
|
|
||||||
async ensureWebhooks() {
|
|
||||||
if (!this.options_.backend_url) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const webhookReq = {
|
|
||||||
verb: "GET",
|
|
||||||
path: "/v1/notifications/webhooks",
|
|
||||||
}
|
|
||||||
const webhookRes = await this.paypal_.execute(webhookReq)
|
|
||||||
|
|
||||||
let found
|
|
||||||
if (webhookRes.result && webhookRes.result.webhooks) {
|
|
||||||
found = webhookRes.result.webhooks.find((w) => {
|
|
||||||
const notificationType = w.event_types.find(
|
|
||||||
(e) => e.name === "PAYMENT.AUTHORIZATION.CREATED"
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
!!notificationType &&
|
|
||||||
w.url === `${this.options_.backend_url}/paypal/hooks`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
const whCreateReq = {
|
|
||||||
verb: "POST",
|
|
||||||
path: "/v1/notifications/webhooks",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
id: "medusa-auth-notification",
|
|
||||||
url: `${this.options_.backend_url}/paypal/hooks`,
|
|
||||||
event_types: [
|
|
||||||
{
|
|
||||||
name: "PAYMENT.AUTHORIZATION.CREATED",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.paypal_.execute(whCreateReq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PayPalProviderService
|
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
import { EOL } from "os"
|
||||||
|
import {
|
||||||
|
AbstractPaymentProcessor,
|
||||||
|
isPaymentProcessorError,
|
||||||
|
PaymentProcessorContext,
|
||||||
|
PaymentProcessorError,
|
||||||
|
PaymentProcessorSessionResponse,
|
||||||
|
PaymentSessionStatus,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
PaypalOptions,
|
||||||
|
PaypalOrder,
|
||||||
|
PaypalOrderStatus,
|
||||||
|
PurchaseUnits,
|
||||||
|
} from "../types"
|
||||||
|
import { humanizeAmount } from "medusa-core-utils"
|
||||||
|
import { roundToTwo } from "./utils/utils"
|
||||||
|
import { CreateOrder, PaypalSdk } from "../core"
|
||||||
|
import { Logger } from "@medusajs/types"
|
||||||
|
|
||||||
|
class PayPalProviderService extends AbstractPaymentProcessor {
|
||||||
|
static identifier = "paypal"
|
||||||
|
|
||||||
|
protected readonly options_: PaypalOptions
|
||||||
|
protected paypal_: PaypalSdk
|
||||||
|
protected readonly logger_: Logger | undefined
|
||||||
|
|
||||||
|
constructor({ logger }: { logger?: Logger }, options) {
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
super(...arguments)
|
||||||
|
|
||||||
|
this.logger_ = logger
|
||||||
|
this.options_ = options
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected init(): void {
|
||||||
|
this.paypal_ = new PaypalSdk({
|
||||||
|
...this.options_,
|
||||||
|
logger: this.logger_,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPaymentStatus(
|
||||||
|
paymentSessionData: Record<string, unknown>
|
||||||
|
): Promise<PaymentSessionStatus> {
|
||||||
|
const order = (await this.retrievePayment(
|
||||||
|
paymentSessionData
|
||||||
|
)) as PaypalOrder
|
||||||
|
|
||||||
|
switch (order.status) {
|
||||||
|
case PaypalOrderStatus.CREATED:
|
||||||
|
return PaymentSessionStatus.PENDING
|
||||||
|
case PaypalOrderStatus.SAVED:
|
||||||
|
case PaypalOrderStatus.APPROVED:
|
||||||
|
case PaypalOrderStatus.PAYER_ACTION_REQUIRED:
|
||||||
|
return PaymentSessionStatus.REQUIRES_MORE
|
||||||
|
case PaypalOrderStatus.VOIDED:
|
||||||
|
return PaymentSessionStatus.CANCELED
|
||||||
|
case PaypalOrderStatus.COMPLETED:
|
||||||
|
return PaymentSessionStatus.AUTHORIZED
|
||||||
|
default:
|
||||||
|
return PaymentSessionStatus.PENDING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initiatePayment(
|
||||||
|
context: PaymentProcessorContext
|
||||||
|
): Promise<PaymentProcessorError | PaymentProcessorSessionResponse> {
|
||||||
|
const { currency_code, amount, resource_id } = context
|
||||||
|
|
||||||
|
let session_data
|
||||||
|
|
||||||
|
try {
|
||||||
|
const intent: CreateOrder["intent"] = this.options_.capture
|
||||||
|
? "CAPTURE"
|
||||||
|
: "AUTHORIZE"
|
||||||
|
|
||||||
|
session_data = await this.paypal_.createOrder({
|
||||||
|
intent,
|
||||||
|
purchase_units: [
|
||||||
|
{
|
||||||
|
custom_id: resource_id,
|
||||||
|
amount: {
|
||||||
|
currency_code: currency_code.toUpperCase(),
|
||||||
|
value: roundToTwo(
|
||||||
|
humanizeAmount(amount, currency_code),
|
||||||
|
currency_code
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
return this.buildError("An error occurred in initiatePayment", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
session_data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async authorizePayment(
|
||||||
|
paymentSessionData: Record<string, unknown>,
|
||||||
|
context: Record<string, unknown>
|
||||||
|
): Promise<
|
||||||
|
| PaymentProcessorError
|
||||||
|
| {
|
||||||
|
status: PaymentSessionStatus
|
||||||
|
data: PaymentProcessorSessionResponse["session_data"]
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
try {
|
||||||
|
const stat = await this.getPaymentStatus(paymentSessionData)
|
||||||
|
const order = (await this.retrievePayment(
|
||||||
|
paymentSessionData
|
||||||
|
)) as PaypalOrder
|
||||||
|
return { data: order, status: stat }
|
||||||
|
} catch (error) {
|
||||||
|
return this.buildError("An error occurred in authorizePayment", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelPayment(
|
||||||
|
paymentSessionData: Record<string, unknown>
|
||||||
|
): Promise<
|
||||||
|
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||||
|
> {
|
||||||
|
const order = (await this.retrievePayment(
|
||||||
|
paymentSessionData
|
||||||
|
)) as PaypalOrder
|
||||||
|
|
||||||
|
const isAlreadyCanceled = order.status === PaypalOrderStatus.VOIDED
|
||||||
|
const isCanceledAndFullyRefund =
|
||||||
|
order.status === PaypalOrderStatus.COMPLETED && !!order.invoice_id
|
||||||
|
|
||||||
|
if (isAlreadyCanceled || isCanceledAndFullyRefund) {
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { purchase_units } = paymentSessionData as {
|
||||||
|
purchase_units: PurchaseUnits
|
||||||
|
}
|
||||||
|
const isAlreadyCaptured = purchase_units.some(
|
||||||
|
(pu) => pu.payments.captures?.length
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isAlreadyCaptured) {
|
||||||
|
const payments = purchase_units[0].payments
|
||||||
|
|
||||||
|
const payId = payments.captures[0].id
|
||||||
|
await this.paypal_.refundPayment(payId)
|
||||||
|
} else {
|
||||||
|
const id = purchase_units[0].payments.authorizations[0].id
|
||||||
|
await this.paypal_.cancelAuthorizedPayment(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await this.retrievePayment(
|
||||||
|
paymentSessionData
|
||||||
|
)) as unknown as PaymentProcessorSessionResponse["session_data"]
|
||||||
|
} catch (error) {
|
||||||
|
return this.buildError("An error occurred in cancelPayment", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async capturePayment(
|
||||||
|
paymentSessionData: Record<string, unknown>
|
||||||
|
): Promise<
|
||||||
|
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||||
|
> {
|
||||||
|
const { purchase_units } = paymentSessionData as {
|
||||||
|
purchase_units: PurchaseUnits
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = purchase_units[0].payments.authorizations[0].id
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.paypal_.captureAuthorizedPayment(id)
|
||||||
|
return await this.retrievePayment(paymentSessionData)
|
||||||
|
} catch (error) {
|
||||||
|
return this.buildError("An error occurred in capturePayment", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paypal does not provide such feature
|
||||||
|
* @param paymentSessionData
|
||||||
|
*/
|
||||||
|
async deletePayment(
|
||||||
|
paymentSessionData: Record<string, unknown>
|
||||||
|
): Promise<
|
||||||
|
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||||
|
> {
|
||||||
|
return paymentSessionData
|
||||||
|
}
|
||||||
|
|
||||||
|
async refundPayment(
|
||||||
|
paymentSessionData: Record<string, unknown>,
|
||||||
|
refundAmount: number
|
||||||
|
): Promise<
|
||||||
|
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||||
|
> {
|
||||||
|
const { purchase_units } = paymentSessionData as {
|
||||||
|
purchase_units: PurchaseUnits
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const purchaseUnit = purchase_units[0]
|
||||||
|
const payments = purchaseUnit.payments
|
||||||
|
const isAlreadyCaptured = purchase_units.some(
|
||||||
|
(pu) => pu.payments.captures?.length
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isAlreadyCaptured) {
|
||||||
|
throw new Error("Cannot refund an uncaptured payment")
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentId = payments.captures[0].id
|
||||||
|
const currencyCode = purchaseUnit.amount.currency_code
|
||||||
|
await this.paypal_.refundPayment(paymentId, {
|
||||||
|
amount: {
|
||||||
|
currency_code: currencyCode,
|
||||||
|
value: roundToTwo(
|
||||||
|
humanizeAmount(refundAmount, currencyCode),
|
||||||
|
currencyCode
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return await this.retrievePayment(paymentSessionData)
|
||||||
|
} catch (error) {
|
||||||
|
return this.buildError("An error occurred in refundPayment", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrievePayment(
|
||||||
|
paymentSessionData: Record<string, unknown>
|
||||||
|
): Promise<
|
||||||
|
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||||
|
> {
|
||||||
|
try {
|
||||||
|
const id = paymentSessionData.id as string
|
||||||
|
return (await this.paypal_.getOrder(
|
||||||
|
id
|
||||||
|
)) as unknown as PaymentProcessorSessionResponse["session_data"]
|
||||||
|
} catch (e) {
|
||||||
|
return this.buildError("An error occurred in retrievePayment", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePayment(
|
||||||
|
context: PaymentProcessorContext
|
||||||
|
): Promise<PaymentProcessorError | PaymentProcessorSessionResponse | void> {
|
||||||
|
try {
|
||||||
|
const { currency_code, amount } = context
|
||||||
|
const id = context.paymentSessionData.id as string
|
||||||
|
|
||||||
|
await this.paypal_.patchOrder(id, [
|
||||||
|
{
|
||||||
|
op: "replace",
|
||||||
|
path: "/purchase_units/@reference_id=='default'",
|
||||||
|
value: {
|
||||||
|
amount: {
|
||||||
|
currency_code: currency_code.toUpperCase(),
|
||||||
|
value: roundToTwo(
|
||||||
|
humanizeAmount(amount, currency_code),
|
||||||
|
currency_code
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
return { session_data: context.paymentSessionData }
|
||||||
|
} catch (error) {
|
||||||
|
return await this.initiatePayment(context).catch((e) => {
|
||||||
|
return this.buildError("An error occurred in updatePayment", e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveOrderFromAuth(authorization) {
|
||||||
|
const link = authorization.links.find((l) => l.rel === "up")
|
||||||
|
const parts = link.href.split("/")
|
||||||
|
const orderId = parts[parts.length - 1]
|
||||||
|
|
||||||
|
if (!orderId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.paypal_.getOrder(orderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveAuthorization(id) {
|
||||||
|
return await this.paypal_.getAuthorizationPayment(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildError(
|
||||||
|
message: string,
|
||||||
|
e: PaymentProcessorError | Error
|
||||||
|
): PaymentProcessorError {
|
||||||
|
return {
|
||||||
|
error: message,
|
||||||
|
code: "code" in e ? e.code : "",
|
||||||
|
detail: isPaymentProcessorError(e)
|
||||||
|
? `${e.error}${EOL}${e.detail ?? ""}`
|
||||||
|
: "detail" in e
|
||||||
|
? e.detail
|
||||||
|
: e.message ?? "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a webhook is verified.
|
||||||
|
* @param {object} data - the verficiation data.
|
||||||
|
* @returns {Promise<object>} the response of the verification request.
|
||||||
|
*/
|
||||||
|
async verifyWebhook(data) {
|
||||||
|
return await this.paypal_.verifyWebhook({
|
||||||
|
webhook_id: this.options_.auth_webhook_id || this.options_.authWebhookId,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PayPalProviderService
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { zeroDecimalCurrencies } from "medusa-core-utils";
|
||||||
|
|
||||||
|
export function roundToTwo(num: number, currency: string): string {
|
||||||
|
if (zeroDecimalCurrencies.includes(currency.toLowerCase())) {
|
||||||
|
return `${num}`
|
||||||
|
}
|
||||||
|
return num.toFixed(2)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
export interface PaypalOptions {
|
||||||
|
/**
|
||||||
|
* Indicate if it should run as sandbox, default false
|
||||||
|
*/
|
||||||
|
sandbox?: boolean
|
||||||
|
clientId: string
|
||||||
|
clientSecret: string
|
||||||
|
authWebhookId: string
|
||||||
|
capture?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backward compatibility options below
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use clientId instead
|
||||||
|
*/
|
||||||
|
client_id: string
|
||||||
|
/**
|
||||||
|
* @deprecated use clientSecret instead
|
||||||
|
*/
|
||||||
|
client_secret: string
|
||||||
|
/**
|
||||||
|
* @deprecated use authWebhookId instead
|
||||||
|
*/
|
||||||
|
auth_webhook_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaypalOrder = {
|
||||||
|
status: keyof typeof PaypalOrderStatus
|
||||||
|
invoice_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PurchaseUnits = {
|
||||||
|
payments: {
|
||||||
|
captures: { id: string }[]
|
||||||
|
authorizations: { id: string }[]
|
||||||
|
}
|
||||||
|
amount: {
|
||||||
|
currency_code: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
|
||||||
|
export const PaypalOrderStatus = {
|
||||||
|
CREATED: "CREATED",
|
||||||
|
COMPLETED: "COMPLETED",
|
||||||
|
SAVED: "SAVED",
|
||||||
|
APPROVED: "APPROVED",
|
||||||
|
PAYER_ACTION_REQUIRED: "PAYER_ACTION_REQUIRED",
|
||||||
|
VOIDED: "VOIDED",
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"es5",
|
||||||
|
"es6",
|
||||||
|
"es2019"
|
||||||
|
],
|
||||||
|
"target": "es5",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"declaration": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"downlevelIteration": true // to use ES5 specific tooling
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"src/**/__tests__",
|
||||||
|
"src/**/__mocks__",
|
||||||
|
"src/**/__fixtures__",
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -101,7 +101,6 @@ describe("StripeTest", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const scopedContainer = { ...container }
|
const scopedContainer = { ...container }
|
||||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||||
await stripeTest.init()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -232,7 +231,6 @@ describe("StripeTest", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const scopedContainer = { ...container }
|
const scopedContainer = { ...container }
|
||||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||||
await stripeTest.init()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -257,7 +255,6 @@ describe("StripeTest", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const scopedContainer = { ...container }
|
const scopedContainer = { ...container }
|
||||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||||
await stripeTest.init()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -300,7 +297,6 @@ describe("StripeTest", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const scopedContainer = { ...container }
|
const scopedContainer = { ...container }
|
||||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||||
await stripeTest.init()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -309,7 +305,7 @@ describe("StripeTest", () => {
|
|||||||
|
|
||||||
it("should succeed", async () => {
|
it("should succeed", async () => {
|
||||||
const result = await stripeTest.capturePayment(
|
const result = await stripeTest.capturePayment(
|
||||||
capturePaymentContextSuccessData
|
capturePaymentContextSuccessData.paymentSessionData
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@@ -319,7 +315,7 @@ describe("StripeTest", () => {
|
|||||||
|
|
||||||
it("should fail on intent capture but still return the intent", async () => {
|
it("should fail on intent capture but still return the intent", async () => {
|
||||||
const result = await stripeTest.capturePayment(
|
const result = await stripeTest.capturePayment(
|
||||||
capturePaymentContextPartiallyFailData
|
capturePaymentContextPartiallyFailData.paymentSessionData
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@@ -330,11 +326,11 @@ describe("StripeTest", () => {
|
|||||||
|
|
||||||
it("should fail on intent capture", async () => {
|
it("should fail on intent capture", async () => {
|
||||||
const result = await stripeTest.capturePayment(
|
const result = await stripeTest.capturePayment(
|
||||||
capturePaymentContextFailData
|
capturePaymentContextFailData.paymentSessionData
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
error: "An error occurred in deletePayment",
|
error: "An error occurred in capturePayment",
|
||||||
code: "",
|
code: "",
|
||||||
detail: "Error",
|
detail: "Error",
|
||||||
})
|
})
|
||||||
@@ -347,7 +343,6 @@ describe("StripeTest", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const scopedContainer = { ...container }
|
const scopedContainer = { ...container }
|
||||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||||
await stripeTest.init()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -391,7 +386,6 @@ describe("StripeTest", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const scopedContainer = { ...container }
|
const scopedContainer = { ...container }
|
||||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||||
await stripeTest.init()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -429,7 +423,6 @@ describe("StripeTest", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const scopedContainer = { ...container }
|
const scopedContainer = { ...container }
|
||||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||||
await stripeTest.init()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -464,7 +457,6 @@ describe("StripeTest", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const scopedContainer = { ...container }
|
const scopedContainer = { ...container }
|
||||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||||
await stripeTest.init()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -186,11 +186,11 @@ abstract class StripeBase extends AbstractPaymentProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async capturePayment(
|
async capturePayment(
|
||||||
context: PaymentProcessorContext
|
paymentSessionData: Record<string, unknown>
|
||||||
): Promise<
|
): Promise<
|
||||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||||
> {
|
> {
|
||||||
const id = context.paymentSessionData.id as string
|
const id = paymentSessionData.id as string
|
||||||
try {
|
try {
|
||||||
const intent = await this.stripe_.paymentIntents.capture(id)
|
const intent = await this.stripe_.paymentIntents.capture(id)
|
||||||
return intent as unknown as PaymentProcessorSessionResponse["session_data"]
|
return intent as unknown as PaymentProcessorSessionResponse["session_data"]
|
||||||
@@ -201,7 +201,7 @@ abstract class StripeBase extends AbstractPaymentProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.buildError("An error occurred in deletePayment", error)
|
return this.buildError("An error occurred in capturePayment", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export * from "./types"
|
||||||
|
export * from "./core/stripe-base"
|
||||||
|
export * from "./services/stripe-blik"
|
||||||
|
export * from "./services/stripe-bancontact"
|
||||||
|
export * from "./services/stripe-giropay"
|
||||||
|
export * from "./services/stripe-ideal"
|
||||||
|
export * from "./services/stripe-przelewy24"
|
||||||
|
export * from "./services/stripe-provider"
|
||||||
@@ -101,14 +101,7 @@ export const defaultStoreOrdersFields = [
|
|||||||
"currency_code",
|
"currency_code",
|
||||||
"tax_rate",
|
"tax_rate",
|
||||||
"created_at",
|
"created_at",
|
||||||
"shipping_total",
|
|
||||||
"discount_total",
|
|
||||||
"tax_total",
|
|
||||||
"items.refundable",
|
"items.refundable",
|
||||||
"refunded_total",
|
|
||||||
"gift_card_total",
|
|
||||||
"subtotal",
|
|
||||||
"total",
|
|
||||||
] as (keyof Order)[]
|
] as (keyof Order)[]
|
||||||
|
|
||||||
export const allowedStoreOrdersFields = [
|
export const allowedStoreOrdersFields = [
|
||||||
|
|||||||
@@ -13872,6 +13872,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"axios@npm:^1.3.4":
|
||||||
|
version: 1.3.4
|
||||||
|
resolution: "axios@npm:1.3.4"
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: ^1.15.0
|
||||||
|
form-data: ^4.0.0
|
||||||
|
proxy-from-env: ^1.1.0
|
||||||
|
checksum: 39f03d83a9ed5760094f92a677af2533ab159448c8e22bfba98d8957bdef2babe142e117a0a7d9a5aff1d5f28f8ced28eb0471b6a91d33410375c89e49032193
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"axobject-query@npm:^2.2.0":
|
"axobject-query@npm:^2.2.0":
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
resolution: "axobject-query@npm:2.2.0"
|
resolution: "axobject-query@npm:2.2.0"
|
||||||
@@ -25020,20 +25031,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"jest-environment-node@npm:25.5.0, jest-environment-node@npm:^25.5.0":
|
|
||||||
version: 25.5.0
|
|
||||||
resolution: "jest-environment-node@npm:25.5.0"
|
|
||||||
dependencies:
|
|
||||||
"@jest/environment": ^25.5.0
|
|
||||||
"@jest/fake-timers": ^25.5.0
|
|
||||||
"@jest/types": ^25.5.0
|
|
||||||
jest-mock: ^25.5.0
|
|
||||||
jest-util: ^25.5.0
|
|
||||||
semver: ^6.3.0
|
|
||||||
checksum: 6c5484f828757abc5d9878d77a7c8d76b44d00de51cd056fc37d0817ae6a5d74ec543a8e02bcd2d8e3a433ec98b416f6c2038919487b9d3eca92d9dd223f0115
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"jest-environment-node@npm:26.6.2, jest-environment-node@npm:^26.6.2":
|
"jest-environment-node@npm:26.6.2, jest-environment-node@npm:^26.6.2":
|
||||||
version: 26.6.2
|
version: 26.6.2
|
||||||
resolution: "jest-environment-node@npm:26.6.2"
|
resolution: "jest-environment-node@npm:26.6.2"
|
||||||
@@ -25048,6 +25045,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jest-environment-node@npm:^25.5.0":
|
||||||
|
version: 25.5.0
|
||||||
|
resolution: "jest-environment-node@npm:25.5.0"
|
||||||
|
dependencies:
|
||||||
|
"@jest/environment": ^25.5.0
|
||||||
|
"@jest/fake-timers": ^25.5.0
|
||||||
|
"@jest/types": ^25.5.0
|
||||||
|
jest-mock: ^25.5.0
|
||||||
|
jest-util: ^25.5.0
|
||||||
|
semver: ^6.3.0
|
||||||
|
checksum: 6c5484f828757abc5d9878d77a7c8d76b44d00de51cd056fc37d0817ae6a5d74ec543a8e02bcd2d8e3a433ec98b416f6c2038919487b9d3eca92d9dd223f0115
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"jest-environment-node@npm:^27.5.1":
|
"jest-environment-node@npm:^27.5.1":
|
||||||
version: 27.5.1
|
version: 27.5.1
|
||||||
resolution: "jest-environment-node@npm:27.5.1"
|
resolution: "jest-environment-node@npm:27.5.1"
|
||||||
@@ -28527,29 +28538,17 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "medusa-payment-paypal@workspace:packages/medusa-payment-paypal"
|
resolution: "medusa-payment-paypal@workspace:packages/medusa-payment-paypal"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/cli": ^7.7.5
|
"@medusajs/medusa": ^1.8.0-rc.0
|
||||||
"@babel/core": ^7.7.5
|
|
||||||
"@babel/node": ^7.7.4
|
|
||||||
"@babel/plugin-proposal-class-properties": ^7.7.4
|
|
||||||
"@babel/plugin-proposal-optional-chaining": ^7.12.7
|
|
||||||
"@babel/plugin-transform-classes": ^7.9.5
|
|
||||||
"@babel/plugin-transform-instanceof": ^7.8.3
|
|
||||||
"@babel/plugin-transform-runtime": ^7.7.6
|
|
||||||
"@babel/preset-env": ^7.7.5
|
|
||||||
"@babel/register": ^7.7.4
|
|
||||||
"@babel/runtime": ^7.9.6
|
|
||||||
"@paypal/checkout-server-sdk": ^1.0.3
|
"@paypal/checkout-server-sdk": ^1.0.3
|
||||||
|
"@types/stripe": ^8.0.417
|
||||||
|
axios: ^1.3.4
|
||||||
body-parser: ^1.19.0
|
body-parser: ^1.19.0
|
||||||
client-sessions: ^0.8.0
|
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
express: ^4.17.1
|
express: ^4.17.1
|
||||||
jest: ^25.5.4
|
jest: ^25.5.4
|
||||||
jest-environment-node: 25.5.0
|
|
||||||
medusa-core-utils: ^1.2.0-rc.0
|
medusa-core-utils: ^1.2.0-rc.0
|
||||||
medusa-interfaces: ^1.3.7-rc.0
|
|
||||||
medusa-test-utils: ^1.1.40-rc.0
|
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
medusa-interfaces: 1.3.7-rc.0
|
"@medusajs/medusa": ^1.7.7
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user