From 69ed827e3cd728c0093070c9569d2793d481191d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 9 Oct 2021 17:10:40 -0600 Subject: [PATCH] initial commit --- .gitignore | 111 +++++++++ .jshintrc | 7 + .prettierignore | 1 + .prettierrc.json | 8 + LICENSE | 6 + README.md | 65 ++++++ example.env | 9 + package-lock.json | 47 ++++ package.json | 28 +++ paypal-checkout.js | 295 ++++++++++++++++++++++++ test.js | 156 +++++++++++++ utils/make-categories.js | 475 +++++++++++++++++++++++++++++++++++++++ 12 files changed, 1208 insertions(+) create mode 100644 .gitignore create mode 100644 .jshintrc create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 example.env create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 paypal-checkout.js create mode 100644 test.js create mode 100644 utils/make-categories.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..779e6ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,111 @@ +privkey.* +.env.* +lib/categories.json + +# vim swap files +.*.sw* + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..87624d5 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,7 @@ +{ + "esversion": 11, + "node": true, + "browser": true, + "curly": true, + "sub": true +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..121531a --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*.min.js diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..d31a491 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "singleQuote": false, + "bracketSpacing": true, + "semi": true, + "proseWrap": "always" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1646036 --- /dev/null +++ b/LICENSE @@ -0,0 +1,6 @@ +Copyright AJ ONeal 2021 +Copyright The Root Group, LLC 2021 + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. diff --git a/README.md b/README.md new file mode 100644 index 0000000..94d3db9 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# @root/paypal-checkout + +In contrast to the official PayPal Checkout SDK - which is auto-generated code +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). + +```bash +npm install --save @root/paypal-checkout +``` + +```js +"use strict"; + +require("dotenv").config({ path: ".env" }); + +let PPC = require("@root/paypal-checkout"); +PPC.init({ + client_id: "xxxx", + client_secret: "****", +}); + +PPC.Subscriptions.create({ +}) +``` + +![](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. + +Sandbox accounts (for creating fake purchases) can be managed at: + + +Note on Auth + Capture: + +> Authorization and capture enables you to authorize fund availability but delay +> fund capture. This can be useful for merchants who have a delayed order +> fulfillment process. Authorize & Capture also enables merchants to change the +> original authorization amount in case the order changes due to shipping, +> taxes, or gratuity. +> +> For any payment type, you can capture less than or the full original +> authorized amount. You can also capture up to 115% of or $75 USD more than the +> original authorized amount, whichever is less. +> +> See +> +> - +> - + +Buttons: + +- <== THE ONE YOU WANT + - Check out with PayPal +- +- diff --git a/example.env b/example.env new file mode 100644 index 0000000..69ae336 --- /dev/null +++ b/example.env @@ -0,0 +1,9 @@ +# shellcheck disable=SC2034 + +# Set to 'production' for the Live PayPal API +NODE_ENV=development + +PAYPAL_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +PAYPAL_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +PAYPAL_SANDBOX_EMAIL=sb-xxxxxxxxxxxx@personal.example.com +PAYPAL_SANDBOX_PASSWORD="********" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b28b07a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,47 @@ +{ + "name": "@root/paypal-checkout", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@root/paypal-checkout", + "version": "0.1.0", + "hasInstallScript": true, + "license": "MPL-2.0", + "dependencies": { + "@root/request": "^1.7.0" + }, + "devDependencies": { + "dotenv": "^10.0.0" + } + }, + "node_modules/@root/request": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.7.0.tgz", + "integrity": "sha512-lre7XVeEwszgyrayWWb/kRn5fuJfa+n0Nh+rflM9E+EpC28yIYA+FPm/OL1uhzp3TxhQM0HFN4FE2RDIPGlnmg==" + }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + } + }, + "dependencies": { + "@root/request": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.7.0.tgz", + "integrity": "sha512-lre7XVeEwszgyrayWWb/kRn5fuJfa+n0Nh+rflM9E+EpC28yIYA+FPm/OL1uhzp3TxhQM0HFN4FE2RDIPGlnmg==" + }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..803dcd1 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "@root/paypal-checkout", + "version": "0.1.0", + "description": "A more sensible, human-generated wrapper for the PayPal Checkout REST API", + "main": "paypal-checkout.js", + "files": ["lib"], + "scripts": { + "postinstall": "node utils/make-categories.js", + "test": "node test.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/therootcompany/paypal-checkout.js.git" + }, + "keywords": ["paypal", "checkout", "sdk", "rest", "api", "subscriptions"], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "MPL-2.0", + "bugs": { + "url": "https://github.com/therootcompany/paypal-checkout.js/issues" + }, + "homepage": "https://github.com/therootcompany/paypal-checkout.js#readme", + "devDependencies": { + "dotenv": "^10.0.0" + }, + "dependencies": { + "@root/request": "^1.7.0" + } +} diff --git a/paypal-checkout.js b/paypal-checkout.js new file mode 100644 index 0000000..fef2e33 --- /dev/null +++ b/paypal-checkout.js @@ -0,0 +1,295 @@ +"use strict"; + +let request = require("@root/request"); + +let PayPal = {}; +PayPal.init = function (id, secret) { + PayPal.__sandboxUrl = "https://api-m.sandbox.paypal.com"; + PayPal.__baseUrl = PayPal.__sandboxUrl; + PayPal.__id = id; + PayPal.__secret = secret; +}; +PayPal.request = async function _paypalRequest(reqObj) { + let headers = {}; + if (reqObj.id) { + // Optional and if passed, helps identify idempotent requests + headers["PayPal-Request-Id"] = reqObj.id; + } + // ex: https://api-m.sandbox.paypal.com/v1/billing/subscriptions + reqObj.url = `${PayPal.__baseUrl}${reqObj.url}`; + reqObj.headers = Object.assign(headers, reqObj.headers || {}); + reqObj.auth = { + user: PayPal.__id, + pass: PayPal.__secret, + }; + return await request(reqObj).then(sanitize); +}; + +function justBody(resp) { + return resp.body; +} + +function sanitize(resp) { + resp = resp.toJSON(); + Object.keys(resp.headers).forEach(function (k) { + if (k.toLowerCase().match(/Auth|Cookie|Token|Key/i)) { + resp.headers[k] = "[redacted]"; + } + }); + Object.keys(resp.request.headers).forEach(function (k) { + if (k.toLowerCase().match(/Auth|Cookie|Token|Key/i)) { + resp.request.headers[k] = "[redacted]"; + } + }); + return resp; +} + +function must201or200(resp) { + if (![200, 201].includes(resp.statusCode)) { + let err = new Error("bad response"); + err.response = resp; + throw err; + } + return resp; +} + +/* +function enumify(obj) { + Object.keys(obj).forEach(function (k) { + obj[k] = k; + }); +} +*/ + +let Product = {}; + +// SaaS would be type=SERVICE, category=SOFTWARE +Product.types = { + DIGITAL: "DIGITAL", + PHYSICAL: "PHYSICAL", + SERVICE: "SERVICE", +}; +Product.__typeNames = Object.keys(Product.types); + +// Documented under "categories" at +// https://developer.paypal.com/docs/api/catalog-products/v1/ +Product.categories = require("./lib/categories.json"); +Product.__categoryNames = Object.keys(Product.categories); +/* +Product.categories = { + SOFTWARE: "SOFTWARE", + PHYSICAL_GOOD: "PHYSICAL_GOOD", + DIGITAL_MEDIA_BOOKS_MOVIES_MUSIC: "DIGITAL_MEDIA_BOOKS_MOVIES_MUSIC", + DIGITAL_GAMES: "DIGITAL_GAMES", +}; +*/ + +Product.create = async function _createSubscription({ + id, + name, + description, + type, + category, + image_url, + home_url, +}) { + if (id) { + if (!id.startsWith("PROD-")) { + console.warn(`Warn: product ID should start with "PROD-"`); + } + } + if (!Product.__typeNames.includes(type)) { + console.warn(`Warn: unknown product type '${type}'`); + } + if (!Product.__categoryNames.includes(category)) { + console.warn(`Warn: unknown product category '${category}'`); + } + + return await PayPal.request({ + method: "POST", + url: "/v1/catalogs/products", + id: id, + json: { + // ex: "Video Streaming Service" + name: name, + // ex: "Video streaming service" + description: description, + // ex: "SERVICE", "PHYSICAL", "DIGITAL" + type: type, + // ex: "SOFTWARE", "PHYSICAL_GOOD" + category: category, + // ex: "https://example.com/streaming.jpg" + image_url: image_url, + // ex: "https://example.com/home" + home_url: home_url, + }, + }) + .then(must201or200) + .then(justBody); +}; + +let Plan = {}; +Plan.intervals = { + DAY: "DAY", + WEEK: "WEEK", + MONTH: "MONTH", + YEAR: "YEAR", +}; +Plan.tenures = { + TRIAL: "TRIAL", + REGULAR: "REGULAR", +}; + +// See https://developer.paypal.com/docs/api/subscriptions/v1/ +Plan.create = async function _createPlan({ + id, + status = "ACTIVE", + product_id, + name, + description = "", + billing_cycles, + payment_preferences, + taxes, // optional + quantity_supported = false, +}) { + let headers = {}; + if (id) { + if (!id.startsWith("PLAN-")) { + // ex: PLAN-18062020-001 + console.warn(`Warn: plan ID should start with "PLAN-"`); + } + } + headers["Prefer"] = "return=representation"; + return await PayPal.request({ + method: "POST", + url: "/v1/billing/plans", + id: id, + headers: headers, + json: { + // ex: "PROD-6XB24663H4094933M" + product_id: product_id, + // ex: "Basic Plan" + name: name, + // ex: "Basic plan" + description: description, + // ex: "CREATED", "ACTIVE", "INACTIVE" + status: status, + // ex: TODO + billing_cycles: billing_cycles.map(function (cycle, i) { + // sequence is the index in the array, + // which should never be out-of-order + if (!cycle.frequency.interval_count) { + cycle.frequency.interval_count = 1; + } + cycle.sequence = i + 1; + if (!cycle.tenure_type) { + cycle.tenure_type = Plan.tenures.REGULAR; + } + if (!cycle.total_cycles) { + cycle.total_cycles = 0; + } + return cycle; + }), + // TODO ??? + payment_preferences: payment_preferences, + taxes: taxes, + quantity_supported: quantity_supported, + }, + }) + .then(must201or200) + .then(justBody); +}; + +let Subscription = {}; +Subscription.actions = { + CONTINUE: "CONTINUE", + SUBSCRIBE_NOW: "SUBSCRIBE_NOW", +}; +Subscription.shipping_preferences = { + GET_FROM_FILE: "GET_FROM_FILE", // provided, or selectable from PayPal addresses + SET_PROVIDED_ADDRESS: "SET_PROVIDED_ADDRESS", // user can't change it here + NO_SHIPPING: "NO_SHIPPING", // duh +}; +Subscription.payer_selections = { + PAYPAL: "PAYPAL", +}; +Subscription.payee_preferences = { + UNRESTRICTED: "UNRESTRICTED", + IMMEDIATE_PAYMENT_REQUIRED: "IMMEDIATE_PAYMENT_REQUIRED", +}; + +Subscription.createRequest = async function _createSubscription({ + id, + plan_id, + start_time, + quantity, + shipping_amount, + subscriber, + application_context, +}) { + return await PayPal.request({ + method: "POST", + url: "/v1/billing/subscriptions", + id: id, + json: { + // ex: "P-5ML4271244454362WXNWU5NQ" + plan_id: plan_id, + // ex: "2018-11-01T00:00:00Z" (must be in the future) + start_time: start_time, + // ex: "20" + quantity: quantity, + // ex: { currency_code: "USD", value: "10.00", }, + shipping_amount: shipping_amount, + /* ex: + { + name: { given_name: "John", surname: "Doe" }, + email_address: "customer@example.com", + shipping_address: { + name: { full_name: "John Doe" }, + address: { + address_line_1: "123 Sesame Street", + address_line_2: "Building 17", + admin_area_2: "San Jose", + admin_area_1: "CA", + postal_code: "95131", + country_code: "US", + }, + } + } + */ + subscriber: subscriber, + /* ex: + { + brand_name: "walmart", + locale: "en-US", + shipping_preference: "SET_PROVIDED_ADDRESS", + user_action: "SUBSCRIBE_NOW", + payment_method: { + payer_selected: "PAYPAL", + payee_preferred: "IMMEDIATE_PAYMENT_REQUIRED", + }, + return_url: "https://example.com/returnUrl", + cancel_url: "https://example.com/cancelUrl", + } + */ + application_context: application_context, + }, + }) + .then(must201or200) + .then(justBody); +}; + +Subscription.get = async function _getSubscription(id) { + return await PayPal.request({ + url: `/v1/billing/subscriptions/${id}`, + json: true, + }) + .then(must201or200) + .then(justBody); +}; + +module.exports.init = PayPal.init; +module.exports.request = PayPal.request; +module.exports.Plan = Plan; +module.exports.Product = Product; +module.exports.Subscription = Subscription; diff --git a/test.js b/test.js new file mode 100644 index 0000000..286a14c --- /dev/null +++ b/test.js @@ -0,0 +1,156 @@ +"use strict"; + +require("dotenv").config({ path: ".env" }); +require("dotenv").config({ path: ".env.secret" }); + +if (!process.env.PAYPAL_CLIENT_ID) { + console.error( + "Please copy example.env to .env and update the values from the PayPal API Dashboard at https://developer.paypal.com/developer/applications" + ); + process.exit(1); +} + +let PayPal = require("./"); +let { Plan, Product, Subscription } = PayPal; + +async function test() { + console.info(); + + let product = await Product.create({ + id: "PROD-test-product-10", + name: "Test Product #10", + description: "A great widget for gizmos and gadgets of all ages!", + type: Product.types.SERVICE, + category: Product.categories.SOFTWARE, + image_url: undefined, + home_url: undefined, + }); + console.info("Product:"); + console.info(JSON.stringify(product, null, 2)); + console.info(); + + let plan = await Plan.create({ + id: "PLAN-test-plan-001", + product_id: "PROD-2TS60422HM5801517", // product.id, + name: "Test Plan #1", + description: "A great plan for pros of all ages!", + billing_cycles: [ + { + frequency: { + interval_unit: Plan.intervals.DAY, + interval_count: 1, + }, + tenure_type: Plan.tenures.TRIAL, + total_cycles: 14, + }, + { + frequency: { + interval_unit: Plan.intervals.YEAR, + interval_count: 1, + }, + tenure_type: Plan.tenures.REGULAR, + total_cycles: 0, + pricing_scheme: { + fixed_price: { + value: "10.00", + currency_code: "USD", + }, + }, + }, + ], + payment_preferences: { + auto_bill_outstanding: true, + setup_fee: { + value: "10", + currency_code: "USD", + }, + setup_fee_failure_action: "CONTINUE", + // suspend the subscription after N attempts + payment_failure_threshold: 3, + }, + taxes: { + percentage: "10", + // was tax included? + inclusive: false, + }, + }); + console.info("Plan:"); + console.info(JSON.stringify(plan, null, 2)); + console.info(); + + let subscription = await Subscription.createRequest({ + // See https://developer.paypal.com/docs/subscriptions/integrate/#use-the-subscriptions-api + plan_id: plan.id, + //start_time: "2018-11-01T00:00:00Z", (must be in the future) + //quantity: "20", + //shipping_amount: { currency_code: "USD", value: "10.00" }, + subscriber: { + name: { given_name: "James", surname: "Doe" }, + email_address: "customer@example.com", + /* + shipping_address: { + name: { full_name: "James Doe" }, + address: { + address_line_1: "123 Sesame Street", + address_line_2: "Building 17", + admin_area_2: "San Jose", + admin_area_1: "CA", + postal_code: "95131", + country_code: "US", + }, + }, + */ + }, + application_context: { + brand_name: "root", + locale: "en-US", + shipping_preference: Subscription.shipping_preferences.NO_SHIPPING, + user_action: Subscription.actions.SUBSCRIBE_NOW, + payment_method: { + payer_selected: Subscription.payer_selections.PAYPAL, + payee_preferred: + Subscription.payee_preferences.IMMEDIATE_PAYMENT_REQUIRED, + }, + return_url: + "https://example.com/api/paypal-checkout/return?my_token=abc123", + cancel_url: + "https://example.com/api/paypal-checkout/cancel?my_token=abc123", + }, + }); + console.info("Subscription (Before Approval):"); + console.info(JSON.stringify(subscription, null, 2)); + console.info(); + + // wait for user to click URL and accept + await new Promise(function (resolve) { + console.info(); + console.info("Please approve the subscription at the following URL:"); + console.info(); + console.info( + "Approve URL:", + subscription.links.find(function (link) { + return "approve" === link.rel; + }).href + ); + console.info("Username:", process.env.PAYPAL_SANDBOX_EMAIL); + console.info("Password:", process.env.PAYPAL_SANDBOX_PASSWORD); + console.info(); + console.info("Did you approve it? Hit the key to continue..."); + console.info(); + process.stdin.once("data", resolve); + }); + process.stdin.pause(); + + let s = await Subscription.get(subscription.id); + console.info("Subscription: (After Approval)"); + console.info(JSON.stringify(s, null, 2)); + console.info(); +} + +if (require.main === module) { + PayPal.init(process.env.PAYPAL_CLIENT_ID, process.env.PAYPAL_CLIENT_SECRET); + test().catch(function (err) { + console.error("Something bad happened:"); + console.error(JSON.stringify(err, null, 2)); + }); +} diff --git a/utils/make-categories.js b/utils/make-categories.js new file mode 100644 index 0000000..f851aa9 --- /dev/null +++ b/utils/make-categories.js @@ -0,0 +1,475 @@ +// Documented under "categories" at +// https://developer.paypal.com/docs/api/catalog-products/v1/ +/* + // To scrape the full list from the site: + var categories = []; + var $c = $$('.dax-def-label code').find(function ($el) { + if ("category" === $el.innerText.toLowerCase()) { + return $el; + } + }); + $c.closest('div').nextElementSibling.querySelectorAll('li').forEach(function ($el) { + categories.push($el.querySelector('code').innerText); + }); + console.log(JSON.stringify(categories)); +*/ +let categories = [ + "AC_REFRIGERATION_REPAIR", + "ACADEMIC_SOFTWARE", + "ACCESSORIES", + "ACCOUNTING", + "ADULT", + "ADVERTISING", + "AFFILIATED_AUTO_RENTAL", + "AGENCIES", + "AGGREGATORS", + "AGRICULTURAL_COOPERATIVE_FOR_MAIL_ORDER", + "AIR_CARRIERS_AIRLINES", + "AIRLINES", + "AIRPORTS_FLYING_FIELDS", + "ALCOHOLIC_BEVERAGES", + "AMUSEMENT_PARKS_CARNIVALS", + "ANIMATION", + "ANTIQUES", + "APPLIANCES", + "AQUARIAMS_SEAQUARIUMS_DOLPHINARIUMS", + "ARCHITECTURAL_ENGINEERING_AND_SURVEYING_SERVICES", + "ART_AND_CRAFT_SUPPLIES", + "ART_DEALERS_AND_GALLERIES", + "ARTIFACTS_GRAVE_RELATED_AND_NATIVE_AMERICAN_CRAFTS", + "ARTS_AND_CRAFTS", + "ARTS_CRAFTS_AND_COLLECTIBLES", + "AUDIO_BOOKS", + "AUTO_ASSOCIATIONS_CLUBS", + "AUTO_DEALER_USED_ONLY", + "AUTO_RENTALS", + "AUTO_SERVICE", + "AUTOMATED_FUEL_DISPENSERS", + "AUTOMOBILE_ASSOCIATIONS", + "AUTOMOTIVE", + "AUTOMOTIVE_REPAIR_SHOPS_NON_DEALER", + "AUTOMOTIVE_TOP_AND_BODY_SHOPS", + "AVIATION", + "BABIES_CLOTHING_AND_SUPPLIES", + "BABY", + "BANDS_ORCHESTRAS_ENTERTAINERS", + "BARBIES", + "BATH_AND_BODY", + "BATTERIES", + "BEAN_BABIES", + "BEAUTY", + "BEAUTY_AND_FRAGRANCES", + "BED_AND_BATH", + "BICYCLE_SHOPS_SALES_AND_SERVICE", + "BICYCLES_AND_ACCESSORIES", + "BILLIARD_POOL_ESTABLISHMENTS", + "BOAT_DEALERS", + "BOAT_RENTALS_AND_LEASING", + "BOATING_SAILING_AND_ACCESSORIES", + "BOOKS", + "BOOKS_AND_MAGAZINES", + "BOOKS_MANUSCRIPTS", + "BOOKS_PERIODICALS_AND_NEWSPAPERS", + "BOWLING_ALLEYS", + "BULLETIN_BOARD", + "BUS_LINE", + "BUS_LINES_CHARTERS_TOUR_BUSES", + "BUSINESS", + "BUSINESS_AND_SECRETARIAL_SCHOOLS", + "BUYING_AND_SHOPPING_SERVICES_AND_CLUBS", + "CABLE_SATELLITE_AND_OTHER_PAY_TELEVISION_AND_RADIO_SERVICES", + "CABLE_SATELLITE_AND_OTHER_PAY_TV_AND_RADIO", + "CAMERA_AND_PHOTOGRAPHIC_SUPPLIES", + "CAMERAS", + "CAMERAS_AND_PHOTOGRAPHY", + "CAMPER_RECREATIONAL_AND_UTILITY_TRAILER_DEALERS", + "CAMPING_AND_OUTDOORS", + "CAMPING_AND_SURVIVAL", + "CAR_AND_TRUCK_DEALERS", + "CAR_AND_TRUCK_DEALERS_USED_ONLY", + "CAR_AUDIO_AND_ELECTRONICS", + "CAR_RENTAL_AGENCY", + "CATALOG_MERCHANT", + "CATALOG_RETAIL_MERCHANT", + "CATERING_SERVICES", + "CHARITY", + "CHECK_CASHIER", + "CHILD_CARE_SERVICES", + "CHILDREN_BOOKS", + "CHIROPODISTS_PODIATRISTS", + "CHIROPRACTORS", + "CIGAR_STORES_AND_STANDS", + "CIVIC_SOCIAL_FRATERNAL_ASSOCIATIONS", + "CIVIL_SOCIAL_FRAT_ASSOCIATIONS", + "CLOTHING", + "CLOTHING_ACCESSORIES_AND_SHOES", + "CLOTHING_RENTAL", + "COFFEE_AND_TEA", + "COIN_OPERATED_BANKS_AND_CASINOS", + "COLLECTIBLES", + "COLLECTION_AGENCY", + "COLLEGES_AND_UNIVERSITIES", + "COMMERCIAL_EQUIPMENT", + "COMMERCIAL_FOOTWEAR", + "COMMERCIAL_PHOTOGRAPHY", + "COMMERCIAL_PHOTOGRAPHY_ART_AND_GRAPHICS", + "COMMERCIAL_SPORTS_PROFESSIONA", + "COMMODITIES_AND_FUTURES_EXCHANGE", + "COMPUTER_AND_DATA_PROCESSING_SERVICES", + "COMPUTER_HARDWARE_AND_SOFTWARE", + "COMPUTER_MAINTENANCE_REPAIR_AND_SERVICES_NOT_ELSEWHERE_CLAS", + "CONSTRUCTION", + "CONSTRUCTION_MATERIALS_NOT_ELSEWHERE_CLASSIFIED", + "CONSULTING_SERVICES", + "CONSUMER_CREDIT_REPORTING_AGENCIES", + "CONVALESCENT_HOMES", + "COSMETIC_STORES", + "COUNSELING_SERVICES_DEBT_MARRIAGE_PERSONAL", + "COUNTERFEIT_CURRENCY_AND_STAMPS", + "COUNTERFEIT_ITEMS", + "COUNTRY_CLUBS", + "COURIER_SERVICES", + "COURIER_SERVICES_AIR_AND_GROUND_AND_FREIGHT_FORWARDERS", + "COURT_COSTS_ALIMNY_CHILD_SUPT", + "COURT_COSTS_INCLUDING_ALIMONY_AND_CHILD_SUPPORT_COURTS_OF_LAW", + "CREDIT_CARDS", + "CREDIT_UNION", + "CULTURE_AND_RELIGION", + "DAIRY_PRODUCTS_STORES", + "DANCE_HALLS_STUDIOS_AND_SCHOOLS", + "DECORATIVE", + "DENTAL", + "DENTISTS_AND_ORTHODONTISTS", + "DEPARTMENT_STORES", + "DESKTOP_PCS", + "DEVICES", + "DIECAST_TOYS_VEHICLES", + "DIGITAL_GAMES", + "DIGITAL_MEDIA_BOOKS_MOVIES_MUSIC", + "DIRECT_MARKETING", + "DIRECT_MARKETING_CATALOG_MERCHANT", + "DIRECT_MARKETING_INBOUND_TELE", + "DIRECT_MARKETING_OUTBOUND_TELE", + "DIRECT_MARKETING_SUBSCRIPTION", + "DISCOUNT_STORES", + "DOOR_TO_DOOR_SALES", + "DRAPERY_WINDOW_COVERING_AND_UPHOLSTERY", + "DRINKING_PLACES", + "DRUGSTORE", + "DURABLE_GOODS", + "ECOMMERCE_DEVELOPMENT", + "ECOMMERCE_SERVICES", + "EDUCATIONAL_AND_TEXTBOOKS", + "ELECTRIC_RAZOR_STORES", + "ELECTRICAL_AND_SMALL_APPLIANCE_REPAIR", + "ELECTRICAL_CONTRACTORS", + "ELECTRICAL_PARTS_AND_EQUIPMENT", + "ELECTRONIC_CASH", + "ELEMENTARY_AND_SECONDARY_SCHOOLS", + "EMPLOYMENT", + "ENTERTAINERS", + "ENTERTAINMENT_AND_MEDIA", + "EQUIP_TOOL_FURNITURE_AND_APPLIANCE_RENTAL_AND_LEASING", + "ESCROW", + "EVENT_AND_WEDDING_PLANNING", + "EXERCISE_AND_FITNESS", + "EXERCISE_EQUIPMENT", + "EXTERMINATING_AND_DISINFECTING_SERVICES", + "FABRICS_AND_SEWING", + "FAMILY_CLOTHING_STORES", + "FASHION_JEWELRY", + "FAST_FOOD_RESTAURANTS", + "FICTION_AND_NONFICTION", + "FINANCE_COMPANY", + "FINANCIAL_AND_INVESTMENT_ADVICE", + "FINANCIAL_INSTITUTIONS_MERCHANDISE_AND_SERVICES", + "FIREARM_ACCESSORIES", + "FIREARMS_WEAPONS_AND_KNIVES", + "FIREPLACE_AND_FIREPLACE_SCREENS", + "FIREWORKS", + "FISHING", + "FLORISTS", + "FLOWERS", + "FOOD_DRINK_AND_NUTRITION", + "FOOD_PRODUCTS", + "FOOD_RETAIL_AND_SERVICE", + "FRAGRANCES_AND_PERFUMES", + "FREEZER_AND_LOCKER_MEAT_PROVISIONERS", + "FUEL_DEALERS_FUEL_OIL_WOOD_AND_COAL", + "FUEL_DEALERS_NON_AUTOMOTIVE", + "FUNERAL_SERVICES_AND_CREMATORIES", + "FURNISHING_AND_DECORATING", + "FURNITURE", + "FURRIERS_AND_FUR_SHOPS", + "GADGETS_AND_OTHER_ELECTRONICS", + "GAMBLING", + "GAME_SOFTWARE", + "GAMES", + "GARDEN_SUPPLIES", + "GENERAL", + "GENERAL_CONTRACTORS", + "GENERAL_GOVERNMENT", + "GENERAL_SOFTWARE", + "GENERAL_TELECOM", + "GIFTS_AND_FLOWERS", + "GLASS_PAINT_AND_WALLPAPER_STORES", + "GLASSWARE_CRYSTAL_STORES", + "GOVERNMENT", + "GOVERNMENT_IDS_AND_LICENSES", + "GOVERNMENT_LICENSED_ON_LINE_CASINOS_ON_LINE_GAMBLING", + "GOVERNMENT_OWNED_LOTTERIES", + "GOVERNMENT_SERVICES", + "GRAPHIC_AND_COMMERCIAL_DESIGN", + "GREETING_CARDS", + "GROCERY_STORES_AND_SUPERMARKETS", + "HARDWARE_AND_TOOLS", + "HARDWARE_EQUIPMENT_AND_SUPPLIES", + "HAZARDOUS_RESTRICTED_AND_PERISHABLE_ITEMS", + "HEALTH_AND_BEAUTY_SPAS", + "HEALTH_AND_NUTRITION", + "HEALTH_AND_PERSONAL_CARE", + "HEARING_AIDS_SALES_AND_SUPPLIES", + "HEATING_PLUMBING_AC", + "HIGH_RISK_MERCHANT", + "HIRING_SERVICES", + "HOBBIES_TOYS_AND_GAMES", + "HOME_AND_GARDEN", + "HOME_AUDIO", + "HOME_DECOR", + "HOME_ELECTRONICS", + "HOSPITALS", + "HOTELS_MOTELS_INNS_RESORTS", + "HOUSEWARES", + "HUMAN_PARTS_AND_REMAINS", + "HUMOROUS_GIFTS_AND_NOVELTIES", + "HUNTING", + "IDS_LICENSES_AND_PASSPORTS", + "ILLEGAL_DRUGS_AND_PARAPHERNALIA", + "INDUSTRIAL", + "INDUSTRIAL_AND_MANUFACTURING_SUPPLIES", + "INSURANCE_AUTO_AND_HOME", + "INSURANCE_DIRECT", + "INSURANCE_LIFE_AND_ANNUITY", + "INSURANCE_SALES_UNDERWRITING", + "INSURANCE_UNDERWRITING_PREMIUMS", + "INTERNET_AND_NETWORK_SERVICES", + "INTRA_COMPANY_PURCHASES", + "LABORATORIES_DENTAL_MEDICAL", + "LANDSCAPING", + "LANDSCAPING_AND_HORTICULTURAL_SERVICES", + "LAUNDRY_CLEANING_SERVICES", + "LEGAL", + "LEGAL_SERVICES_AND_ATTORNEYS", + "LOCAL_DELIVERY_SERVICE", + "LOCKSMITH", + "LODGING_AND_ACCOMMODATIONS", + "LOTTERY_AND_CONTESTS", + "LUGGAGE_AND_LEATHER_GOODS", + "LUMBER_AND_BUILDING_MATERIALS", + "MAGAZINES", + "MAINTENANCE_AND_REPAIR_SERVICES", + "MAKEUP_AND_COSMETICS", + "MANUAL_CASH_DISBURSEMENTS", + "MASSAGE_PARLORS", + "MEDICAL", + "MEDICAL_AND_PHARMACEUTICAL", + "MEDICAL_CARE", + "MEDICAL_EQUIPMENT_AND_SUPPLIES", + "MEDICAL_SERVICES", + "MEETING_PLANNERS", + "MEMBERSHIP_CLUBS_AND_ORGANIZATIONS", + "MEMBERSHIP_COUNTRY_CLUBS_GOLF", + "MEMORABILIA", + "MEN_AND_BOY_CLOTHING_AND_ACCESSORY_STORES", + "MEN_CLOTHING", + "MERCHANDISE", + "METAPHYSICAL", + "MILITARIA", + "MILITARY_AND_CIVIL_SERVICE_UNIFORMS", + "MISC._AUTOMOTIVE_AIRCRAFT_AND_FARM_EQUIPMENT_DEALERS", + "MISC._GENERAL_MERCHANDISE", + "MISCELLANEOUS_GENERAL_SERVICES", + "MISCELLANEOUS_REPAIR_SHOPS_AND_RELATED_SERVICES", + "MODEL_KITS", + "MONEY_TRANSFER_MEMBER_FINANCIAL_INSTITUTION", + "MONEY_TRANSFER_MERCHANT", + "MOTION_PICTURE_THEATERS", + "MOTOR_FREIGHT_CARRIERS_AND_TRUCKING", + "MOTOR_HOME_AND_RECREATIONAL_VEHICLE_RENTAL", + "MOTOR_HOMES_DEALERS", + "MOTOR_VEHICLE_SUPPLIES_AND_NEW_PARTS", + "MOTORCYCLE_DEALERS", + "MOTORCYCLES", + "MOVIE", + "MOVIE_TICKETS", + "MOVING_AND_STORAGE", + "MULTI_LEVEL_MARKETING", + "MUSIC_CDS_CASSETTES_AND_ALBUMS", + "MUSIC_STORE_INSTRUMENTS_AND_SHEET_MUSIC", + "NETWORKING", + "NEW_AGE", + "NEW_PARTS_AND_SUPPLIES_MOTOR_VEHICLE", + "NEWS_DEALERS_AND_NEWSTANDS", + "NON_DURABLE_GOODS", + "NON_FICTION", + "NON_PROFIT_POLITICAL_AND_RELIGION", + "NONPROFIT", + "NOVELTIES", + "OEM_SOFTWARE", + "OFFICE_SUPPLIES_AND_EQUIPMENT", + "ONLINE_DATING", + "ONLINE_GAMING", + "ONLINE_GAMING_CURRENCY", + "ONLINE_SERVICES", + "OOUTBOUND_TELEMARKETING_MERCH", + "OPHTHALMOLOGISTS_OPTOMETRIST", + "OPTICIANS_AND_DISPENSING", + "ORTHOPEDIC_GOODS_PROSTHETICS", + "OSTEOPATHS", + "OTHER", + "PACKAGE_TOUR_OPERATORS", + "PAINTBALL", + "PAINTS_VARNISHES_AND_SUPPLIES", + "PARKING_LOTS_AND_GARAGES", + "PARTS_AND_ACCESSORIES", + "PAWN_SHOPS", + "PAYCHECK_LENDER_OR_CASH_ADVANCE", + "PERIPHERALS", + "PERSONALIZED_GIFTS", + "PET_SHOPS_PET_FOOD_AND_SUPPLIES", + "PETROLEUM_AND_PETROLEUM_PRODUCTS", + "PETS_AND_ANIMALS", + "PHOTOFINISHING_LABORATORIES_PHOTO_DEVELOPING", + "PHOTOGRAPHIC_STUDIOS_PORTRAITS", + "PHOTOGRAPHY", + "PHYSICAL_GOOD", + "PICTURE_VIDEO_PRODUCTION", + "PIECE_GOODS_NOTIONS_AND_OTHER_DRY_GOODS", + "PLANTS_AND_SEEDS", + "PLUMBING_AND_HEATING_EQUIPMENTS_AND_SUPPLIES", + "POLICE_RELATED_ITEMS", + "POLITICAL_ORGANIZATIONS", + "POSTAL_SERVICES_GOVERNMENT_ONLY", + "POSTERS", + "PREPAID_AND_STORED_VALUE_CARDS", + "PRESCRIPTION_DRUGS", + "PROMOTIONAL_ITEMS", + "PUBLIC_WAREHOUSING_AND_STORAGE", + "PUBLISHING_AND_PRINTING", + "PUBLISHING_SERVICES", + "RADAR_DECTORS", + "RADIO_TELEVISION_AND_STEREO_REPAIR", + "REAL_ESTATE", + "REAL_ESTATE_AGENT", + "REAL_ESTATE_AGENTS_AND_MANAGERS_RENTALS", + "RELIGION_AND_SPIRITUALITY_FOR_PROFIT", + "RELIGIOUS", + "RELIGIOUS_ORGANIZATIONS", + "REMITTANCE", + "RENTAL_PROPERTY_MANAGEMENT", + "RESIDENTIAL", + "RETAIL", + "RETAIL_FINE_JEWELRY_AND_WATCHES", + "REUPHOLSTERY_AND_FURNITURE_REPAIR", + "RINGS", + "ROOFING_SIDING_SHEET_METAL", + "RUGS_AND_CARPETS", + "SCHOOLS_AND_COLLEGES", + "SCIENCE_FICTION", + "SCRAPBOOKING", + "SCULPTURES", + "SECURITIES_BROKERS_AND_DEALERS", + "SECURITY_AND_SURVEILLANCE", + "SECURITY_AND_SURVEILLANCE_EQUIPMENT", + "SECURITY_BROKERS_AND_DEALERS", + "SEMINARS", + "SERVICE_STATIONS", + "SERVICES", + "SEWING_NEEDLEWORK_FABRIC_AND_PIECE_GOODS_STORES", + "SHIPPING_AND_PACKING", + "SHOE_REPAIR_HAT_CLEANING", + "SHOE_STORES", + "SHOES", + "SNOWMOBILE_DEALERS", + "SOFTWARE", + "SPECIALTY_AND_MISC._FOOD_STORES", + "SPECIALTY_CLEANING_POLISHING_AND_SANITATION_PREPARATIONS", + "SPECIALTY_OR_RARE_PETS", + "SPORT_GAMES_AND_TOYS", + "SPORTING_AND_RECREATIONAL_CAMPS", + "SPORTING_GOODS", + "SPORTS_AND_OUTDOORS", + "SPORTS_AND_RECREATION", + "STAMP_AND_COIN", + "STATIONARY_PRINTING_AND_WRITING_PAPER", + "STENOGRAPHIC_AND_SECRETARIAL_SUPPORT_SERVICES", + "STOCKS_BONDS_SECURITIES_AND_RELATED_CERTIFICATES", + "STORED_VALUE_CARDS", + "SUPPLIES", + "SUPPLIES_AND_TOYS", + "SURVEILLANCE_EQUIPMENT", + "SWIMMING_POOLS_AND_SPAS", + "SWIMMING_POOLS_SALES_SUPPLIES_SERVICES", + "TAILORS_AND_ALTERATIONS", + "TAX_PAYMENTS", + "TAX_PAYMENTS_GOVERNMENT_AGENCIES", + "TAXICABS_AND_LIMOUSINES", + "TELECOMMUNICATION_SERVICES", + "TELEPHONE_CARDS", + "TELEPHONE_EQUIPMENT", + "TELEPHONE_SERVICES", + "THEATER", + "TIRE_RETREADING_AND_REPAIR", + "TOLL_OR_BRIDGE_FEES", + "TOOLS_AND_EQUIPMENT", + "TOURIST_ATTRACTIONS_AND_EXHIBITS", + "TOWING_SERVICE", + "TOYS_AND_GAMES", + "TRADE_AND_VOCATIONAL_SCHOOLS", + "TRADEMARK_INFRINGEMENT", + "TRAILER_PARKS_AND_CAMPGROUNDS", + "TRAINING_SERVICES", + "TRANSPORTATION_SERVICES", + "TRAVEL", + "TRUCK_AND_UTILITY_TRAILER_RENTALS", + "TRUCK_STOP", + "TYPESETTING_PLATE_MAKING_AND_RELATED_SERVICES", + "USED_MERCHANDISE_AND_SECONDHAND_STORES", + "USED_PARTS_MOTOR_VEHICLE", + "UTILITIES", + "UTILITIES_ELECTRIC_GAS_WATER_SANITARY", + "VARIETY_STORES", + "VEHICLE_SALES", + "VEHICLE_SERVICE_AND_ACCESSORIES", + "VIDEO_EQUIPMENT", + "VIDEO_GAME_ARCADES_ESTABLISH", + "VIDEO_GAMES_AND_SYSTEMS", + "VIDEO_TAPE_RENTAL_STORES", + "VINTAGE_AND_COLLECTIBLE_VEHICLES", + "VINTAGE_AND_COLLECTIBLES", + "VITAMINS_AND_SUPPLEMENTS", + "VOCATIONAL_AND_TRADE_SCHOOLS", + "WATCH_CLOCK_AND_JEWELRY_REPAIR", + "WEB_HOSTING_AND_DESIGN", + "WELDING_REPAIR", + "WHOLESALE_CLUBS", + "WHOLESALE_FLORIST_SUPPLIERS", + "WHOLESALE_PRESCRIPTION_DRUGS", + "WILDLIFE_PRODUCTS", + "WIRE_TRANSFER", + "WIRE_TRANSFER_AND_MONEY_ORDER", + "WOMEN_ACCESSORY_SPECIALITY", + "WOMEN_CLOTHING", +]; + +let Fs = require("fs"); +let categoriesMap = categories.reduce(function (obj, name) { + obj[name] = name; + return obj; +}, {}); +Fs.mkdirSync(__dirname + "/../lib", { recursive: true }); +Fs.writeFileSync( + __dirname + "/../lib/categories.json", + JSON.stringify(categoriesMap, null, 2), + "utf8" +);