14
packages/medusa-payment-paypal/.babelrc
Normal file
14
packages/medusa-payment-paypal/.babelrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/medusa-payment-paypal/.eslintrc
Normal file
9
packages/medusa-payment-paypal/.eslintrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"semi": "error",
|
||||
"no-unused-expressions": "true"
|
||||
}
|
||||
}
|
||||
16
packages/medusa-payment-paypal/.gitignore
vendored
Normal file
16
packages/medusa-payment-paypal/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
/dist
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/loaders
|
||||
/__mocks__
|
||||
13
packages/medusa-payment-paypal/.npmignore
Normal file
13
packages/medusa-payment-paypal/.npmignore
Normal file
@@ -0,0 +1,13 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
src
|
||||
.gitignore
|
||||
.eslintrc
|
||||
.babelrc
|
||||
.prettierrc
|
||||
|
||||
7
packages/medusa-payment-paypal/.prettierrc
Normal file
7
packages/medusa-payment-paypal/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
11
packages/medusa-payment-paypal/README.md
Normal file
11
packages/medusa-payment-paypal/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# medusa-payment-paypal
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
sandbox: [default: false],
|
||||
client_id: "CLIENT_ID", REQUIRED
|
||||
client_secret: "CLIENT_SECRET", REQUIRED
|
||||
auth_webhook_id: REQUIRED for webhook to work
|
||||
```
|
||||
|
||||
1
packages/medusa-payment-paypal/index.js
Normal file
1
packages/medusa-payment-paypal/index.js
Normal file
@@ -0,0 +1 @@
|
||||
// noop
|
||||
47
packages/medusa-payment-paypal/package.json
Normal file
47
packages/medusa-payment-paypal/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "medusa-payment-paypal",
|
||||
"version": "1.0.0",
|
||||
"description": "Paypal Payment provider for Meduas Commerce",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/medusa-payment-paypal"
|
||||
},
|
||||
"author": "Sebastian Rindom",
|
||||
"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",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2",
|
||||
"medusa-test-utils": "^1.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src -d . --ignore **/__tests__",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir . --ignore **/__tests__",
|
||||
"test": "jest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"medusa-interfaces": "1.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paypal/checkout-server-sdk": "^1.0.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^1.1.0"
|
||||
},
|
||||
"gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
|
||||
}
|
||||
10
packages/medusa-payment-paypal/src/api/index.js
Normal file
10
packages/medusa-payment-paypal/src/api/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Router } from "express"
|
||||
import hooks from "./routes/hooks"
|
||||
|
||||
export default (container) => {
|
||||
const app = Router()
|
||||
|
||||
hooks(app)
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default (fn) => (...args) => fn(...args).catch(args[2])
|
||||
@@ -0,0 +1,5 @@
|
||||
import { default as wrap } from "./await-middleware"
|
||||
|
||||
export default {
|
||||
wrap,
|
||||
}
|
||||
13
packages/medusa-payment-paypal/src/api/routes/hooks/index.js
Normal file
13
packages/medusa-payment-paypal/src/api/routes/hooks/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
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,55 @@
|
||||
export default async (req, res) => {
|
||||
const auth_algo = req.headers["paypal-auth-algo"]
|
||||
const cert_url = req.headers["paypal-cert-url"]
|
||||
const transmission_id = req.headers["paypal-transmission-id"]
|
||||
const transmission_sig = req.headers["paypal-transmission-sig"]
|
||||
const transmission_time = req.headers["paypal-transmission-time"]
|
||||
|
||||
const paypalService = req.scope.resolve("paypalProviderService")
|
||||
|
||||
try {
|
||||
await paypalService.verifyWebhook({
|
||||
auth_algo,
|
||||
cert_url,
|
||||
transmission_id,
|
||||
transmission_sig,
|
||||
transmission_time,
|
||||
webhook_event: req.body,
|
||||
})
|
||||
} catch (err) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const authId = req.body.resource.id
|
||||
const auth = await paypalService.retrieveAuthorization(authId)
|
||||
|
||||
const order = await paypalService.retrieveOrderFromAuth(auth)
|
||||
|
||||
const purchaseUnit = order.purchase_units[0]
|
||||
const cartId = purchaseUnit.custom_id
|
||||
|
||||
const manager = req.scope.resolve("manager")
|
||||
const cartService = req.scope.resolve("cartService")
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
|
||||
await manager.transaction(async (m) => {
|
||||
const order = await orderService
|
||||
.withTransaction(m)
|
||||
.retrieveByCartId(cartId)
|
||||
.catch((_) => undefined)
|
||||
|
||||
if (!order) {
|
||||
await cartService.withTransaction(m).setPaymentSession(cartId, "paypal")
|
||||
await cartService.withTransaction(m).authorizePayment(cartId)
|
||||
await orderService.withTransaction(m).createFromCart(cartId)
|
||||
}
|
||||
})
|
||||
|
||||
res.sendStatus(200)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.sendStatus(409)
|
||||
}
|
||||
}
|
||||
402
packages/medusa-payment-paypal/src/services/paypal-provider.js
Normal file
402
packages/medusa-payment-paypal/src/services/paypal-provider.js
Normal file
@@ -0,0 +1,402 @@
|
||||
import _ from "lodash"
|
||||
import PayPal from "@paypal/checkout-server-sdk"
|
||||
import { PaymentService } from "medusa-interfaces"
|
||||
|
||||
class PayPalProviderService extends PaymentService {
|
||||
static identifier = "paypal"
|
||||
|
||||
constructor({ customerService, totalsService, 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 {CustomerService} */
|
||||
this.customerService_ = customerService
|
||||
|
||||
/** @private @const {RegionService} */
|
||||
this.regionService_ = regionService
|
||||
|
||||
/** @private @const {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an open PayPal order and maps its status to Medusa payment
|
||||
* statuses.
|
||||
* @param {object} paymentData - the data stored with the payment
|
||||
* @returns {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"
|
||||
// return "captured"
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported
|
||||
*/
|
||||
async retrieveSavedMethods(customer) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const { customer_id, region_id, email } = cart
|
||||
const { currency_code } = await this.regionService_.retrieve(region_id)
|
||||
|
||||
const amount = await this.totalsService_.getTotal(cart)
|
||||
|
||||
const request = new PayPal.orders.OrdersCreateRequest()
|
||||
request.requestBody({
|
||||
intent: "AUTHORIZE",
|
||||
application_context: {
|
||||
shipping_preference: "NO_SHIPPING",
|
||||
},
|
||||
purchase_units: [
|
||||
{
|
||||
custom_id: cart.id,
|
||||
amount: {
|
||||
currency_code: currency_code.toUpperCase(),
|
||||
value: (amount / 100).toFixed(2),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
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(paymentSession.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 {
|
||||
const { region_id } = cart
|
||||
const { currency_code } = await this.regionService_.retrieve(region_id)
|
||||
|
||||
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: (cart.total / 100).toFixed(),
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await this.paypal_.execute(request)
|
||||
|
||||
return sessionData
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {string} 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: (amountToRefund / 100).toFixed(),
|
||||
},
|
||||
})
|
||||
|
||||
await this.paypal_.execute(request)
|
||||
|
||||
return this.retrievePayment(payment.id)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels payment for Stripe payment intent.
|
||||
* @param {object} paymentData - payment method data from cart
|
||||
* @returns {object} canceled payment intent
|
||||
*/
|
||||
async cancelPayment(payment) {
|
||||
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 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)
|
||||
|
||||
console.log(webhookRes.result.webhooks)
|
||||
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
|
||||
@@ -21,7 +21,7 @@ const defaultRelations = [
|
||||
"claims.fulfillments",
|
||||
"claims.claim_items",
|
||||
"claims.claim_items.images",
|
||||
"claims.claim_items.tags",
|
||||
// "claims.claim_items.tags",
|
||||
"swaps",
|
||||
"swaps.return_order",
|
||||
"swaps.payment",
|
||||
|
||||
@@ -905,11 +905,13 @@ class OrderService extends BaseService {
|
||||
|
||||
const result = await orderRepo.save(order)
|
||||
|
||||
this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(OrderService.Events.PAYMENT_CAPTURED, {
|
||||
id: result.id,
|
||||
})
|
||||
if (order.payment_status === "captured") {
|
||||
this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(OrderService.Events.PAYMENT_CAPTURED, {
|
||||
id: result.id,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user