feature!: more CRUD, also renamed .get to .details to match PayPal docs

This commit is contained in:
AJ ONeal 2021-10-12 01:24:06 -06:00
parent 82d225cafc
commit 0a9d277317
4 changed files with 217 additions and 19 deletions

View File

@ -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")
<img src="https://www.paypalobjects.com/webstatic/en_US/i/buttons/checkout-logo-large.png" alt="Check out with PayPal" />
The Good Documentation™ for the PayPal API (a.k.a. PayPal Checkout SDK) is the
"REST API". See
- <https://developer.paypal.com/docs/api/orders/v2/> (one-time payments)
- <https://developer.paypal.com/docs/api/subscriptions/v1/> (recurring
subscriptions)
- <https://www.paypal.com/webapps/mpp/logos-buttons> (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:
- <https://developer.paypal.com/developer/applications>
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
- <https://developer.paypal.com/docs/api/orders/v2/> (one-time payments)
- <https://developer.paypal.com/docs/api/subscriptions/v1/> (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.

View File

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

View File

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

View File

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