From 0a9d27731719b4366db011b7dd1fdee0d07313ab Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 12 Oct 2021 01:24:06 -0600 Subject: [PATCH] feature!: more CRUD, also renamed .get to .details to match PayPal docs --- README.md | 54 +++++++++++-- paypal-checkout.js | 152 +++++++++++++++++++++++++++++++++++-- test.js | 2 +- tests/{list.js => crud.js} | 28 ++++++- 4 files changed, 217 insertions(+), 19 deletions(-) rename tests/{list.js => crud.js} (51%) diff --git a/README.md b/README.md index 4f9d7e4..334c6e8 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,26 @@ with lots of abstraction without much value - this is very little abstraction, but specificially designed to be (mostly) idiomatic JavaScript / Node.js. \ (excuse the `snake_case` - that's how the PayPal REST API is designed). +![](https://i.imgur.com/brFTseM.png "PayPal Checkout API Flow") + +Check out with PayPal + +The Good Documentation™ for the PayPal API (a.k.a. PayPal Checkout SDK) is the +"REST API". See + +- (one-time payments) +- (recurring + subscriptions) +- (the buttons) + +# Install + ```bash npm install --save @root/paypal-checkout ``` +# Usage + ```js "use strict"; @@ -25,15 +41,39 @@ PPC.Subscriptions.createRequest({ }); ``` +# API + +```txt +PayPal.init(client_id, client_secret, 'sandbox|live', defaults); +PayPal.request({ method, url, headers, json }); + +PayPal.Product.create({ ... }); // event_type: "CATALOG.PRODUCT.CREATED" +PayPal.Product.list(); +PayPal.Product.details(id); +PayPal.Product.update(id, { description }); // event_type: "CATALOG.PRODUCT.UPDATED" + +PayPal.Plan.create({ ... }); // event_type: "BILLING.PLAN.CREATED" +PayPal.Plan.list(); +PayPal.Plan.details(id); +PayPal.Plan.update(id, { description }); // event_type: "BILLING.PLAN.UPDATED" + +PayPal.Subscription.create({ ... }); +PayPal.Subscription.details(id); +PayPal.Subscription.cancel(id, { reason }); +``` + +# Webhooks + +Webhooks can be set up in the Application section of the Dashboard: + +- + +You'll see a list of applications. Click on one to access the webhooks. + +# Notes + ![](https://i.imgur.com/brFTseM.png "PayPal Checkout API Flow") -The Good Documentation™ for the PayPal API (a.k.a. PayPal Checkout SDK) is the -"REST API". See - -- (one-time payments) -- (recurring - subscriptions) - Note: Just about everything in the PayPal SDK that uses `ALL_CAPS` is a `constant`/`enum` representing an option you can pick from limited number of options. diff --git a/paypal-checkout.js b/paypal-checkout.js index d99ff08..6d626ce 100644 --- a/paypal-checkout.js +++ b/paypal-checkout.js @@ -1,13 +1,34 @@ "use strict"; +let qs = require("querystring"); + let request = require("@root/request"); let PayPal = {}; -PayPal.init = function (client_id, client_secret) { +PayPal.init = function (client_id, client_secret, env, opts) { + if (!opts) { + opts = {}; + } + if (!("total_required" in opts)) { + opts.total_required = true; + } + if (!opts.page_size) { + opts.page_size = 20; + } + if (!opts.prefer) { + opts.prefer = "return=representation"; + } + PayPal.__sandboxUrl = "https://api-m.sandbox.paypal.com"; PayPal.__baseUrl = PayPal.__sandboxUrl; PayPal.__id = client_id; PayPal.__secret = client_secret; + PayPal.__defaultQuery = { + page_size: opts.page_size, + total_required: opts.total_required, + page: 1, + }; + PayPal.__prefer = opts.prefer; }; PayPal.request = async function _paypalRequest(reqObj) { let headers = {}; @@ -24,6 +45,30 @@ PayPal.request = async function _paypalRequest(reqObj) { }; return await request(reqObj).then(sanitize); }; +PayPal._patch = function (obj) { + let ops = []; + + Object.keys(obj).forEach(function (k) { + let val = obj[k]; + if ("undefined" === typeof val) { + return; + } + + let op = "replace"; + if (null === val) { + op = "delete"; + val = undefined; + } + + ops.push({ + path: `/${k}`, + op: op, + value: val, + }); + }); + + return ops; +}; function justBody(resp) { return resp.body; @@ -52,6 +97,14 @@ function must201or200(resp) { } return resp; } +function must204or200(resp) { + if (![200, 204].includes(resp.statusCode)) { + let err = new Error("[@root/paypal-checkout] bad response"); + err.response = resp; + throw err; + } + return resp; +} /* function enumify(obj) { @@ -127,15 +180,49 @@ Product.create = async function _createProduct({ .then(must201or200) .then(justBody); }; -Product.list = async function _listProducts() { + +Product.list = async function _listProducts(query = {}) { + query = Object.assign({}, PayPal.__defaultQuery, query); + let search = qs.stringify(query); return await PayPal.request({ - url: "/v1/catalogs/products?page_size=20&total_required=true", + url: `/v1/catalogs/products?${search}`, json: true, }) .then(must201or200) .then(justBody); }; +Product.details = async function _showProductDetails(id) { + return await PayPal.request({ + url: `/v1/catalogs/products/${id}`, + json: true, + }) + .then(must201or200) + .then(justBody); +}; + +/** + * Update product info + * @param id + * @param {{ + * description: string, + * category: string, + * image_url: string, + * home_url: string, + * }} + */ +Product.update = async function _updateProduct( + id, + { description, category, image_url, home_url } +) { + let body = PayPal._patch({ description, category, image_url, home_url }); + return await PayPal.request({ + method: "PATCH", + url: `/v1/catalogs/products/${id}`, + json: body, + }).then(must204or200); +}; + let Plan = {}; Plan.intervals = { DAY: "DAY", @@ -209,16 +296,50 @@ Plan.create = async function _createPlan({ .then(justBody); }; -Plan.list = async function _listPlans() { - // TODO paging +Plan.list = async function _listPlans(query = {}) { + query = Object.assign({}, PayPal.__defaultQuery, query); + let search = qs.stringify(query); return await PayPal.request({ - url: "/v1/billing/plans?page_size=20&total_required=true", + url: `/v1/billing/plans?${search}`, json: true, }) .then(must201or200) .then(justBody); }; +Plan.details = async function _showPlanDetails(id) { + return await PayPal.request({ + url: `/v1/billing/plans/${id}`, + json: true, + }) + .then(must201or200) + .then(justBody); +}; + +/** + * Update plan info + * @param id + * @param {{ + * description: string, + * payment_preferences.auto_bill_outstandin: boolean, + * taxes.percentage: string, + * payment_preferences.payment_failure_threshold: number, + * payment_preferences.setup_fee: string, + * payment_preferences.setup_fee_failure_action: string, + * }} + */ +Plan.update = async function _updatePlan( + id, + // TODO handle nested keys (ex: 'taxes.percentage') + { description /*, payment_preferences, taxes*/ } +) { + return await PayPal.request({ + method: "PATCH", + url: `/v1/billing/plans/${id}`, + json: PayPal._patch({ description }), + }).then(must204or200); +}; + let Subscription = {}; Subscription.actions = { CONTINUE: "CONTINUE", @@ -298,7 +419,7 @@ Subscription.createRequest = async function _createSubscription({ .then(justBody); }; -Subscription.get = async function _getSubscription(id) { +Subscription.details = async function _getSubscription(id) { return await PayPal.request({ url: `/v1/billing/subscriptions/${id}`, json: true, @@ -307,6 +428,23 @@ Subscription.get = async function _getSubscription(id) { .then(justBody); }; +/** + * Cancel a subscription (prevent future auto billing) + * @param id + * @param {{ + * reason: string + * }} + */ +Subscription.cancel = async function _showProductDetails(id, { reason }) { + return await PayPal.request({ + method: "POST", + url: `/v1/catalogs/products/${id}/cancel`, + json: { reason }, + }) + .then(must201or200) + .then(justBody); +}; + module.exports.init = PayPal.init; module.exports.request = PayPal.request; module.exports.Plan = Plan; diff --git a/test.js b/test.js index 286a14c..1b9e14f 100644 --- a/test.js +++ b/test.js @@ -141,7 +141,7 @@ async function test() { }); process.stdin.pause(); - let s = await Subscription.get(subscription.id); + let s = await Subscription.details(subscription.id); console.info("Subscription: (After Approval)"); console.info(JSON.stringify(s, null, 2)); console.info(); diff --git a/tests/list.js b/tests/crud.js similarity index 51% rename from tests/list.js rename to tests/crud.js index 9473a7a..a869280 100644 --- a/tests/list.js +++ b/tests/crud.js @@ -16,13 +16,33 @@ let { Plan, Product, Subscription } = PayPal; async function test() { let products = await Product.list(); console.info(); - console.info("Products:"); - console.info(JSON.stringify(products, null, 2)); + console.info("Products:", products.products.length); + //console.info(JSON.stringify(products, null, 2)); + + if (products.products.length) { + let product = await Product.details(products.products[0].id); + console.info("Product 0:"); + console.info(JSON.stringify(product, null, 2)); + + await Product.update(product.id, { + description: `Product Description 10${Math.random()}`, + }); + } let plans = await Plan.list(); console.info(); - console.info("Plans:"); - console.info(JSON.stringify(plans, null, 2)); + console.info("Plans:", plans.plans.length); + //console.info(JSON.stringify(plans, null, 2)); + + if (plans.plans.length) { + let plan = await Plan.details(plans.plans[0].id); + console.info("Plan 0:"); + console.info(JSON.stringify(plan, null, 2)); + + await Plan.update(plan.id, { + description: `Plan Description 20${Math.random()}`, + }); + } console.info(); }