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).
+
+
+
+
+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
+

-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();
}