From e1fa70d495fc92452eee10fea538b5f18eaeeb29 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 16 Oct 2021 22:07:59 -0600 Subject: [PATCH] WIP: Order.{createRequest,capture} --- README.md | 40 +++++++++++++++++++++ paypal-checkout.js | 64 +++++++++++++++++++++++++++++++++ tests/crud.js | 8 +++-- tests/order.js | 88 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 tests/order.js diff --git a/README.md b/README.md index 695e25a..9c7f253 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,11 @@ PPC.Subscriptions.createRequest({ ```txt PayPal.init(client_id, client_secret, 'sandbox|live', defaults); PayPal.request({ method, url, headers, json }); +``` +### Subscrptions (Recurring Payments) + +```txt // Webhook 'event_type': PayPal.Product.create({ ... }); // CATALOG.PRODUCT.CREATED @@ -66,11 +70,47 @@ PayPal.Subscription.details(id); PayPal.Subscription.cancel(id, { reason }); ``` +### Orders (One-Time Payments) + +```txt +PayPal.Order.createRequest({ ... }); // ?? +``` + +See also: + +- +- +- + # Redirects - `return_url` - `cancel_url` +### Orders + +#### `return_url` + +Order Request `return_url` will be called with the `token` query param as the +`order_id`: + +```txt +https://example.com/redirects/paypal-checkout/return + ?token=XXXXXXXXXXXXXXXXX + &PayerID=XXXXXXXXXXXXX +``` + +Again, `token` is the `order_id`. + +#### `cancel_url` + +The `cancel_url` will have the same query params as the `return_url`. + +Also, PayPal presents the raw `cancel_url` and will NOT update the order status. +It's up to you to confirm with the user and change the status to `CANCELLED`. + +### Subscriptions + #### `return_url` Subscription Request `return_url` will include the following: diff --git a/paypal-checkout.js b/paypal-checkout.js index 6d626ce..aba8fa7 100644 --- a/paypal-checkout.js +++ b/paypal-checkout.js @@ -114,6 +114,69 @@ function enumify(obj) { } */ +let Order = {}; +Order.intents = { + CAPTURE: "CAPTURE", + AUTHORIZE: "AUTHORIZE", +}; +// See +// https://developer.paypal.com/docs/api/orders/v2/#orders_create +// https://developer.paypal.com/docs/api/orders/v2/#definition-purchase_unit_request +// https://developer.paypal.com/docs/api/orders/v2/#definition-order_application_context +Order.createRequest = async function (order) { + if (!order.intent) { + order.intent = Order.intents.CAPTURE; + } + if (!order.purchase_units?.length) { + throw new Error("must have 'purchase_units'"); + } + + /* + { + intent: "CAPTURE", + purchase_units: [ + { + amount: { + currency_code: "USD", + value: "100.00", + }, + }, + ], + } + */ + return await PayPal.request({ + method: "POST", + url: `/v2/checkout/orders`, + json: order, + }) + .then(must201or200) + .then(justBody); +}; + +Order.details = async function (id) { + return await PayPal.request({ + url: `/v2/checkout/orders/${id}`, + json: true, + }) + .then(must201or200) // 200 + .then(justBody); +}; + +/** + * Captures (finalizes) an approved order. + * @param {String} id + * @param {any} body + */ +Order.capture = async function (id, { note_to_payer, final_capture }) { + return await PayPal.request({ + method: "POST", + url: `/v2/checkout/orders/${id}/capture`, + json: { note_to_payer, final_capture }, + }) + .then(must201or200) + .then(justBody); +}; + let Product = {}; // SaaS would be type=SERVICE, category=SOFTWARE @@ -447,6 +510,7 @@ Subscription.cancel = async function _showProductDetails(id, { reason }) { module.exports.init = PayPal.init; module.exports.request = PayPal.request; +module.exports.Order = Order; module.exports.Plan = Plan; module.exports.Product = Product; module.exports.Subscription = Subscription; diff --git a/tests/crud.js b/tests/crud.js index a869280..3982d4a 100644 --- a/tests/crud.js +++ b/tests/crud.js @@ -11,7 +11,7 @@ if (!process.env.PAYPAL_CLIENT_ID) { } let PayPal = require("../"); -let { Plan, Product, Subscription } = PayPal; +let { Plan, Product /*, Subscription*/ } = PayPal; async function test() { let products = await Product.list(); @@ -48,7 +48,11 @@ async function test() { } if (require.main === module) { - PayPal.init(process.env.PAYPAL_CLIENT_ID, process.env.PAYPAL_CLIENT_SECRET); + PayPal.init( + process.env.PAYPAL_CLIENT_ID, + process.env.PAYPAL_CLIENT_SECRET, + "sandbox" + ); test().catch(function (err) { console.error("Something bad happened:"); console.error(JSON.stringify(err, null, 2)); diff --git a/tests/order.js b/tests/order.js new file mode 100644 index 0000000..13595d1 --- /dev/null +++ b/tests/order.js @@ -0,0 +1,88 @@ +"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 { Order } = PayPal; + +async function test() { + let ppcOrder = await Order.createRequest({ + application_context: { + brand_name: "Bliss via The Root Group, LLC", + shipping_preference: "NO_SHIPPING", + // ("checkout with paypal") or "BILLING" (credit card) or NO_PREFERENCE + landing_page: "LOGIN", + user_action: "PAY_NOW", + return_url: `https://example.com/api/redirects/paypal-checkout/return`, + cancel_url: `https://example.com/api/redirects/paypal-checkout/cancel`, + }, + purchase_units: [ + { + request_id: 0, + custom_id: "xxxx", // Our own (User x Product) ID + description: "1 year of pure Bliss", // shown in PayPal Checkout Flow UI + soft_descriptor: "Bliss", // on the charge (credit card) statement + amount: { + currency_code: "USD", + value: "10.00", + }, + }, + ], + }); + + console.info(); + console.info("Order:"); + console.info(JSON.stringify(ppcOrder, null, 2)); + + // wait for user to click URL and accept + await new Promise(function (resolve) { + console.info(); + console.info("Please approve the order at the following URL:"); + console.info(); + console.info( + "Approve URL:", + ppcOrder.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 ppcCapture = await Order.capture(ppcOrder.id, { + note_to_payer: undefined, + final_order: true, + }); + + console.info(); + console.info("Capture:"); + console.info(JSON.stringify(ppcCapture, null, 2)); + + console.info(); +} + +if (require.main === module) { + PayPal.init( + process.env.PAYPAL_CLIENT_ID, + process.env.PAYPAL_CLIENT_SECRET, + "sandbox" + ); + test().catch(function (err) { + console.error("Something bad happened:"); + console.error(err); + console.error(JSON.stringify(err, null, 2)); + }); +}