mirror of
https://github.com/therootcompany/paypal-checkout.js.git
synced 2025-05-13 09:36:37 +00:00
feature: add Order.{createRequest,details,capture}
This commit is contained in:
parent
19cfcabc27
commit
5fe43c4a48
215
README.md
215
README.md
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -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;
|
||||||
|
@ -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
88
tests/order.js
Normal 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));
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user