From 08c95f989c0bea470c2f8738eb489c01ff1aa8c9 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 23 Nov 2019 19:36:47 -0500 Subject: [PATCH] :sparkles: added create a payout batch --- .../credentials/PaypalApi.credentials.ts | 20 +- .../ActiveCampaign/EcomOrderDescription.ts | 2 +- .../nodes/Paypal/GenericFunctions.ts | 110 ++++++ .../nodes/Paypal/PaymentDescription.ts | 340 ++++++++++++++++++ .../nodes/Paypal/PaymentInteface.ts | 38 ++ .../nodes-base/nodes/Paypal/Paypal.node.ts | 108 +++++- packages/nodes-base/package.json | 4 +- 7 files changed, 611 insertions(+), 11 deletions(-) create mode 100644 packages/nodes-base/nodes/Paypal/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Paypal/PaymentDescription.ts create mode 100644 packages/nodes-base/nodes/Paypal/PaymentInteface.ts diff --git a/packages/nodes-base/credentials/PaypalApi.credentials.ts b/packages/nodes-base/credentials/PaypalApi.credentials.ts index adfa66f5f9..ae04c5ef87 100644 --- a/packages/nodes-base/credentials/PaypalApi.credentials.ts +++ b/packages/nodes-base/credentials/PaypalApi.credentials.ts @@ -4,9 +4,9 @@ import { } from 'n8n-workflow'; -export class PaypalApi implements ICredentialType { +export class PayPalApi implements ICredentialType { name = 'paypalApi'; - displayName = 'Paypal API'; + displayName = 'PayPal API'; properties = [ { displayName: 'Client ID', @@ -20,5 +20,21 @@ export class PaypalApi implements ICredentialType { 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..ef17b3ae07 --- /dev/null +++ b/packages/nodes-base/nodes/Paypal/GenericFunctions.ts @@ -0,0 +1,110 @@ +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'); + let tokenInfo; + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + // @ts-ignore + const env = { + 'sanbox': 'https://api.sandbox.paypal.com', + 'live': 'https://api.paypal.com' + }[credentials.env as string]; + + const data = new Buffer(`${credentials.clientId}:${credentials.secret}`).toString(BINARY_ENCODING); + let headerWithAuthentication = Object.assign({}, + { Authorization: `Basic ${data}`, 'Content-Type': 'application/x-www-form-urlencoded' }); + let options: OptionsWithUri = { + headers: headerWithAuthentication, + method, + qs: query, + uri: `${env}/v1/oauth2/token`, + body, + json: true + }; + try { + tokenInfo = 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; + } + headerWithAuthentication = Object.assign({ }, + { Authorization: `Bearer ${tokenInfo.access_token}`, 'Content-Type': 'application/json' }); + + 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; + } +} + + + +/** + * Make an API request to paginated intercom endpoint + * and return all results + */ +export async function intercomApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.per_page = 60; + + let uri: string | undefined; + + do { + responseData = await paypalApiRequest.call(this, endpoint, method, body, query, uri); + uri = responseData.pages.next; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.pages !== undefined && + responseData.pages.next !== undefined && + responseData.pages.next !== null + ); + + return returnData; +} + + +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/PaymentDescription.ts b/packages/nodes-base/nodes/Paypal/PaymentDescription.ts new file mode 100644 index 0000000000..c8cef397db --- /dev/null +++ b/packages/nodes-base/nodes/Paypal/PaymentDescription.ts @@ -0,0 +1,340 @@ +import { INodeProperties } from "n8n-workflow"; + +export const payoutOpeations = [ + { + 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 payout batch 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. Value is a string of up to 127 single-byte characters.', + }, + { + 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 value of up to 127 single-byte 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. Value is an alphanumeric string of up to 255 single-byte 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. The maximum combined length of the notes is 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', + required: 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[]; diff --git a/packages/nodes-base/nodes/Paypal/PaymentInteface.ts b/packages/nodes-base/nodes/Paypal/PaymentInteface.ts new file mode 100644 index 0000000000..07db0ebd2e --- /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?: string; +} + +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.node.ts b/packages/nodes-base/nodes/Paypal/Paypal.node.ts index b8a14e1224..fa29233f48 100644 --- a/packages/nodes-base/nodes/Paypal/Paypal.node.ts +++ b/packages/nodes-base/nodes/Paypal/Paypal.node.ts @@ -8,18 +8,33 @@ import { INodeExecutionData, INodeType, } from 'n8n-workflow'; +import { + payoutOpeations, + payoutFields, +} from './PaymentDescription'; +import { + IPaymentBatch, + ISenderBatchHeader, + IItem, IAmount, + RecipientType, + RecipientWallet, + } from './PaymentInteface'; +import { + validateJSON, + paypalApiRequest, + } from './GenericFunctions'; -export class Paypal implements INodeType { +export class PayPal implements INodeType { description: INodeTypeDescription = { - displayName: 'Paypal', - name: 'Paypal', + displayName: 'PayPal', + name: 'paypal', icon: 'file:paypal.png', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Paypal API', + description: 'Consume PayPal API', defaults: { - name: 'Paypal', + name: 'PayPal', color: '#356ae6', }, inputs: ['main'], @@ -30,10 +45,91 @@ export class Paypal implements INodeType { required: true, } ], - properties: [], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Payout', + value: 'payout', + description: 'Use the Payouts API to make payments to multiple PayPal or Venmo recipients. The Payouts API is a fast, convenient way to send commissions, rebates, rewards, and general disbursements. You can send up to 15,000 payments per call. If you integrated the Payouts API before September 1, 2017, you receive transaction reports through Mass Payments Reporting.', + }, + ], + default: 'payout', + description: 'Resource to consume.', + }, + ...payoutOpeations, + ...payoutFields, + ], }; async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let qs: IDataObject; + let responseData; + for (let i = 0; i < length; i++) { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + 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 = o.receiverValue as string; + payoutItem.amount = amount; + payoutItem.note = o.note as string || ''; + payoutItem.receiver = o.receiver 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, '/payouts', 'POST', body); + } 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({})]; } } diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 4f759d729b..da41902dde 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -50,7 +50,7 @@ "dist/credentials/OpenWeatherMapApi.credentials.js", "dist/credentials/PipedriveApi.credentials.js", "dist/credentials/Postgres.credentials.js", - "dist/credentials/Paypal.credentials.js", + "dist/credentials/PayPalApi.credentials.js", "dist/credentials/Redis.credentials.js", "dist/credentials/RocketchatApi.credentials.js", "dist/credentials/SlackApi.credentials.js", @@ -110,7 +110,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/PayPal/PayPal.node.js", "dist/nodes/Rocketchat/Rocketchat.node.js", "dist/nodes/ReadBinaryFile.node.js", "dist/nodes/ReadBinaryFiles.node.js",