diff --git a/packages/nodes-base/credentials/PayPalApi.credentials.ts b/packages/nodes-base/credentials/PayPalApi.credentials.ts new file mode 100644 index 0000000000..96674b7370 --- /dev/null +++ b/packages/nodes-base/credentials/PayPalApi.credentials.ts @@ -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' + }, + ] + }, + ]; +} diff --git a/packages/nodes-base/nodes/ActiveCampaign/EcomOrderDescription.ts b/packages/nodes-base/nodes/ActiveCampaign/EcomOrderDescription.ts index 525fa0eb20..752b551ad1 100644 --- a/packages/nodes-base/nodes/ActiveCampaign/EcomOrderDescription.ts +++ b/packages/nodes-base/nodes/ActiveCampaign/EcomOrderDescription.ts @@ -713,4 +713,4 @@ export const ecomOrderFields = [ default: 100, description: 'How many results to return.', }, -] as INodeProperties[]; \ No newline at end of file +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/PayPal/GenericFunctions.ts b/packages/nodes-base/nodes/PayPal/GenericFunctions.ts new file mode 100644 index 0000000000..28dc39e01a --- /dev/null +++ b/packages/nodes-base/nodes/PayPal/GenericFunctions.ts @@ -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 { // 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 { // 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 { // 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; +} diff --git a/packages/nodes-base/nodes/PayPal/PayPal.node.ts b/packages/nodes-base/nodes/PayPal/PayPal.node.ts new file mode 100644 index 0000000000..9f664a46ee --- /dev/null +++ b/packages/nodes-base/nodes/PayPal/PayPal.node.ts @@ -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 { + 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)]; + } +} diff --git a/packages/nodes-base/nodes/PayPal/PaymentDescription.ts b/packages/nodes-base/nodes/PayPal/PaymentDescription.ts new file mode 100644 index 0000000000..13a74cea62 --- /dev/null +++ b/packages/nodes-base/nodes/PayPal/PaymentDescription.ts @@ -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
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
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
for a payout item completes. The subject line is the same for all
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.
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.
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[]; diff --git a/packages/nodes-base/nodes/PayPal/PaymentInteface.ts b/packages/nodes-base/nodes/PayPal/PaymentInteface.ts new file mode 100644 index 0000000000..f2d11710a8 --- /dev/null +++ b/packages/nodes-base/nodes/PayPal/PaymentInteface.ts @@ -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[]; +} diff --git a/packages/nodes-base/nodes/PayPal/paypal.png b/packages/nodes-base/nodes/PayPal/paypal.png new file mode 100644 index 0000000000..01891251ef Binary files /dev/null and b/packages/nodes-base/nodes/PayPal/paypal.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ae70262dc8..92a4f0e63d 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -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",