commit 69ed827e3cd728c0093070c9569d2793d481191d Author: AJ ONeal Date: Sat Oct 9 17:10:40 2021 -0600 initial commit 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" +);