feature: add Order.{createRequest,details,capture}

This commit is contained in:
AJ ONeal 2021-10-16 23:11:47 -06:00
parent 19cfcabc27
commit 5fe43c4a48
4 changed files with 367 additions and 8 deletions

215
README.md
View File

@ -46,7 +46,11 @@ PPC.Subscriptions.createRequest({
```txt ```txt
PayPal.init(client_id, client_secret, 'sandbox|live', defaults); PayPal.init(client_id, client_secret, 'sandbox|live', defaults);
PayPal.request({ method, url, headers, json }); PayPal.request({ method, url, headers, json });
```
### Subscrptions (Recurring Payments)
```txt
// Webhook 'event_type': // Webhook 'event_type':
PayPal.Product.create({ ... }); // CATALOG.PRODUCT.CREATED PayPal.Product.create({ ... }); // CATALOG.PRODUCT.CREATED
@ -66,6 +70,21 @@ PayPal.Subscription.details(id);
PayPal.Subscription.cancel(id, { reason }); PayPal.Subscription.cancel(id, { reason });
``` ```
### Orders (One-Time Payments)
```txt
// Webhook 'event_type':
PayPal.Order.createRequest({ purchase_units }); // CHECKOUT.ORDER.APPROVED
PayPal.Order.capture(id, { final_capture }); // PAYMENT.CAPTURE.COMPLETED
```
See also:
- <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>
# Redirects # Redirects
- `return_url` - `return_url`
@ -73,22 +92,41 @@ PayPal.Subscription.cancel(id, { reason });
#### `return_url` #### `return_url`
Subscription Request `return_url` will include the following: **_Order_** and **_Subscription_** requests have a return `return_url` will be
called with some or all of the following params:
```txt ```txt
# Order
https://example.com/redirects/paypal-checkout/return
?token=XXXXXXXXXXXXXXXXX
&PayerID=XXXXXXXXXXXXX
```
- `token` is the **_Order ID_**
- `PayerID` is... exactly what it seems (no idea how you can access the Payer
object though)
```txt
# Subscrption
https://example.com/redirects/paypal-checkout/return https://example.com/redirects/paypal-checkout/return
?subscription_id=XXXXXXXXXXXXXX ?subscription_id=XXXXXXXXXXXXXX
&ba_token=BA-00000000000000000 &ba_token=BA-00000000000000000
&token=XXXXXXXXXXXXXXXXX &token=XXXXXXXXXXXXXXXXX
``` ```
- `subscription_id` refers to both the **_Subscription ID_** and the
`billing_agreement_id` of the corresponding **_Payments_**.
- `ba_token` (deprecated) refers to `/v1/payments/billing-agreements/:ba_token`
- `token` refers to the **_Order ID_** (perhaps created as part of the setup fee
or first billing cycle payment).
#### `cancel_url` #### `cancel_url`
The `cancel_url` will have the same query params as the `return_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 subscription Also, PayPal presents the raw `cancel_url` and will NOT update the order or
status. It's up to you to confirm with the user and change the status to subscription status. It's up to you to confirm with the user and change the
`CANCELLED`. status to `CANCELLED`.
# Webhooks # Webhooks
@ -117,6 +155,10 @@ See:
# Notes # Notes
My discussions with Twitter Support (@paypaldev):
- <https://twitter.com/search?q=(from%3Acoolaj86)%20(to%3Apaypaldev)&src=typed_query>
Note: Just about everything in the PayPal SDK that uses `ALL_CAPS` is a 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 `constant`/`enum` representing an option you can pick from limited number of
options. options.
@ -124,7 +166,7 @@ options.
Sandbox accounts (for creating fake purchases) can be managed at: Sandbox accounts (for creating fake purchases) can be managed at:
<https://developer.paypal.com/developer/accounts> <https://developer.paypal.com/developer/accounts>
Note on Auth + Capture: ## Auth vs Capture
> Authorization and capture enables you to authorize fund availability but delay > Authorization and capture enables you to authorize fund availability but delay
> fund capture. This can be useful for merchants who have a delayed order > fund capture. This can be useful for merchants who have a delayed order
@ -141,9 +183,170 @@ Note on Auth + Capture:
> - <https://developer.paypal.com/docs/admin/auth-capture/> > - <https://developer.paypal.com/docs/admin/auth-capture/>
> - <https://developer.paypal.com/docs/api/payments/v2/#authorizations_capture> > - <https://developer.paypal.com/docs/api/payments/v2/#authorizations_capture>
Buttons: You can auth once and capture multiple times (unless you set `final_capture`).
## PayPal Checkout Buttons
- <https://www.paypal.com/webapps/mpp/logos-buttons> <== THE ONE YOU WANT - <https://www.paypal.com/webapps/mpp/logos-buttons> <== THE ONE YOU WANT
- <img src="https://www.paypalobjects.com/webstatic/en_US/i/buttons/checkout-logo-large.png" alt="Check out with PayPal" /> - <img src="https://www.paypalobjects.com/webstatic/en_US/i/buttons/checkout-logo-large.png" alt="Check out with PayPal" />
- <https://developer.paypal.com/docs/checkout/> - <https://developer.paypal.com/docs/checkout/>
- <https://www.paypal.com/buttons/> - <https://www.paypal.com/buttons/>
# Glossary
## Webhook Event: CHECKOUT.ORDER.APPROVED
```json
{
"id": "WH-1V203642KU442722T-3S346483MF8733038",
"event_version": "1.0",
"create_time": "2021-10-17T05:04:22.404Z",
"resource_type": "checkout-order",
"resource_version": "2.0",
"event_type": "CHECKOUT.ORDER.APPROVED",
"summary": "An order has been approved by buyer",
"resource": {
"create_time": "2021-10-17T05:03:26Z",
"purchase_units": [
{
"reference_id": "{purchase-unit-id}",
"amount": {
"currency_code": "USD",
"value": "10.00"
},
"payee": {
"email_address": "sb-a9xvi8075587@business.example.com",
"merchant_id": "4RXRQC77UD53U",
"display_data": {
"brand_name": "Bliss via The Root Group, LLC"
}
},
"description": "1 year of pure Bliss",
"custom_id": "{my-local-db-purchase-id}",
"soft_descriptor": "Bliss"
}
],
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/4K5112848U951142F",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/4K5112848U951142F",
"rel": "update",
"method": "PATCH"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/4K5112848U951142F/capture",
"rel": "capture",
"method": "POST"
}
],
"id": "4K5112848U951142F",
"intent": "CAPTURE",
"payer": {
"name": {
"given_name": "John",
"surname": "Doe"
},
"email_address": "sb-ka5d18075586@personal.example.com",
"payer_id": "YTENGYR8PAF9A",
"address": {
"country_code": "US"
}
},
"status": "APPROVED"
},
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1V203642KU442722T-3S346483MF8733038",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1V203642KU442722T-3S346483MF8733038/resend",
"rel": "resend",
"method": "POST"
}
]
}
```
## Webhook Event: PAYMENT.CAPTURE.COMPLETED
```json
{
"id": "WH-3UT90572MR669760L-7LL94124G5389840D",
"event_version": "1.0",
"create_time": "2021-10-17T05:05:03.389Z",
"resource_type": "capture",
"resource_version": "2.0",
"event_type": "PAYMENT.CAPTURE.COMPLETED",
"summary": "Payment completed for $ 10.0 USD",
"resource": {
"amount": {
"value": "10.00",
"currency_code": "USD"
},
"seller_protection": {
"dispute_categories": ["ITEM_NOT_RECEIVED", "UNAUTHORIZED_TRANSACTION"],
"status": "ELIGIBLE"
},
"supplementary_data": {
"related_ids": {
"order_id": "4K5112848U951142F"
}
},
"update_time": "2021-10-17T05:04:29Z",
"create_time": "2021-10-17T05:04:29Z",
"final_capture": true,
"seller_receivable_breakdown": {
"paypal_fee": {
"value": "0.84",
"currency_code": "USD"
},
"gross_amount": {
"value": "10.00",
"currency_code": "USD"
},
"net_amount": {
"value": "9.16",
"currency_code": "USD"
}
},
"custom_id": "{my-local-db-purchase-id}",
"links": [
{
"method": "GET",
"rel": "self",
"href": "https://api.sandbox.paypal.com/v2/payments/captures/5VK462069F664902F"
},
{
"method": "POST",
"rel": "refund",
"href": "https://api.sandbox.paypal.com/v2/payments/captures/5VK462069F664902F/refund"
},
{
"method": "GET",
"rel": "up",
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/4K5112848U951142F"
}
],
"id": "5VK462069F664902F",
"status": "COMPLETED"
},
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-3UT90572MR669760L-7LL94124G5389840D",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-3UT90572MR669760L-7LL94124G5389840D/resend",
"rel": "resend",
"method": "POST"
}
]
}
```

View File

@ -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 = {}; let Product = {};
// SaaS would be type=SERVICE, category=SOFTWARE // 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.init = PayPal.init;
module.exports.request = PayPal.request; module.exports.request = PayPal.request;
module.exports.Order = Order;
module.exports.Plan = Plan; module.exports.Plan = Plan;
module.exports.Product = Product; module.exports.Product = Product;
module.exports.Subscription = Subscription; module.exports.Subscription = Subscription;

View File

@ -11,7 +11,7 @@ if (!process.env.PAYPAL_CLIENT_ID) {
} }
let PayPal = require("../"); let PayPal = require("../");
let { Plan, Product, Subscription } = PayPal; let { Plan, Product /*, Subscription*/ } = PayPal;
async function test() { async function test() {
let products = await Product.list(); let products = await Product.list();
@ -48,7 +48,11 @@ async function test() {
} }
if (require.main === module) { 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) { test().catch(function (err) {
console.error("Something bad happened:"); console.error("Something bad happened:");
console.error(JSON.stringify(err, null, 2)); console.error(JSON.stringify(err, null, 2));

88
tests/order.js Normal file
View File

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