🔀 Merge branch 'RicardoE105-feature/paypal-node'

This commit is contained in:
Jan Oberhauser 2019-12-01 16:18:10 +01:00
commit 6214ce57ec
8 changed files with 797 additions and 1 deletions

View file

@ -0,0 +1,40 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class PayPalApi implements ICredentialType {
name = 'payPalApi';
displayName = 'PayPal API';
properties = [
{
displayName: 'Client ID',
name: 'clientId',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Secret',
name: 'secret',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Enviroment',
name: 'env',
type: 'options' as NodePropertyTypes,
default: 'live',
options: [
{
name: 'Sanbox',
value: 'sanbox'
},
{
name: 'Live',
value: 'live'
},
]
},
];
}

View file

@ -713,4 +713,4 @@ export const ecomOrderFields = [
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];
] as INodeProperties[];

View file

@ -0,0 +1,119 @@
import { OptionsWithUri } from 'request';
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IExecuteSingleFunctions,
BINARY_ENCODING
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function payPalApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('payPalApi');
const env = getEnviroment(credentials!.env as string);
const tokenInfo = await getAccessToken.call(this);
const headerWithAuthentication = Object.assign({ },
{ Authorization: `Bearer ${tokenInfo.access_token}`, 'Content-Type': 'application/json' });
const options = {
headers: headerWithAuthentication,
method,
qs: query || {},
uri: uri || `${env}/v1${endpoint}`,
body,
json: true
};
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = error.response.body.message || error.response.body.Message;
if (errorMessage !== undefined) {
throw errorMessage;
}
throw error.response.body;
}
}
function getEnviroment(env: string): string {
// @ts-ignore
return {
'sanbox': 'https://api.sandbox.paypal.com',
'live': 'https://api.paypal.com'
}[env];
}
async function getAccessToken(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('payPalApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const env = getEnviroment(credentials!.env as string);
const data = Buffer.from(`${credentials!.clientId}:${credentials!.secret}`).toString(BINARY_ENCODING);
const headerWithAuthentication = Object.assign({},
{ Authorization: `Basic ${data}`, 'Content-Type': 'application/x-www-form-urlencoded' });
const options: OptionsWithUri = {
headers: headerWithAuthentication,
method: 'POST',
form: {
grant_type: 'client_credentials',
},
uri: `${env}/v1/oauth2/token`,
json: true
};
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = error.response.body.message || error.response.body.Message;
if (errorMessage !== undefined) {
throw errorMessage;
}
throw error.response.body;
}
}
/**
* Make an API request to paginated paypal endpoint
* and return all results
*/
export async function payPalApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query!.page_size = 1000;
do {
responseData = await payPalApiRequest.call(this, endpoint, method, body, query, uri);
uri = getNext(responseData.links);
returnData.push.apply(returnData, responseData[propertyName]);
} while (
getNext(responseData.links) !== undefined
);
return returnData;
}
function getNext(links: IDataObject[]): string | undefined {
for (const link of links) {
if (link.rel === 'next') {
return link.href as string;
}
}
return undefined;
}
export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = '';
}
return result;
}

View file

@ -0,0 +1,178 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
INodeExecutionData,
INodeType,
} from 'n8n-workflow';
import {
payoutOperations,
payoutItemOperations,
payoutFields,
payoutItemFields,
} from './PaymentDescription';
import {
IPaymentBatch,
ISenderBatchHeader,
IItem, IAmount,
RecipientType,
RecipientWallet,
} from './PaymentInteface';
import {
validateJSON,
payPalApiRequest,
payPalApiRequestAllItems
} from './GenericFunctions';
export class PayPal implements INodeType {
description: INodeTypeDescription = {
displayName: 'PayPal',
name: 'payPal',
icon: 'file:paypal.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume PayPal API',
defaults: {
name: 'PayPal',
color: '#356ae6',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'payPalApi',
required: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Payout',
value: 'payout',
},
{
name: 'Payout Item',
value: 'payoutItem',
},
],
default: 'payout',
description: 'Resource to consume.',
},
// Payout
...payoutOperations,
...payoutItemOperations,
...payoutFields,
...payoutItemFields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let responseData;
const qs: IDataObject = {};
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'payout') {
if (operation === 'create') {
const body: IPaymentBatch = {};
const header: ISenderBatchHeader = {};
const jsonActive = this.getNodeParameter('jsonParameters', i) as boolean;
const senderBatchId = this.getNodeParameter('senderBatchId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
header.sender_batch_id = senderBatchId;
if (additionalFields.emailSubject) {
header.email_subject = additionalFields.emailSubject as string;
}
if (additionalFields.emailMessage) {
header.email_message = additionalFields.emailMessage as string;
}
if (additionalFields.note) {
header.note = additionalFields.note as string;
}
body.sender_batch_header = header;
if (!jsonActive) {
const payoutItems: IItem[] = [];
const itemsValues = (this.getNodeParameter('itemsUi', i) as IDataObject).itemsValues as IDataObject[];
if (itemsValues && itemsValues.length > 0) {
itemsValues.forEach(o => {
const payoutItem: IItem = {};
const amount: IAmount = {};
amount.currency = o.currency as string;
amount.value = parseFloat(o.amount as string);
payoutItem.amount = amount;
payoutItem.note = o.note as string || '';
payoutItem.receiver = o.receiverValue as string;
payoutItem.recipient_type = o.recipientType as RecipientType;
payoutItem.recipient_wallet = o.recipientWallet as RecipientWallet;
payoutItem.sender_item_id = o.senderItemId as string || '';
payoutItems.push(payoutItem);
});
body.items = payoutItems;
} else {
throw new Error('You must have at least one item.');
}
} else {
const itemsJson = validateJSON(this.getNodeParameter('itemsJson', i) as string);
body.items = itemsJson;
}
try {
responseData = await payPalApiRequest.call(this, '/payments/payouts', 'POST', body);
} catch (err) {
throw new Error(`PayPal Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'get') {
const payoutBatchId = this.getNodeParameter('payoutBatchId', i) as string;
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
try {
if (returnAll === true) {
responseData = await payPalApiRequestAllItems.call(this, 'items', `/payments/payouts/${payoutBatchId}`, 'GET', {}, qs);
} else {
qs.page_size = this.getNodeParameter('limit', i) as number;
responseData = await payPalApiRequest.call(this, `/payments/payouts/${payoutBatchId}`, 'GET', {}, qs);
responseData = responseData.items;
}
} catch (err) {
throw new Error(`PayPal Error: ${JSON.stringify(err)}`);
}
}
} else if (resource === 'payoutItem') {
if (operation === 'get') {
const payoutItemId = this.getNodeParameter('payoutItemId', i) as string;
try {
responseData = await payPalApiRequest.call(this,`/payments/payouts-item/${payoutItemId}`, 'GET', {}, qs);
} catch (err) {
throw new Error(`PayPal Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'cancel') {
const payoutItemId = this.getNodeParameter('payoutItemId', i) as string;
try {
responseData = await payPalApiRequest.call(this,`/payments/payouts-item/${payoutItemId}/cancel`, 'POST', {}, qs);
} catch (err) {
throw new Error(`PayPal Error: ${JSON.stringify(err)}`);
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,419 @@
import { INodeProperties } from "n8n-workflow";
export const payoutOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'payout',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a batch payout',
},
{
name: 'Get',
value: 'get',
description: 'Show batch payout details',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const payoutFields = [
/* -------------------------------------------------------------------------- */
/* payout:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Sender Batch ID',
name: 'senderBatchId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'create',
],
},
},
default: '',
description: 'A sender-specified ID number. Tracks the payout in an accounting system.',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
'payout'
],
operation: [
'create',
]
},
},
},
{
displayName: 'Items',
name: 'itemsUi',
placeholder: 'Add Item',
type: 'fixedCollection',
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'create',
],
jsonParameters: [
false
]
},
},
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'itemsValues',
displayName: 'Item',
values: [
{
displayName: 'Recipient Type',
name: 'recipientType',
type: 'options',
options: [
{
name: 'Phone',
value: 'phone',
description: 'The unencrypted phone number',
},
{
name: 'Email',
value: 'email',
description: 'The unencrypted email.',
},
{
name: 'PayPal ID',
value: 'paypalId',
description: 'The encrypted PayPal account number.',
},
],
default: 'email',
description: 'The ID type that identifies the recipient of the payment.',
},
{
displayName: 'Receiver Value',
name: 'receiverValue',
type: 'string',
required: true,
default: '',
description: 'The receiver of the payment. Corresponds to the recipient_type value<br />in the request. Max length: 127 characters.',
},
{
displayName: 'Currency',
name: 'currency',
type: 'options',
options: [
{
name: 'Australian dollar',
value: 'AUD'
},
{
name: 'Brazilian real',
value: 'BRL'
},
{
name: 'Canadian dollar',
value: 'CAD'
},
{
name: 'Czech koruna',
value: 'CZK'
},
{
name: 'Danish krone',
value: 'DKK'
},
{
name: 'Euro',
value: 'EUR'
},
{
name: 'United States dollar',
value: 'USD'
}
],
default: 'USD',
description: 'Currency',
},
{
displayName: 'Amount',
name: 'amount',
type: 'string',
required: true,
default: '',
description: 'The value, which might be',
},
{
displayName: 'Note',
name: 'note',
type: 'string',
required: false,
default: '',
description: 'The sender-specified note for notifications. Supports up to<br />4000 ASCII characters and 1000 non-ASCII characters.',
},
{
displayName: 'Sender Item ID',
name: 'senderItemId',
type: 'string',
default: '',
description: 'The sender-specified ID number. Tracks the payout in an accounting system.',
},
{
displayName: 'Recipient Wallet',
name: 'recipientWallet',
type: 'options',
options: [
{
name: 'PayPal',
value: 'paypal',
description: 'PayPal Wallet',
},
{
name: 'Venmo',
value: 'venmo',
description: 'Venmo Wallet',
},
],
default: 'paypal',
description: 'The recipient wallet',
},
]
},
],
},
{
displayName: 'Items',
name: 'itemsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
description: 'An array of individual payout items.',
displayOptions: {
show: {
resource: [
'payout'
],
operation: [
'create',
],
jsonParameters: [
true,
]
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Email Subject',
name: 'emailSubject',
type: 'string',
default: '',
description: 'The subject line for the email that PayPal sends when payment<br />for a payout item completes. The subject line is the same for all<br />recipients. Max length: 255 characters.',
},
{
displayName: 'Email Message',
name: 'emailMessage',
type: 'string',
default: '',
description: 'The email message that PayPal sends when the payout item completes.<br />The message is the same for all recipients.',
},
{
displayName: 'Note',
name: 'note',
type: 'string',
default: '',
description: 'The payouts and item-level notes are concatenated in the email.<br />Max length: 1000 characters.',
},
],
},
/* -------------------------------------------------------------------------- */
/* payout:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Payout Batch Id',
name: 'payoutBatchId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'get',
],
},
},
description: 'The ID of the payout for which to show details.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'get',
],
},
},
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
typeOptions: {
maxValue: 1000,
minValue: 1
},
default: 100,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'get',
],
returnAll: [
false,
],
},
},
description: 'If all results should be returned or only up to a given limit.',
},
] as INodeProperties[];
export const payoutItemOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'payoutItem',
],
},
},
options: [
{
name: 'Cancel',
value: 'cancel',
description: 'Cancels an unclaimed payout item',
},
{
name: 'Get',
value: 'get',
description: 'Show payout item details',
},
],
default: 'get',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const payoutItemFields = [
/* -------------------------------------------------------------------------- */
/* payoutItem:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Payout Item Id',
name: 'payoutItemId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'payoutItem',
],
operation: [
'get',
],
},
},
description: 'The ID of the payout item for which to show details.',
},
/* -------------------------------------------------------------------------- */
/* payoutItem:cancel */
/* -------------------------------------------------------------------------- */
{
displayName: 'Payout Item Id',
name: 'payoutItemId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'payoutItem',
],
operation: [
'cancel',
],
},
},
description: 'The ID of the payout item to cancel.',
},
] as INodeProperties[];

View file

@ -0,0 +1,38 @@
import { IDataObject } from "n8n-workflow";
export enum RecipientType {
email = 'EMAIL',
phone = 'PHONE',
paypalId = 'PAYPAL_ID',
}
export enum RecipientWallet {
paypal = 'PAYPAL',
venmo = 'VENMO',
}
export interface IAmount {
currency?: string;
value?: number;
}
export interface ISenderBatchHeader {
sender_batch_id?: string;
email_subject?: string;
email_message?: string;
note?: string;
}
export interface IItem {
recipient_type?: RecipientType;
amount?: IAmount;
note?: string;
receiver?: string;
sender_item_id?: string;
recipient_wallet?: RecipientWallet;
}
export interface IPaymentBatch {
sender_batch_header?: ISenderBatchHeader;
items?: IItem[];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -53,6 +53,7 @@
"dist/credentials/OpenWeatherMapApi.credentials.js",
"dist/credentials/PipedriveApi.credentials.js",
"dist/credentials/Postgres.credentials.js",
"dist/credentials/PayPalApi.credentials.js",
"dist/credentials/Redis.credentials.js",
"dist/credentials/RocketchatApi.credentials.js",
"dist/credentials/SlackApi.credentials.js",
@ -115,6 +116,7 @@
"dist/nodes/Pipedrive/Pipedrive.node.js",
"dist/nodes/Pipedrive/PipedriveTrigger.node.js",
"dist/nodes/Postgres/Postgres.node.js",
"dist/nodes/PayPal/PayPal.node.js",
"dist/nodes/Rocketchat/Rocketchat.node.js",
"dist/nodes/ReadBinaryFile.node.js",
"dist/nodes/ReadBinaryFiles.node.js",