first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
All Stripe webhook events are taken from docs:
|
||||
[https://stripe.com/docs/api/events/types#event_types](https://stripe.com/docs/api/events/types#event_types)
|
||||
|
||||
To get the entire list of events as a JS array, scrape the website:
|
||||
|
||||
1. manually add the id #event-types to `<ul>` that contains all event types
|
||||
2. copy-paste the function in the JS console
|
||||
3. the result is copied into in the clipboard
|
||||
4. paste the prepared array in StripeTrigger.node.ts
|
||||
|
||||
```js
|
||||
types = [];
|
||||
$$('ul#event-types li').forEach((el) => {
|
||||
const value = el.querySelector('.method-list-item-label-name').innerText;
|
||||
|
||||
types.push({
|
||||
name: value
|
||||
.replace(/(\.|_)/, ' ')
|
||||
.split(' ')
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.substring(1))
|
||||
.join(' '),
|
||||
value,
|
||||
description: el.querySelector('.method-list-item-description').innerText,
|
||||
});
|
||||
});
|
||||
copy(types);
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.stripe",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Finance & Accounting", "Sales"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/stripe/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.stripe/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
balanceOperations,
|
||||
chargeFields,
|
||||
chargeOperations,
|
||||
couponFields,
|
||||
couponOperations,
|
||||
customerCardFields,
|
||||
customerCardOperations,
|
||||
customerFields,
|
||||
customerOperations,
|
||||
meterEventFields,
|
||||
meterEventOperations,
|
||||
sourceFields,
|
||||
sourceOperations,
|
||||
tokenFields,
|
||||
tokenOperations,
|
||||
} from './descriptions';
|
||||
import {
|
||||
adjustChargeFields,
|
||||
adjustCustomerFields,
|
||||
adjustMetadata,
|
||||
handleListing,
|
||||
loadResource,
|
||||
stripeApiRequest,
|
||||
} from './helpers';
|
||||
|
||||
export class Stripe implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Stripe',
|
||||
name: 'stripe',
|
||||
icon: 'file:stripe.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume the Stripe API',
|
||||
defaults: {
|
||||
name: 'Stripe',
|
||||
},
|
||||
usableAsTool: true,
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'stripeApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Balance',
|
||||
value: 'balance',
|
||||
},
|
||||
{
|
||||
name: 'Charge',
|
||||
value: 'charge',
|
||||
},
|
||||
{
|
||||
name: 'Coupon',
|
||||
value: 'coupon',
|
||||
},
|
||||
{
|
||||
name: 'Customer',
|
||||
value: 'customer',
|
||||
},
|
||||
{
|
||||
name: 'Customer Card',
|
||||
value: 'customerCard',
|
||||
},
|
||||
{
|
||||
name: 'Meter Event',
|
||||
value: 'meterEvent',
|
||||
},
|
||||
{
|
||||
name: 'Source',
|
||||
value: 'source',
|
||||
},
|
||||
{
|
||||
name: 'Token',
|
||||
value: 'token',
|
||||
},
|
||||
],
|
||||
default: 'balance',
|
||||
},
|
||||
...balanceOperations,
|
||||
...customerCardOperations,
|
||||
...customerCardFields,
|
||||
...chargeOperations,
|
||||
...chargeFields,
|
||||
...couponOperations,
|
||||
...couponFields,
|
||||
...customerOperations,
|
||||
...customerFields,
|
||||
...meterEventOperations,
|
||||
...meterEventFields,
|
||||
...sourceOperations,
|
||||
...sourceFields,
|
||||
...tokenOperations,
|
||||
...tokenFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
async getCustomers(this: ILoadOptionsFunctions) {
|
||||
return await loadResource.call(this, 'customer');
|
||||
},
|
||||
async getCurrencies(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { data } = await stripeApiRequest.call(this, 'GET', '/country_specs', {});
|
||||
for (const currency of data[0].supported_payment_currencies) {
|
||||
returnData.push({
|
||||
name: currency.toUpperCase(),
|
||||
value: currency,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
|
||||
let responseData;
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
if (resource === 'balance') {
|
||||
// *********************************************************************
|
||||
// balance
|
||||
// *********************************************************************
|
||||
|
||||
// https://stripe.com/docs/api/balance
|
||||
|
||||
if (operation === 'get') {
|
||||
// ----------------------------------
|
||||
// balance: get
|
||||
// ----------------------------------
|
||||
|
||||
responseData = await stripeApiRequest.call(this, 'GET', '/balance', {}, {});
|
||||
}
|
||||
} else if (resource === 'customerCard') {
|
||||
// *********************************************************************
|
||||
// customer card
|
||||
// *********************************************************************
|
||||
|
||||
// https://stripe.com/docs/api/cards
|
||||
|
||||
if (operation === 'add') {
|
||||
// ----------------------------------
|
||||
// customerCard: add
|
||||
// ----------------------------------
|
||||
|
||||
const body = {
|
||||
source: this.getNodeParameter('token', i),
|
||||
} as IDataObject;
|
||||
|
||||
const customerId = this.getNodeParameter('customerId', i);
|
||||
const endpoint = `/customers/${customerId}/sources`;
|
||||
responseData = await stripeApiRequest.call(this, 'POST', endpoint, body, {});
|
||||
} else if (operation === 'remove') {
|
||||
// ----------------------------------
|
||||
// customerCard: remove
|
||||
// ----------------------------------
|
||||
|
||||
const customerId = this.getNodeParameter('customerId', i);
|
||||
const cardId = this.getNodeParameter('cardId', i);
|
||||
const endpoint = `/customers/${customerId}/sources/${cardId}`;
|
||||
responseData = await stripeApiRequest.call(this, 'DELETE', endpoint, {}, {});
|
||||
} else if (operation === 'get') {
|
||||
// ----------------------------------
|
||||
// customerCard: get
|
||||
// ----------------------------------
|
||||
|
||||
const customerId = this.getNodeParameter('customerId', i);
|
||||
const sourceId = this.getNodeParameter('sourceId', i);
|
||||
const endpoint = `/customers/${customerId}/sources/${sourceId}`;
|
||||
responseData = await stripeApiRequest.call(this, 'GET', endpoint, {}, {});
|
||||
}
|
||||
} else if (resource === 'charge') {
|
||||
// *********************************************************************
|
||||
// charge
|
||||
// *********************************************************************
|
||||
|
||||
// https://stripe.com/docs/api/charges
|
||||
|
||||
if (operation === 'create') {
|
||||
// ----------------------------------
|
||||
// charge: create
|
||||
// ----------------------------------
|
||||
|
||||
const body = {
|
||||
customer: this.getNodeParameter('customerId', i),
|
||||
currency: (this.getNodeParameter('currency', i) as string).toLowerCase(),
|
||||
amount: this.getNodeParameter('amount', i),
|
||||
source: this.getNodeParameter('source', i),
|
||||
} as IDataObject;
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
|
||||
if (!isEmpty(additionalFields)) {
|
||||
Object.assign(body, adjustChargeFields(additionalFields));
|
||||
}
|
||||
|
||||
responseData = await stripeApiRequest.call(this, 'POST', '/charges', body, {});
|
||||
} else if (operation === 'get') {
|
||||
// ----------------------------------
|
||||
// charge: get
|
||||
// ----------------------------------
|
||||
|
||||
const chargeId = this.getNodeParameter('chargeId', i);
|
||||
responseData = await stripeApiRequest.call(this, 'GET', `/charges/${chargeId}`, {}, {});
|
||||
} else if (operation === 'getAll') {
|
||||
// ----------------------------------
|
||||
// charge: getAll
|
||||
// ----------------------------------
|
||||
|
||||
responseData = await handleListing.call(this, resource, i);
|
||||
} else if (operation === 'update') {
|
||||
// ----------------------------------
|
||||
// charge: update
|
||||
// ----------------------------------
|
||||
|
||||
const body = {} as IDataObject;
|
||||
|
||||
const updateFields = this.getNodeParameter('updateFields', i);
|
||||
|
||||
if (isEmpty(updateFields)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`Please enter at least one field to update for the ${resource}.`,
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(body, adjustChargeFields(updateFields));
|
||||
|
||||
const chargeId = this.getNodeParameter('chargeId', i);
|
||||
responseData = await stripeApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/charges/${chargeId}`,
|
||||
body,
|
||||
{},
|
||||
);
|
||||
}
|
||||
} else if (resource === 'coupon') {
|
||||
// *********************************************************************
|
||||
// coupon
|
||||
// *********************************************************************
|
||||
|
||||
// https://stripe.com/docs/api/coupons
|
||||
|
||||
if (operation === 'create') {
|
||||
// ----------------------------------
|
||||
// coupon: create
|
||||
// ----------------------------------
|
||||
|
||||
const body = {
|
||||
duration: this.getNodeParameter('duration', i),
|
||||
} as IDataObject;
|
||||
|
||||
const type = this.getNodeParameter('type', i);
|
||||
|
||||
if (type === 'fixedAmount') {
|
||||
body.amount_off = this.getNodeParameter('amountOff', i);
|
||||
body.currency = this.getNodeParameter('currency', i);
|
||||
} else {
|
||||
body.percent_off = this.getNodeParameter('percentOff', i);
|
||||
}
|
||||
|
||||
responseData = await stripeApiRequest.call(this, 'POST', '/coupons', body, {});
|
||||
} else if (operation === 'getAll') {
|
||||
// ----------------------------------
|
||||
// coupon: getAll
|
||||
// ----------------------------------
|
||||
|
||||
responseData = await handleListing.call(this, resource, i);
|
||||
}
|
||||
} else if (resource === 'customer') {
|
||||
// *********************************************************************
|
||||
// customer
|
||||
// *********************************************************************
|
||||
|
||||
// https://stripe.com/docs/api/customers
|
||||
|
||||
if (operation === 'create') {
|
||||
// ----------------------------------
|
||||
// customer: create
|
||||
// ----------------------------------
|
||||
|
||||
const body = {
|
||||
name: this.getNodeParameter('name', i),
|
||||
} as IDataObject;
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
|
||||
if (!isEmpty(additionalFields)) {
|
||||
Object.assign(body, adjustCustomerFields(additionalFields));
|
||||
}
|
||||
|
||||
responseData = await stripeApiRequest.call(this, 'POST', '/customers', body, {});
|
||||
} else if (operation === 'delete') {
|
||||
// ----------------------------------
|
||||
// customer: delete
|
||||
// ----------------------------------
|
||||
|
||||
const customerId = this.getNodeParameter('customerId', i);
|
||||
responseData = await stripeApiRequest.call(
|
||||
this,
|
||||
'DELETE',
|
||||
`/customers/${customerId}`,
|
||||
{},
|
||||
{},
|
||||
);
|
||||
} else if (operation === 'get') {
|
||||
// ----------------------------------
|
||||
// customer: get
|
||||
// ----------------------------------
|
||||
|
||||
const customerId = this.getNodeParameter('customerId', i);
|
||||
responseData = await stripeApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/customers/${customerId}`,
|
||||
{},
|
||||
{},
|
||||
);
|
||||
} else if (operation === 'getAll') {
|
||||
// ----------------------------------
|
||||
// customer: getAll
|
||||
// ----------------------------------
|
||||
|
||||
const qs = {} as IDataObject;
|
||||
const filters = this.getNodeParameter('filters', i);
|
||||
|
||||
if (!isEmpty(filters)) {
|
||||
qs.email = filters.email;
|
||||
}
|
||||
|
||||
responseData = await handleListing.call(this, resource, i, qs);
|
||||
} else if (operation === 'update') {
|
||||
// ----------------------------------
|
||||
// customer: update
|
||||
// ----------------------------------
|
||||
|
||||
const body = {} as IDataObject;
|
||||
|
||||
const updateFields = this.getNodeParameter('updateFields', i);
|
||||
|
||||
if (isEmpty(updateFields)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`Please enter at least one field to update for the ${resource}.`,
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(body, adjustCustomerFields(updateFields));
|
||||
|
||||
const customerId = this.getNodeParameter('customerId', i);
|
||||
responseData = await stripeApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/customers/${customerId}`,
|
||||
body,
|
||||
{},
|
||||
);
|
||||
}
|
||||
} else if (resource === 'source') {
|
||||
// *********************************************************************
|
||||
// source
|
||||
// *********************************************************************
|
||||
|
||||
// https://stripe.com/docs/api/sources
|
||||
|
||||
if (operation === 'create') {
|
||||
// ----------------------------------
|
||||
// source: create
|
||||
// ----------------------------------
|
||||
|
||||
const customerId = this.getNodeParameter('customerId', i);
|
||||
|
||||
const body = {
|
||||
type: this.getNodeParameter('type', i),
|
||||
amount: this.getNodeParameter('amount', i),
|
||||
currency: this.getNodeParameter('currency', i),
|
||||
} as IDataObject;
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
|
||||
if (!isEmpty(additionalFields)) {
|
||||
Object.assign(body, adjustMetadata(additionalFields));
|
||||
}
|
||||
|
||||
responseData = await stripeApiRequest.call(this, 'POST', '/sources', body, {});
|
||||
|
||||
// attach source to customer
|
||||
const endpoint = `/customers/${customerId}/sources`;
|
||||
await stripeApiRequest.call(this, 'POST', endpoint, { source: responseData.id }, {});
|
||||
} else if (operation === 'delete') {
|
||||
// ----------------------------------
|
||||
// source: delete
|
||||
// ----------------------------------
|
||||
|
||||
const sourceId = this.getNodeParameter('sourceId', i);
|
||||
const customerId = this.getNodeParameter('customerId', i);
|
||||
const endpoint = `/customers/${customerId}/sources/${sourceId}`;
|
||||
responseData = await stripeApiRequest.call(this, 'DELETE', endpoint, {}, {});
|
||||
} else if (operation === 'get') {
|
||||
// ----------------------------------
|
||||
// source: get
|
||||
// ----------------------------------
|
||||
|
||||
const sourceId = this.getNodeParameter('sourceId', i);
|
||||
responseData = await stripeApiRequest.call(this, 'GET', `/sources/${sourceId}`, {}, {});
|
||||
}
|
||||
} else if (resource === 'token') {
|
||||
// *********************************************************************
|
||||
// token
|
||||
// *********************************************************************
|
||||
|
||||
// https://stripe.com/docs/api/tokens
|
||||
|
||||
if (operation === 'create') {
|
||||
// ----------------------------------
|
||||
// token: create
|
||||
// ----------------------------------
|
||||
|
||||
const type = this.getNodeParameter('type', i);
|
||||
const body = {} as IDataObject;
|
||||
|
||||
if (type !== 'cardToken') {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Only card token creation implemented.',
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
|
||||
body.card = {
|
||||
number: this.getNodeParameter('number', i),
|
||||
exp_month: this.getNodeParameter('expirationMonth', i),
|
||||
exp_year: this.getNodeParameter('expirationYear', i),
|
||||
cvc: this.getNodeParameter('cvc', i),
|
||||
};
|
||||
|
||||
responseData = await stripeApiRequest.call(this, 'POST', '/tokens', body, {});
|
||||
}
|
||||
} else if (resource === 'meterEvent') {
|
||||
// *********************************************************************
|
||||
// meter event
|
||||
// *********************************************************************
|
||||
|
||||
// https://stripe.com/docs/api/billing/meter-event
|
||||
|
||||
if (operation === 'create') {
|
||||
// ----------------------------------
|
||||
// meterEvent: create
|
||||
// ----------------------------------
|
||||
|
||||
const eventName = this.getNodeParameter('eventName', i);
|
||||
const customerId = this.getNodeParameter('customerId', i);
|
||||
const value = this.getNodeParameter('value', i);
|
||||
|
||||
const payload: IDataObject = {
|
||||
stripe_customer_id: customerId,
|
||||
value,
|
||||
};
|
||||
|
||||
const body: IDataObject = {
|
||||
event_name: eventName,
|
||||
payload,
|
||||
};
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
|
||||
if (!isEmpty(additionalFields)) {
|
||||
if (additionalFields.identifier) {
|
||||
body.identifier = additionalFields.identifier;
|
||||
}
|
||||
|
||||
if (additionalFields.timestamp) {
|
||||
// Convert ISO date string to Unix timestamp
|
||||
const timestamp = new Date(additionalFields.timestamp as string).getTime() / 1000;
|
||||
body.timestamp = Math.floor(timestamp);
|
||||
}
|
||||
|
||||
if (additionalFields.customPayload) {
|
||||
const customPayloadData = additionalFields.customPayload as {
|
||||
properties: Array<{ key: string; value: string }>;
|
||||
};
|
||||
if (customPayloadData.properties && customPayloadData.properties.length > 0) {
|
||||
customPayloadData.properties.forEach((prop) => {
|
||||
// Guard against overwriting required fields
|
||||
if (prop.key !== 'stripe_customer_id' && prop.key !== 'value') {
|
||||
payload[prop.key] = prop.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
responseData = await stripeApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
'/billing/meter_events',
|
||||
body,
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.stripeTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Finance & Accounting", "Sales"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/stripe/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.stripetrigger/"
|
||||
}
|
||||
],
|
||||
"generic": [
|
||||
{
|
||||
"label": "Automate your customer journey with n8n: An interview with Blent.ai",
|
||||
"icon": "🚀",
|
||||
"url": "https://n8n.io/blog/automate-your-customer-journey-with-n8n-an-interview-with-blent-ai/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,985 @@
|
||||
/* eslint-disable n8n-nodes-base/node-param-description-excess-final-period */
|
||||
import type {
|
||||
IDataObject,
|
||||
IHookFunctions,
|
||||
IWebhookFunctions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IWebhookResponseData,
|
||||
JsonObject,
|
||||
NodeParameterValue,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError, NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { stripeApiRequest } from './helpers';
|
||||
import { verifySignature } from './StripeTriggerHelpers';
|
||||
|
||||
export class StripeTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Stripe Trigger',
|
||||
name: 'stripeTrigger',
|
||||
icon: 'file:stripe.svg',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Handle Stripe events via webhooks',
|
||||
defaults: {
|
||||
name: 'Stripe Trigger',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'stripeApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Events',
|
||||
name: 'events',
|
||||
type: 'multiOptions',
|
||||
required: true,
|
||||
default: [],
|
||||
description: 'The event to listen to',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-multi-options-type-unsorted-items
|
||||
options: [
|
||||
{
|
||||
name: '*',
|
||||
value: '*',
|
||||
description: 'Any time any event is triggered (Wildcard Event)',
|
||||
},
|
||||
{
|
||||
name: 'Account Updated',
|
||||
value: 'account.updated',
|
||||
description: 'Occurs whenever an account status or property has changed',
|
||||
},
|
||||
{
|
||||
name: 'Account Application.authorized',
|
||||
value: 'account.application.authorized',
|
||||
description:
|
||||
'Occurs whenever a user authorizes an application. Sent to the related application only.',
|
||||
},
|
||||
{
|
||||
name: 'Account Application.deauthorized',
|
||||
value: 'account.application.deauthorized',
|
||||
description:
|
||||
'Occurs whenever a user deauthorizes an application. Sent to the related application only.',
|
||||
},
|
||||
{
|
||||
name: 'Account External_account.created',
|
||||
value: 'account.external_account.created',
|
||||
description: 'Occurs whenever an external account is created.',
|
||||
},
|
||||
{
|
||||
name: 'Account External_account.deleted',
|
||||
value: 'account.external_account.deleted',
|
||||
description: 'Occurs whenever an external account is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Account External_account.updated',
|
||||
value: 'account.external_account.updated',
|
||||
description: 'Occurs whenever an external account is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Application Fee.created',
|
||||
value: 'application_fee.created',
|
||||
description: 'Occurs whenever an application fee is created on a charge.',
|
||||
},
|
||||
{
|
||||
name: 'Application Fee.refunded',
|
||||
value: 'application_fee.refunded',
|
||||
description:
|
||||
'Occurs whenever an application fee is refunded, whether from refunding a charge or from refunding the application fee directly. This includes partial refunds.',
|
||||
},
|
||||
{
|
||||
name: 'Application Fee.refund.updated',
|
||||
value: 'application_fee.refund.updated',
|
||||
description: 'Occurs whenever an application fee refund is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Balance Available',
|
||||
value: 'balance.available',
|
||||
description:
|
||||
'Occurs whenever your Stripe balance has been updated (e.g., when a charge is available to be paid out). By default, Stripe automatically transfers funds in your balance to your bank account on a daily basis.',
|
||||
},
|
||||
{
|
||||
name: 'Capability Updated',
|
||||
value: 'capability.updated',
|
||||
description: 'Occurs whenever a capability has new requirements or a new status.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Captured',
|
||||
value: 'charge.captured',
|
||||
description: 'Occurs whenever a previously uncaptured charge is captured.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Expired',
|
||||
value: 'charge.expired',
|
||||
description: 'Occurs whenever an uncaptured charge expires.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Failed',
|
||||
value: 'charge.failed',
|
||||
description: 'Occurs whenever a failed charge attempt occurs.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Pending',
|
||||
value: 'charge.pending',
|
||||
description: 'Occurs whenever a pending charge is created.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Refunded',
|
||||
value: 'charge.refunded',
|
||||
description: 'Occurs whenever a charge is refunded, including partial refunds.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Succeeded',
|
||||
value: 'charge.succeeded',
|
||||
description: 'Occurs whenever a new charge is created and is successful.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Updated',
|
||||
value: 'charge.updated',
|
||||
description: 'Occurs whenever a charge description or metadata is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Dispute.closed',
|
||||
value: 'charge.dispute.closed',
|
||||
description:
|
||||
'Occurs when a dispute is closed and the dispute status changes to lost, warning_closed, or won.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Dispute.created',
|
||||
value: 'charge.dispute.created',
|
||||
description: 'Occurs whenever a customer disputes a charge with their bank.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Dispute.funds_reinstated',
|
||||
value: 'charge.dispute.funds_reinstated',
|
||||
description:
|
||||
'Occurs when funds are reinstated to your account after a dispute is closed. This includes partially refunded payments.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Dispute.funds_withdrawn',
|
||||
value: 'charge.dispute.funds_withdrawn',
|
||||
description: 'Occurs when funds are removed from your account due to a dispute.',
|
||||
},
|
||||
{
|
||||
name: 'Charge Dispute.updated',
|
||||
value: 'charge.dispute.updated',
|
||||
description: 'Occurs when the dispute is updated (usually with evidence).',
|
||||
},
|
||||
{
|
||||
name: 'Charge Refund.updated',
|
||||
value: 'charge.refund.updated',
|
||||
description: 'Occurs whenever a refund is updated, on selected payment methods.',
|
||||
},
|
||||
{
|
||||
name: 'Checkout Session.completed',
|
||||
value: 'checkout.session.completed',
|
||||
description: 'Occurs when a Checkout Session has been successfully completed.',
|
||||
},
|
||||
{
|
||||
name: 'Coupon Created',
|
||||
value: 'coupon.created',
|
||||
description: 'Occurs whenever a coupon is created.',
|
||||
},
|
||||
{
|
||||
name: 'Coupon Deleted',
|
||||
value: 'coupon.deleted',
|
||||
description: 'Occurs whenever a coupon is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Coupon Updated',
|
||||
value: 'coupon.updated',
|
||||
description: 'Occurs whenever a coupon is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Credit Note.created',
|
||||
value: 'credit_note.created',
|
||||
description: 'Occurs whenever a credit note is created.',
|
||||
},
|
||||
{
|
||||
name: 'Credit Note.updated',
|
||||
value: 'credit_note.updated',
|
||||
description: 'Occurs whenever a credit note is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Credit Note.voided',
|
||||
value: 'credit_note.voided',
|
||||
description: 'Occurs whenever a credit note is voided.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Created',
|
||||
value: 'customer.created',
|
||||
description: 'Occurs whenever a new customer is created.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Deleted',
|
||||
value: 'customer.deleted',
|
||||
description: 'Occurs whenever a customer is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Updated',
|
||||
value: 'customer.updated',
|
||||
description: 'Occurs whenever any property of a customer changes.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Discount.created',
|
||||
value: 'customer.discount.created',
|
||||
description: 'Occurs whenever a coupon is attached to a customer.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Discount.deleted',
|
||||
value: 'customer.discount.deleted',
|
||||
description: 'Occurs whenever a coupon is removed from a customer.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Discount.updated',
|
||||
value: 'customer.discount.updated',
|
||||
description: 'Occurs whenever a customer is switched from one coupon to another.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Source.created',
|
||||
value: 'customer.source.created',
|
||||
description: 'Occurs whenever a new source is created for a customer.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Source.deleted',
|
||||
value: 'customer.source.deleted',
|
||||
description: 'Occurs whenever a source is removed from a customer.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Source.expiring',
|
||||
value: 'customer.source.expiring',
|
||||
description: 'Occurs whenever a card or source will expire at the end of the month.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Source.updated',
|
||||
value: 'customer.source.updated',
|
||||
description: "Occurs whenever a source's details are changed.",
|
||||
},
|
||||
{
|
||||
name: 'Customer Subscription.created',
|
||||
value: 'customer.subscription.created',
|
||||
description: 'Occurs whenever a customer is signed up for a new plan.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Subscription.deleted',
|
||||
value: 'customer.subscription.deleted',
|
||||
description: "Occurs whenever a customer's subscription ends.",
|
||||
},
|
||||
{
|
||||
name: 'Customer Subscription.trial_will_end',
|
||||
value: 'customer.subscription.trial_will_end',
|
||||
description:
|
||||
"Occurs three days before a subscription's trial period is scheduled to end, or when a trial is ended immediately (using trial_end=now).",
|
||||
},
|
||||
{
|
||||
name: 'Customer Subscription.updated',
|
||||
value: 'customer.subscription.updated',
|
||||
description:
|
||||
'Occurs whenever a subscription changes (e.g., switching from one plan to another, or changing the status from trial to active).',
|
||||
},
|
||||
{
|
||||
name: 'Customer Tax_id.created',
|
||||
value: 'customer.tax_id.created',
|
||||
description: 'Occurs whenever a tax ID is created for a customer.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Tax_id.deleted',
|
||||
value: 'customer.tax_id.deleted',
|
||||
description: 'Occurs whenever a tax ID is deleted from a customer.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Tax_id.updated',
|
||||
value: 'customer.tax_id.updated',
|
||||
description: "Occurs whenever a customer's tax ID is updated.",
|
||||
},
|
||||
{
|
||||
name: 'File Created',
|
||||
value: 'file.created',
|
||||
description:
|
||||
'Occurs whenever a new Stripe-generated file is available for your account.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Created',
|
||||
value: 'invoice.created',
|
||||
description:
|
||||
'Occurs whenever a new invoice is created. To learn how webhooks can be used with this event, and how they can affect it, see Using Webhooks with Subscriptions.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Deleted',
|
||||
value: 'invoice.deleted',
|
||||
description: 'Occurs whenever a draft invoice is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Finalized',
|
||||
value: 'invoice.finalized',
|
||||
description:
|
||||
'Occurs whenever a draft invoice is finalized and updated to be an open invoice.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Marked_uncollectible',
|
||||
value: 'invoice.marked_uncollectible',
|
||||
description: 'Occurs whenever an invoice is marked uncollectible.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Paid',
|
||||
value: 'invoice.paid',
|
||||
description:
|
||||
'Occurs whenever an invoice payment attempt succeeds or an invoice is marked as paid out-of-band.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Payment_action_required',
|
||||
value: 'invoice.payment_action_required',
|
||||
description:
|
||||
'Occurs whenever an invoice payment attempt requires further user action to complete.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Payment_failed',
|
||||
value: 'invoice.payment_failed',
|
||||
description:
|
||||
'Occurs whenever an invoice payment attempt fails, due either to a declined payment or to the lack of a stored payment method.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Payment_paid',
|
||||
value: 'invoice_payment.paid',
|
||||
description: 'Occurs when an InvoicePayment is successfully paid.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Payment_succeeded',
|
||||
value: 'invoice.payment_succeeded',
|
||||
description: 'Occurs whenever an invoice payment attempt succeeds.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Sent',
|
||||
value: 'invoice.sent',
|
||||
description: 'Occurs whenever an invoice email is sent out.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Upcoming',
|
||||
value: 'invoice.upcoming',
|
||||
description:
|
||||
'Occurs X number of days before a subscription is scheduled to create an invoice that is automatically charged—where X is determined by your subscriptions settings. Note: The received Invoice object will not have an invoice ID.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Updated',
|
||||
value: 'invoice.updated',
|
||||
description: 'Occurs whenever an invoice changes (e.g., the invoice amount).',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Voided',
|
||||
value: 'invoice.voided',
|
||||
description: 'Occurs whenever an invoice is voided.',
|
||||
},
|
||||
{
|
||||
name: 'Invoiceitem Created',
|
||||
value: 'invoiceitem.created',
|
||||
description: 'Occurs whenever an invoice item is created.',
|
||||
},
|
||||
{
|
||||
name: 'Invoiceitem Deleted',
|
||||
value: 'invoiceitem.deleted',
|
||||
description: 'Occurs whenever an invoice item is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Invoiceitem Updated',
|
||||
value: 'invoiceitem.updated',
|
||||
description: 'Occurs whenever an invoice item is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Authorization.created',
|
||||
value: 'issuing_authorization.created',
|
||||
description: 'Occurs whenever an authorization is created.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Authorization.request',
|
||||
value: 'issuing_authorization.request',
|
||||
description:
|
||||
'Represents a synchronous request for authorization, see Using your integration to handle authorization requests.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Authorization.updated',
|
||||
value: 'issuing_authorization.updated',
|
||||
description: 'Occurs whenever an authorization is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Card.created',
|
||||
value: 'issuing_card.created',
|
||||
description: 'Occurs whenever a card is created.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Card.updated',
|
||||
value: 'issuing_card.updated',
|
||||
description: 'Occurs whenever a card is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Cardholder.created',
|
||||
value: 'issuing_cardholder.created',
|
||||
description: 'Occurs whenever a cardholder is created.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Cardholder.updated',
|
||||
value: 'issuing_cardholder.updated',
|
||||
description: 'Occurs whenever a cardholder is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Dispute.created',
|
||||
value: 'issuing_dispute.created',
|
||||
description: 'Occurs whenever a dispute is created.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Dispute.updated',
|
||||
value: 'issuing_dispute.updated',
|
||||
description: 'Occurs whenever a dispute is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Settlement.created',
|
||||
value: 'issuing_settlement.created',
|
||||
description: 'Occurs whenever an issuing settlement is created.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Settlement.updated',
|
||||
value: 'issuing_settlement.updated',
|
||||
description: 'Occurs whenever an issuing settlement is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Transaction.created',
|
||||
value: 'issuing_transaction.created',
|
||||
description: 'Occurs whenever an issuing transaction is created.',
|
||||
},
|
||||
{
|
||||
name: 'Issuing Transaction.updated',
|
||||
value: 'issuing_transaction.updated',
|
||||
description: 'Occurs whenever an issuing transaction is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Order Created',
|
||||
value: 'order.created',
|
||||
description: 'Occurs whenever an order is created.',
|
||||
},
|
||||
{
|
||||
name: 'Order Payment_failed',
|
||||
value: 'order.payment_failed',
|
||||
description: 'Occurs whenever an order payment attempt fails.',
|
||||
},
|
||||
{
|
||||
name: 'Order Payment_succeeded',
|
||||
value: 'order.payment_succeeded',
|
||||
description: 'Occurs whenever an order payment attempt succeeds.',
|
||||
},
|
||||
{
|
||||
name: 'Order Updated',
|
||||
value: 'order.updated',
|
||||
description: 'Occurs whenever an order is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Order Return.created',
|
||||
value: 'order_return.created',
|
||||
description: 'Occurs whenever an order return is created.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Intent.amount_capturable_updated',
|
||||
value: 'payment_intent.amount_capturable_updated',
|
||||
description:
|
||||
'Occurs when a PaymentIntent has funds to be captured. Check the amount_capturable property on the PaymentIntent to determine the amount that can be captured. You may capture the PaymentIntent with an amount_to_capture value up to the specified amount. Learn more about capturing PaymentIntents.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Intent.canceled',
|
||||
value: 'payment_intent.canceled',
|
||||
description: 'Occurs when a PaymentIntent is canceled.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Intent.created',
|
||||
value: 'payment_intent.created',
|
||||
description: 'Occurs when a new PaymentIntent is created.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Intent.payment_failed',
|
||||
value: 'payment_intent.payment_failed',
|
||||
description:
|
||||
'Occurs when a PaymentIntent has failed the attempt to create a source or a payment.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Intent.succeeded',
|
||||
value: 'payment_intent.succeeded',
|
||||
description: 'Occurs when a PaymentIntent has been successfully fulfilled.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Intent.requires_action',
|
||||
value: 'payment_intent.requires_action',
|
||||
description: 'Occurs when a PaymentIntent requires an action.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Method.attached',
|
||||
value: 'payment_method.attached',
|
||||
description: 'Occurs whenever a new payment method is attached to a customer.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Method.card_automatically_updated',
|
||||
value: 'payment_method.card_automatically_updated',
|
||||
description:
|
||||
"Occurs whenever a card payment method's details are automatically updated by the network.",
|
||||
},
|
||||
{
|
||||
name: 'Payment Method.detached',
|
||||
value: 'payment_method.detached',
|
||||
description: 'Occurs whenever a payment method is detached from a customer.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Method.updated',
|
||||
value: 'payment_method.updated',
|
||||
description:
|
||||
'Occurs whenever a payment method is updated via the PaymentMethod update API.',
|
||||
},
|
||||
{
|
||||
name: 'Payout Canceled',
|
||||
value: 'payout.canceled',
|
||||
description: 'Occurs whenever a payout is canceled.',
|
||||
},
|
||||
{
|
||||
name: 'Payout Created',
|
||||
value: 'payout.created',
|
||||
description: 'Occurs whenever a payout is created.',
|
||||
},
|
||||
{
|
||||
name: 'Payout Failed',
|
||||
value: 'payout.failed',
|
||||
description: 'Occurs whenever a payout attempt fails.',
|
||||
},
|
||||
{
|
||||
name: 'Payout Paid',
|
||||
value: 'payout.paid',
|
||||
description:
|
||||
'Occurs whenever a payout is expected to be available in the destination account. If the payout fails, a payout.failed notification is also sent, at a later time.',
|
||||
},
|
||||
{
|
||||
name: 'Payout Updated',
|
||||
value: 'payout.updated',
|
||||
description: 'Occurs whenever a payout is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Person Created',
|
||||
value: 'person.created',
|
||||
description: 'Occurs whenever a person associated with an account is created.',
|
||||
},
|
||||
{
|
||||
name: 'Person Deleted',
|
||||
value: 'person.deleted',
|
||||
description: 'Occurs whenever a person associated with an account is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Person Updated',
|
||||
value: 'person.updated',
|
||||
description: 'Occurs whenever a person associated with an account is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Plan Created',
|
||||
value: 'plan.created',
|
||||
description: 'Occurs whenever a plan is created.',
|
||||
},
|
||||
{
|
||||
name: 'Plan Deleted',
|
||||
value: 'plan.deleted',
|
||||
description: 'Occurs whenever a plan is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Plan Updated',
|
||||
value: 'plan.updated',
|
||||
description: 'Occurs whenever a plan is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Product Created',
|
||||
value: 'product.created',
|
||||
description: 'Occurs whenever a product is created.',
|
||||
},
|
||||
{
|
||||
name: 'Product Deleted',
|
||||
value: 'product.deleted',
|
||||
description: 'Occurs whenever a product is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Product Updated',
|
||||
value: 'product.updated',
|
||||
description: 'Occurs whenever a product is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Radar Early_fraud_warning.created',
|
||||
value: 'radar.early_fraud_warning.created',
|
||||
description: 'Occurs whenever an early fraud warning is created.',
|
||||
},
|
||||
{
|
||||
name: 'Radar Early_fraud_warning.updated',
|
||||
value: 'radar.early_fraud_warning.updated',
|
||||
description: 'Occurs whenever an early fraud warning is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Recipient Created',
|
||||
value: 'recipient.created',
|
||||
description: 'Occurs whenever a recipient is created.',
|
||||
},
|
||||
{
|
||||
name: 'Recipient Deleted',
|
||||
value: 'recipient.deleted',
|
||||
description: 'Occurs whenever a recipient is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Recipient Updated',
|
||||
value: 'recipient.updated',
|
||||
description: 'Occurs whenever a recipient is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Reporting Report_run.failed',
|
||||
value: 'reporting.report_run.failed',
|
||||
description: 'Occurs whenever a requested **ReportRun** failed to complete.',
|
||||
},
|
||||
{
|
||||
name: 'Reporting Report_run.succeeded',
|
||||
value: 'reporting.report_run.succeeded',
|
||||
description: 'Occurs whenever a requested **ReportRun** completed succesfully.',
|
||||
},
|
||||
{
|
||||
name: 'Reporting Report_type.updated',
|
||||
value: 'reporting.report_type.updated',
|
||||
description:
|
||||
"Occurs whenever a **ReportType** is updated (typically to indicate that a new day's data has come available).",
|
||||
},
|
||||
{
|
||||
name: 'Review Closed',
|
||||
value: 'review.closed',
|
||||
description:
|
||||
"Occurs whenever a review is closed. The review's reason field indicates why: approved, disputed, refunded, or refunded_as_fraud.",
|
||||
},
|
||||
{
|
||||
name: 'Review Opened',
|
||||
value: 'review.opened',
|
||||
description: 'Occurs whenever a review is opened.',
|
||||
},
|
||||
{
|
||||
name: 'Setup Intent.canceled',
|
||||
value: 'setup_intent.canceled',
|
||||
description: 'Occurs when a SetupIntent is canceled.',
|
||||
},
|
||||
{
|
||||
name: 'Setup Intent.created',
|
||||
value: 'setup_intent.created',
|
||||
description: 'Occurs when a new SetupIntent is created.',
|
||||
},
|
||||
{
|
||||
name: 'Setup Intent.setup_failed',
|
||||
value: 'setup_intent.setup_failed',
|
||||
description:
|
||||
'Occurs when a SetupIntent has failed the attempt to setup a payment method.',
|
||||
},
|
||||
{
|
||||
name: 'Setup Intent.succeeded',
|
||||
value: 'setup_intent.succeeded',
|
||||
description: 'Occurs when an SetupIntent has successfully setup a payment method.',
|
||||
},
|
||||
{
|
||||
name: 'Sigma Scheduled_query_run.created',
|
||||
value: 'sigma.scheduled_query_run.created',
|
||||
description: 'Occurs whenever a Sigma scheduled query run finishes.',
|
||||
},
|
||||
{
|
||||
name: 'Sku Created',
|
||||
value: 'sku.created',
|
||||
description: 'Occurs whenever a SKU is created.',
|
||||
},
|
||||
{
|
||||
name: 'Sku Deleted',
|
||||
value: 'sku.deleted',
|
||||
description: 'Occurs whenever a SKU is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Sku Updated',
|
||||
value: 'sku.updated',
|
||||
description: 'Occurs whenever a SKU is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Source Canceled',
|
||||
value: 'source.canceled',
|
||||
description: 'Occurs whenever a source is canceled.',
|
||||
},
|
||||
{
|
||||
name: 'Source Chargeable',
|
||||
value: 'source.chargeable',
|
||||
description: 'Occurs whenever a source transitions to chargeable.',
|
||||
},
|
||||
{
|
||||
name: 'Source Failed',
|
||||
value: 'source.failed',
|
||||
description: 'Occurs whenever a source fails.',
|
||||
},
|
||||
{
|
||||
name: 'Source Mandate_notification',
|
||||
value: 'source.mandate_notification',
|
||||
description: 'Occurs whenever a source mandate notification method is set to manual.',
|
||||
},
|
||||
{
|
||||
name: 'Source Refund_attributes_required',
|
||||
value: 'source.refund_attributes_required',
|
||||
description:
|
||||
'Occurs whenever the refund attributes are required on a receiver source to process a refund or a mispayment.',
|
||||
},
|
||||
{
|
||||
name: 'Source Transaction.created',
|
||||
value: 'source.transaction.created',
|
||||
description: 'Occurs whenever a source transaction is created.',
|
||||
},
|
||||
{
|
||||
name: 'Source Transaction.updated',
|
||||
value: 'source.transaction.updated',
|
||||
description: 'Occurs whenever a source transaction is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Schedule.aborted',
|
||||
value: 'subscription_schedule.aborted',
|
||||
description:
|
||||
'Occurs whenever a subscription schedule is canceled due to the underlying subscription being canceled because of delinquency.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Schedule.canceled',
|
||||
value: 'subscription_schedule.canceled',
|
||||
description: 'Occurs whenever a subscription schedule is canceled.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Schedule.completed',
|
||||
value: 'subscription_schedule.completed',
|
||||
description: 'Occurs whenever a new subscription schedule is completed.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Schedule.created',
|
||||
value: 'subscription_schedule.created',
|
||||
description: 'Occurs whenever a new subscription schedule is created.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Schedule.expiring',
|
||||
value: 'subscription_schedule.expiring',
|
||||
description: 'Occurs 7 days before a subscription schedule will expire.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Schedule.released',
|
||||
value: 'subscription_schedule.released',
|
||||
description: 'Occurs whenever a new subscription schedule is released.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Schedule.updated',
|
||||
value: 'subscription_schedule.updated',
|
||||
description: 'Occurs whenever a subscription schedule is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Tax Rate.created',
|
||||
value: 'tax_rate.created',
|
||||
description: 'Occurs whenever a new tax rate is created.',
|
||||
},
|
||||
{
|
||||
name: 'Tax Rate.updated',
|
||||
value: 'tax_rate.updated',
|
||||
description: 'Occurs whenever a tax rate is updated.',
|
||||
},
|
||||
{
|
||||
name: 'Topup Canceled',
|
||||
value: 'topup.canceled',
|
||||
description: 'Occurs whenever a top-up is canceled.',
|
||||
},
|
||||
{
|
||||
name: 'Topup Created',
|
||||
value: 'topup.created',
|
||||
description: 'Occurs whenever a top-up is created.',
|
||||
},
|
||||
{
|
||||
name: 'Topup Failed',
|
||||
value: 'topup.failed',
|
||||
description: 'Occurs whenever a top-up fails.',
|
||||
},
|
||||
{
|
||||
name: 'Topup Reversed',
|
||||
value: 'topup.reversed',
|
||||
description: 'Occurs whenever a top-up is reversed.',
|
||||
},
|
||||
{
|
||||
name: 'Topup Succeeded',
|
||||
value: 'topup.succeeded',
|
||||
description: 'Occurs whenever a top-up succeeds.',
|
||||
},
|
||||
{
|
||||
name: 'Transfer Created',
|
||||
value: 'transfer.created',
|
||||
description: 'Occurs whenever a transfer is created.',
|
||||
},
|
||||
{
|
||||
name: 'Transfer Failed',
|
||||
value: 'transfer.failed',
|
||||
description: 'Occurs whenever a transfer failed.',
|
||||
},
|
||||
{
|
||||
name: 'Transfer Paid',
|
||||
value: 'transfer.paid',
|
||||
description:
|
||||
'Occurs after a transfer is paid. For Instant Payouts, the event will be sent on the next business day, although the funds should be received well beforehand.',
|
||||
},
|
||||
{
|
||||
name: 'Transfer Reversed',
|
||||
value: 'transfer.reversed',
|
||||
description: 'Occurs whenever a transfer is reversed, including partial reversals.',
|
||||
},
|
||||
{
|
||||
name: 'Transfer Updated',
|
||||
value: 'transfer.updated',
|
||||
description: "Occurs whenever a transfer's description or metadata is updated.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'API Version',
|
||||
name: 'apiVersion',
|
||||
type: 'string',
|
||||
placeholder: '2026-01-28.clover',
|
||||
default: '',
|
||||
description:
|
||||
'The API version to use for requests. It controls the format and structure of the incoming event payloads that Stripe sends to your webhook. If empty, Stripe will use the default API version set in your account at the time, which may lead to event processing issues if the API version changes in the future.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
|
||||
if (webhookData.webhookId === undefined) {
|
||||
// No webhook id is set so no webhook can exist
|
||||
return false;
|
||||
}
|
||||
|
||||
// Webhook got created before so check if it still exists
|
||||
const endpoint = `/webhook_endpoints/${webhookData.webhookId}`;
|
||||
|
||||
try {
|
||||
await stripeApiRequest.call(this, 'GET', endpoint, {});
|
||||
} catch (error) {
|
||||
if (error.httpCode === '404' || error.message.includes('resource_missing')) {
|
||||
// Webhook does not exist
|
||||
delete webhookData.webhookId;
|
||||
delete webhookData.webhookEvents;
|
||||
delete webhookData.webhookSecret;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Some error occured
|
||||
throw error;
|
||||
}
|
||||
|
||||
// If it did not error then the webhook exists
|
||||
return true;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
|
||||
const webhookDescription = `Created by n8n for workflow ID: ${this.getWorkflow().id}`;
|
||||
|
||||
const events = this.getNodeParameter('events', []);
|
||||
|
||||
const endpoint = '/webhook_endpoints';
|
||||
|
||||
interface StripeWebhookBody {
|
||||
url: string | undefined;
|
||||
description: string;
|
||||
enabled_events: object | NodeParameterValue;
|
||||
api_version?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const body: StripeWebhookBody = {
|
||||
url: webhookUrl,
|
||||
description: webhookDescription,
|
||||
enabled_events: events,
|
||||
};
|
||||
|
||||
const apiVersion = this.getNodeParameter('apiVersion', '');
|
||||
if (apiVersion && apiVersion !== '') {
|
||||
body.api_version = apiVersion as string;
|
||||
}
|
||||
|
||||
const responseData = await stripeApiRequest.call(this, 'POST', endpoint, body);
|
||||
|
||||
if (
|
||||
responseData.id === undefined ||
|
||||
responseData.secret === undefined ||
|
||||
responseData.status !== 'enabled'
|
||||
) {
|
||||
// Required data is missing so was not successful
|
||||
throw new NodeApiError(this.getNode(), responseData as JsonObject, {
|
||||
message: 'Stripe webhook creation response did not contain the expected data.',
|
||||
});
|
||||
}
|
||||
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
webhookData.webhookId = responseData.id as string;
|
||||
webhookData.webhookEvents = responseData.enabled_events as string[];
|
||||
webhookData.webhookSecret = responseData.secret as string;
|
||||
|
||||
return true;
|
||||
},
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
|
||||
if (webhookData.webhookId !== undefined) {
|
||||
const endpoint = `/webhook_endpoints/${webhookData.webhookId}`;
|
||||
const body = {};
|
||||
|
||||
try {
|
||||
await stripeApiRequest.call(this, 'DELETE', endpoint, body);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove from the static workflow data so that it is clear
|
||||
// that no webhooks are registered anymore
|
||||
delete webhookData.webhookId;
|
||||
delete webhookData.webhookEvents;
|
||||
delete webhookData.webhookSecret;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const bodyData = this.getBodyData();
|
||||
const req = this.getRequestObject();
|
||||
|
||||
if (!(await verifySignature.call(this))) {
|
||||
const res = this.getResponseObject();
|
||||
res.status(401).send('Unauthorized').end();
|
||||
return {
|
||||
noWebhookResponse: true,
|
||||
};
|
||||
}
|
||||
|
||||
const events = this.getNodeParameter('events', []) as string[];
|
||||
const eventType = bodyData.type as string | undefined;
|
||||
|
||||
if (eventType === undefined || (!events.includes('*') && !events.includes(eventType))) {
|
||||
// If not eventType is defined or when one is defined but we are not
|
||||
// listening to it do not start the workflow.
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
workflowData: [this.helpers.returnJsonArray(req.body as IDataObject)],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import type { IWebhookFunctions } from 'n8n-workflow';
|
||||
|
||||
export async function verifySignature(this: IWebhookFunctions): Promise<boolean> {
|
||||
const credential = await this.getCredentials('stripeApi');
|
||||
if (!credential?.signatureSecret) {
|
||||
return true; // No signature secret provided, skip verification
|
||||
}
|
||||
|
||||
const req = this.getRequestObject();
|
||||
|
||||
const signature = req.header('stripe-signature');
|
||||
if (!signature) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse the Stripe signature header
|
||||
const elements = signature.split(',');
|
||||
let timestamp: string | undefined;
|
||||
let signatureValue: string | undefined;
|
||||
|
||||
for (const element of elements) {
|
||||
if (element.startsWith('t=')) {
|
||||
timestamp = element.substring(2);
|
||||
} else if (element.startsWith('v1=')) {
|
||||
signatureValue = element.substring(3);
|
||||
}
|
||||
}
|
||||
|
||||
if (!timestamp || !signatureValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify timestamp
|
||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||
const webhookTimestamp = parseInt(timestamp, 10);
|
||||
const TIMESTAMP_TOLERANCE_SECONDS = 300; // 5 minutes
|
||||
|
||||
if (Math.abs(currentTimestamp - webhookTimestamp) > TIMESTAMP_TOLERANCE_SECONDS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof credential.signatureSecret !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!req.rawBody) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let rawBodyString: string;
|
||||
if (Buffer.isBuffer(req.rawBody)) {
|
||||
rawBodyString = req.rawBody.toString();
|
||||
} else {
|
||||
rawBodyString = typeof req.rawBody === 'string' ? req.rawBody : JSON.stringify(req.rawBody);
|
||||
}
|
||||
|
||||
const signedPayload = `${timestamp}.${rawBodyString}`;
|
||||
const hmac = createHmac('sha256', credential.signatureSecret);
|
||||
hmac.update(signedPayload);
|
||||
const computedSignature = hmac.digest('hex');
|
||||
|
||||
const computedBuffer = Buffer.from(computedSignature);
|
||||
const providedBuffer = Buffer.from(signatureValue);
|
||||
|
||||
return (
|
||||
computedBuffer.length === providedBuffer.length &&
|
||||
timingSafeEqual(computedBuffer, providedBuffer)
|
||||
);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"available": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"source_types": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"card": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"livemode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"pending": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"source_types": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"card": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"refund_and_dispute_prefunding": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"available": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pending": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 2
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"amount_captured": {
|
||||
"type": "integer"
|
||||
},
|
||||
"amount_refunded": {
|
||||
"type": "integer"
|
||||
},
|
||||
"application_fee": {
|
||||
"type": "null"
|
||||
},
|
||||
"application_fee_amount": {
|
||||
"type": "null"
|
||||
},
|
||||
"captured": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"destination": {
|
||||
"type": "null"
|
||||
},
|
||||
"dispute": {
|
||||
"type": "null"
|
||||
},
|
||||
"disputed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"failure_balance_transaction": {
|
||||
"type": "null"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"livemode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date_due": {
|
||||
"type": "string"
|
||||
},
|
||||
"erp_provider_dimension_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoice_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"period_end": {
|
||||
"type": "string"
|
||||
},
|
||||
"period_start": {
|
||||
"type": "string"
|
||||
},
|
||||
"reservation_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"on_behalf_of": {
|
||||
"type": "null"
|
||||
},
|
||||
"order": {
|
||||
"type": "null"
|
||||
},
|
||||
"outcome": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"network_status": {
|
||||
"type": "string"
|
||||
},
|
||||
"risk_level": {
|
||||
"type": "string"
|
||||
},
|
||||
"risk_score": {
|
||||
"type": "integer"
|
||||
},
|
||||
"seller_message": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"paid": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"payment_intent": {
|
||||
"type": "string"
|
||||
},
|
||||
"payment_method": {
|
||||
"type": "string"
|
||||
},
|
||||
"payment_method_details": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"card": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"brand": {
|
||||
"type": "string"
|
||||
},
|
||||
"capture_before": {
|
||||
"type": "integer"
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
},
|
||||
"exp_month": {
|
||||
"type": "integer"
|
||||
},
|
||||
"exp_year": {
|
||||
"type": "integer"
|
||||
},
|
||||
"extended_authorization": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fingerprint": {
|
||||
"type": "string"
|
||||
},
|
||||
"funding": {
|
||||
"type": "string"
|
||||
},
|
||||
"incremental_authorization": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"installments": {
|
||||
"type": "null"
|
||||
},
|
||||
"last4": {
|
||||
"type": "string"
|
||||
},
|
||||
"mandate": {
|
||||
"type": "null"
|
||||
},
|
||||
"multicapture": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
"type": "string"
|
||||
},
|
||||
"network_token": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"used": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"network_transaction_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"overcapture": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maximum_amount_capturable": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"regulated_status": {
|
||||
"type": "string"
|
||||
},
|
||||
"three_d_secure": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"refunded": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"refunds": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"balance_transaction": {
|
||||
"type": "string"
|
||||
},
|
||||
"charge": {
|
||||
"type": "string"
|
||||
},
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"destination_details": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"card": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reference": {
|
||||
"type": "string"
|
||||
},
|
||||
"reference_status": {
|
||||
"type": "string"
|
||||
},
|
||||
"reference_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"payment_intent": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"type": "null"
|
||||
},
|
||||
"source_transfer_reversal": {
|
||||
"type": "null"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"transfer_reversal": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"has_more": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"type": "null"
|
||||
},
|
||||
"shipping": {
|
||||
"type": "null"
|
||||
},
|
||||
"source": {
|
||||
"type": "null"
|
||||
},
|
||||
"source_transfer": {
|
||||
"type": "null"
|
||||
},
|
||||
"statement_descriptor": {
|
||||
"type": "null"
|
||||
},
|
||||
"statement_descriptor_suffix": {
|
||||
"type": "null"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"transfer_data": {
|
||||
"type": "null"
|
||||
},
|
||||
"transfer_group": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"amount_captured": {
|
||||
"type": "integer"
|
||||
},
|
||||
"amount_refunded": {
|
||||
"type": "integer"
|
||||
},
|
||||
"application_fee": {
|
||||
"type": "null"
|
||||
},
|
||||
"captured": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"destination": {
|
||||
"type": "null"
|
||||
},
|
||||
"dispute": {
|
||||
"type": "null"
|
||||
},
|
||||
"disputed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"failure_balance_transaction": {
|
||||
"type": "null"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"livemode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"on_behalf_of": {
|
||||
"type": "null"
|
||||
},
|
||||
"order": {
|
||||
"type": "null"
|
||||
},
|
||||
"outcome": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"network_status": {
|
||||
"type": "string"
|
||||
},
|
||||
"risk_level": {
|
||||
"type": "string"
|
||||
},
|
||||
"seller_message": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"paid": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"payment_method_details": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"card": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"brand": {
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
},
|
||||
"exp_month": {
|
||||
"type": "integer"
|
||||
},
|
||||
"exp_year": {
|
||||
"type": "integer"
|
||||
},
|
||||
"extended_authorization": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fingerprint": {
|
||||
"type": "string"
|
||||
},
|
||||
"funding": {
|
||||
"type": "string"
|
||||
},
|
||||
"incremental_authorization": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"installments": {
|
||||
"type": "null"
|
||||
},
|
||||
"last4": {
|
||||
"type": "string"
|
||||
},
|
||||
"mandate": {
|
||||
"type": "null"
|
||||
},
|
||||
"multicapture": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
"type": "string"
|
||||
},
|
||||
"network_token": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"used": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overcapture": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maximum_amount_capturable": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"regulated_status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"refunded": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"refunds": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"has_more": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"type": "null"
|
||||
},
|
||||
"source_transfer": {
|
||||
"type": "null"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"transfer_data": {
|
||||
"type": "null"
|
||||
},
|
||||
"transfer_group": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"balance": {
|
||||
"type": "integer"
|
||||
},
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"currency": {
|
||||
"type": "null"
|
||||
},
|
||||
"default_source": {
|
||||
"type": "null"
|
||||
},
|
||||
"delinquent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"discount": {
|
||||
"type": "null"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoice_prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoice_settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"custom_fields": {
|
||||
"type": "null"
|
||||
},
|
||||
"default_payment_method": {
|
||||
"type": "null"
|
||||
},
|
||||
"footer": {
|
||||
"type": "null"
|
||||
},
|
||||
"rendering_options": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
"livemode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"next_invoice_sequence": {
|
||||
"type": "integer"
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"tax_exempt": {
|
||||
"type": "string"
|
||||
},
|
||||
"test_clock": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"balance": {
|
||||
"type": "integer"
|
||||
},
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"delinquent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoice_prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoice_settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"footer": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
"livemode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"preferred_locales": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tax_exempt": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"balance": {
|
||||
"type": "integer"
|
||||
},
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"delinquent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoice_prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoice_settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"custom_fields": {
|
||||
"type": "null"
|
||||
},
|
||||
"footer": {
|
||||
"type": "null"
|
||||
},
|
||||
"rendering_options": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
"livemode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"preferred_locales": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tax_exempt": {
|
||||
"type": "string"
|
||||
},
|
||||
"test_clock": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"balance": {
|
||||
"type": "integer"
|
||||
},
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"delinquent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"discount": {
|
||||
"type": "null"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoice_prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoice_settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"custom_fields": {
|
||||
"type": "null"
|
||||
},
|
||||
"footer": {
|
||||
"type": "null"
|
||||
},
|
||||
"rendering_options": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
"livemode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Telefone": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"next_invoice_sequence": {
|
||||
"type": "integer"
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"preferred_locales": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tax_exempt": {
|
||||
"type": "string"
|
||||
},
|
||||
"test_clock": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { IHookFunctions, IWebhookFunctions } from 'n8n-workflow';
|
||||
|
||||
import { stripeApiRequest } from '../helpers';
|
||||
import { StripeTrigger } from '../StripeTrigger.node';
|
||||
import { verifySignature } from '../StripeTriggerHelpers';
|
||||
|
||||
jest.mock('../helpers', () => ({
|
||||
stripeApiRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../StripeTriggerHelpers', () => ({
|
||||
verifySignature: jest.fn().mockResolvedValue(true),
|
||||
}));
|
||||
|
||||
const mockedStripeApiRequest = jest.mocked(stripeApiRequest);
|
||||
const mockedVerifySignature = jest.mocked(verifySignature);
|
||||
|
||||
describe('Stripe Trigger Node', () => {
|
||||
let node: StripeTrigger;
|
||||
let mockNodeFunctions: IHookFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
node = new StripeTrigger();
|
||||
|
||||
mockNodeFunctions = {
|
||||
getNodeWebhookUrl: jest.fn().mockReturnValue('https://webhook.url/test'),
|
||||
getWorkflow: jest.fn().mockReturnValue({ id: 'test-workflow-id' }),
|
||||
getNodeParameter: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
getWorkflowStaticData: jest.fn().mockReturnValue({}),
|
||||
getNode: jest.fn().mockReturnValue({ name: 'StripeTrigger' }),
|
||||
getWebhookName: jest.fn().mockReturnValue('default'),
|
||||
getContext: jest.fn(),
|
||||
getActivationMode: jest.fn(),
|
||||
getMode: jest.fn(),
|
||||
getNodeExecutionData: jest.fn(),
|
||||
getRestApiUrl: jest.fn(),
|
||||
getTimezone: jest.fn(),
|
||||
helpers: {} as any,
|
||||
} as unknown as IHookFunctions;
|
||||
|
||||
// (mockNodeFunctions.getCredentials as jest.Mock).mockResolvedValue({
|
||||
// secretKey: 'sk_test_123',
|
||||
// });
|
||||
|
||||
mockedStripeApiRequest.mockResolvedValue({
|
||||
id: 'we_test123',
|
||||
secret: 'whsec_test123',
|
||||
status: 'enabled',
|
||||
enabled_events: ['*'],
|
||||
});
|
||||
|
||||
mockedStripeApiRequest.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not send API version in body if not specified', async () => {
|
||||
(mockNodeFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
||||
if (param === 'events') return ['*'];
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const expectedRequestBody = {
|
||||
url: 'https://webhook.url/test',
|
||||
description: 'Created by n8n for workflow ID: test-workflow-id',
|
||||
enabled_events: ['*'],
|
||||
};
|
||||
|
||||
const endpoint = '/webhook_endpoints';
|
||||
|
||||
await node.webhookMethods.default.create.call(mockNodeFunctions);
|
||||
expect(mockedStripeApiRequest).toHaveBeenCalledWith('POST', endpoint, expectedRequestBody);
|
||||
|
||||
const callArgs = mockedStripeApiRequest.mock.calls[0];
|
||||
const requestBody = callArgs[2];
|
||||
expect(requestBody).not.toHaveProperty('api_version');
|
||||
});
|
||||
|
||||
it('should send API version in body if specified in node parameters', async () => {
|
||||
(mockNodeFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
||||
if (param === 'apiVersion') return '2025-05-28.basil';
|
||||
if (param === 'events') return ['*'];
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const expectedRequestBody = {
|
||||
url: 'https://webhook.url/test',
|
||||
description: 'Created by n8n for workflow ID: test-workflow-id',
|
||||
enabled_events: ['*'],
|
||||
api_version: '2025-05-28.basil',
|
||||
};
|
||||
|
||||
const endpoint = '/webhook_endpoints';
|
||||
|
||||
await node.webhookMethods.default.create.call(mockNodeFunctions);
|
||||
expect(mockedStripeApiRequest).toHaveBeenCalledWith('POST', endpoint, expectedRequestBody);
|
||||
|
||||
const callArgs = mockedStripeApiRequest.mock.calls[0];
|
||||
const requestBody = callArgs[2];
|
||||
expect(requestBody).toHaveProperty('api_version', '2025-05-28.basil');
|
||||
});
|
||||
|
||||
describe('webhook signature verification', () => {
|
||||
let mockWebhookFunctions: IWebhookFunctions;
|
||||
const testBody = { type: 'charge.succeeded', id: 'ch_123' };
|
||||
const rawBody = JSON.stringify(testBody);
|
||||
|
||||
beforeEach(() => {
|
||||
mockWebhookFunctions = {
|
||||
getBodyData: jest.fn().mockReturnValue(testBody),
|
||||
getRequestObject: jest.fn().mockReturnValue({
|
||||
rawBody: Buffer.from(rawBody),
|
||||
body: testBody,
|
||||
}),
|
||||
getResponseObject: jest.fn().mockReturnValue({
|
||||
status: jest.fn().mockReturnThis(),
|
||||
send: jest.fn().mockReturnThis(),
|
||||
end: jest.fn(),
|
||||
}),
|
||||
getNodeParameter: jest.fn().mockReturnValue(['*']),
|
||||
helpers: {
|
||||
returnJsonArray: jest.fn().mockImplementation((data) => [data]),
|
||||
},
|
||||
} as unknown as IWebhookFunctions;
|
||||
|
||||
// Reset the verifySignature mock to return true by default
|
||||
mockedVerifySignature.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it('should process webhook with valid signature', async () => {
|
||||
mockedVerifySignature.mockResolvedValue(true);
|
||||
|
||||
const result = await node.webhook.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual({
|
||||
workflowData: [[testBody]],
|
||||
});
|
||||
expect(mockedVerifySignature).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should reject webhook with invalid signature', async () => {
|
||||
mockedVerifySignature.mockResolvedValue(false);
|
||||
|
||||
const result = await node.webhook.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual({
|
||||
noWebhookResponse: true,
|
||||
});
|
||||
expect(mockedVerifySignature).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should handle events filtering correctly', async () => {
|
||||
mockedVerifySignature.mockResolvedValue(true);
|
||||
(mockWebhookFunctions.getNodeParameter as jest.Mock).mockReturnValue([
|
||||
'payment_intent.succeeded',
|
||||
]);
|
||||
|
||||
const result = await node.webhook.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should process webhook when event type matches filter', async () => {
|
||||
mockedVerifySignature.mockResolvedValue(true);
|
||||
(mockWebhookFunctions.getNodeParameter as jest.Mock).mockReturnValue(['charge.succeeded']);
|
||||
|
||||
const result = await node.webhook.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual({
|
||||
workflowData: [[testBody]],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
import { createHmac } from 'crypto';
|
||||
import type { IWebhookFunctions } from 'n8n-workflow';
|
||||
|
||||
import { verifySignature } from '../StripeTriggerHelpers';
|
||||
|
||||
describe('StripeTriggerHelpers', () => {
|
||||
describe('verifySignature', () => {
|
||||
let mockWebhookFunctions: IWebhookFunctions;
|
||||
const webhookSecret = 'whsec_test123456789';
|
||||
const getCurrentTimestamp = () => Math.floor(Date.now() / 1000).toString();
|
||||
const testBody = { type: 'charge.succeeded', id: 'ch_123' };
|
||||
const rawBody = JSON.stringify(testBody);
|
||||
|
||||
function generateValidSignature(timestamp: string, body: string, secret: string): string {
|
||||
const signedPayload = `${timestamp}.${body}`;
|
||||
const signature = createHmac('sha256', secret).update(signedPayload).digest('hex');
|
||||
return `t=${timestamp},v1=${signature}`;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockWebhookFunctions = {
|
||||
getCredentials: jest.fn().mockResolvedValue({
|
||||
secretKey: 'sk_test_123',
|
||||
signatureSecret: webhookSecret,
|
||||
}),
|
||||
getRequestObject: jest.fn().mockReturnValue({
|
||||
header: jest.fn(),
|
||||
rawBody: Buffer.from(rawBody),
|
||||
}),
|
||||
} as unknown as IWebhookFunctions;
|
||||
});
|
||||
|
||||
it('should return true when no signature secret is provided', async () => {
|
||||
(mockWebhookFunctions.getCredentials as jest.Mock).mockResolvedValue({
|
||||
secretKey: 'sk_test_123',
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when stripe-signature header is missing', async () => {
|
||||
const mockHeader = jest.fn().mockReturnValue(undefined);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockHeader).toHaveBeenCalledWith('stripe-signature');
|
||||
});
|
||||
|
||||
it('should return false when signature format is invalid', async () => {
|
||||
const mockHeader = jest.fn().mockReturnValue('invalid-format');
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when timestamp is missing', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const signature = createHmac('sha256', webhookSecret)
|
||||
.update(`${timestamp}.${rawBody}`)
|
||||
.digest('hex');
|
||||
const mockHeader = jest.fn().mockReturnValue(`v1=${signature}`);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when v1 signature is missing', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const mockHeader = jest.fn().mockReturnValue(`t=${timestamp}`);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when signature is valid', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const validSignature = generateValidSignature(timestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when signature is invalid', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const wrongSecret = 'wrong_secret';
|
||||
const invalidSignature = generateValidSignature(timestamp, rawBody, wrongSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(invalidSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle complex signature header with multiple elements', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const signature = createHmac('sha256', webhookSecret)
|
||||
.update(`${timestamp}.${rawBody}`)
|
||||
.digest('hex');
|
||||
const complexHeader = `t=${timestamp},v1=${signature},v0=old_signature`;
|
||||
const mockHeader = jest.fn().mockReturnValue(complexHeader);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle string rawBody', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const validSignature = generateValidSignature(timestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody, // String instead of Buffer
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when rawBody is missing', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const validSignature = generateValidSignature(timestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: null,
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when signatureSecret is not a string', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const validSignature = generateValidSignature(timestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
(mockWebhookFunctions.getCredentials as jest.Mock).mockResolvedValue({
|
||||
secretKey: 'sk_test_123',
|
||||
signatureSecret: 123, // Not a string
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when timestamp is older than 5 minutes', async () => {
|
||||
// Create timestamp that's 6 minutes (360 seconds) old
|
||||
const oldTimestamp = (Math.floor(Date.now() / 1000) - 360).toString();
|
||||
const validSignature = generateValidSignature(oldTimestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when timestamp is from the future beyond tolerance', async () => {
|
||||
// Create timestamp that's 6 minutes (360 seconds) in the future
|
||||
const futureTimestamp = (Math.floor(Date.now() / 1000) + 360).toString();
|
||||
const validSignature = generateValidSignature(futureTimestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when timestamp is within tolerance', async () => {
|
||||
// Create timestamp that's 4 minutes (240 seconds) old - within 5 minute tolerance
|
||||
const recentTimestamp = (Math.floor(Date.now() / 1000) - 240).toString();
|
||||
const validSignature = generateValidSignature(recentTimestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as helpers from '../helpers';
|
||||
|
||||
describe('adjustMetadata', () => {
|
||||
it('it should adjust multiple metadata values', async () => {
|
||||
const additionalFieldsValues = {
|
||||
metadata: {
|
||||
metadataProperties: [
|
||||
{
|
||||
key: 'keyA',
|
||||
value: 'valueA',
|
||||
},
|
||||
{
|
||||
key: 'keyB',
|
||||
value: 'valueB',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const adjustedMetadata = helpers.adjustMetadata(additionalFieldsValues);
|
||||
|
||||
const expectedAdjustedMetadata = {
|
||||
metadata: {
|
||||
keyA: 'valueA',
|
||||
keyB: 'valueB',
|
||||
},
|
||||
};
|
||||
expect(adjustedMetadata).toStrictEqual(expectedAdjustedMetadata);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
const baseUrl = 'https://api.stripe.com/v1';
|
||||
|
||||
const meterEventResponse = {
|
||||
id: 'evt_test_123',
|
||||
object: 'billing.meter_event',
|
||||
event_name: 'api_request',
|
||||
created: 1705320600,
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: 100,
|
||||
},
|
||||
livemode: false,
|
||||
};
|
||||
|
||||
describe('Stripe - Meter Event Workflows', () => {
|
||||
const credentials = {
|
||||
stripeApi: {
|
||||
secretKey: 'sk_test_fake_key',
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
// Basic meter event creation
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.post('/billing/meter_events', {
|
||||
event_name: 'api_request',
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: 100,
|
||||
},
|
||||
})
|
||||
.reply(200, meterEventResponse);
|
||||
|
||||
// Meter event with identifier
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.post('/billing/meter_events', {
|
||||
event_name: 'api_request',
|
||||
identifier: 'unique_event_id_123',
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: 100,
|
||||
},
|
||||
})
|
||||
.reply(200, {
|
||||
...meterEventResponse,
|
||||
identifier: 'unique_event_id_123',
|
||||
});
|
||||
|
||||
// Meter event with custom payload properties
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.post('/billing/meter_events', {
|
||||
event_name: 'api_request',
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: 100,
|
||||
endpoint: '/api/v1/users',
|
||||
method: 'GET',
|
||||
},
|
||||
})
|
||||
.reply(200, {
|
||||
...meterEventResponse,
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: 100,
|
||||
endpoint: '/api/v1/users',
|
||||
method: 'GET',
|
||||
},
|
||||
});
|
||||
|
||||
// Negative value support
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.post('/billing/meter_events', {
|
||||
event_name: 'api_request',
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: -50,
|
||||
},
|
||||
})
|
||||
.reply(200, {
|
||||
...meterEventResponse,
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: -50,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// NodeTestHarness will discover and run all workflow JSON files in this directory
|
||||
new NodeTestHarness().setupTests({ credentials });
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - Basic Create",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": 100
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": 100
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - With Custom Payload",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": 100,
|
||||
"additionalFields": {
|
||||
"customPayload": {
|
||||
"properties": [
|
||||
{
|
||||
"key": "endpoint",
|
||||
"value": "/api/v1/users"
|
||||
},
|
||||
{
|
||||
"key": "method",
|
||||
"value": "GET"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": 100,
|
||||
"endpoint": "/api/v1/users",
|
||||
"method": "GET"
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - Guard Customer ID",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": 100,
|
||||
"additionalFields": {
|
||||
"customPayload": {
|
||||
"properties": [
|
||||
{
|
||||
"key": "stripe_customer_id",
|
||||
"value": "cus_malicious_override"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": 100
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - Guard Value",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": 100,
|
||||
"additionalFields": {
|
||||
"customPayload": {
|
||||
"properties": [
|
||||
{
|
||||
"key": "value",
|
||||
"value": "999"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": 100
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - With Identifier",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": 100,
|
||||
"additionalFields": {
|
||||
"identifier": "unique_event_id_123"
|
||||
}
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"identifier": "unique_event_id_123",
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": 100
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - Negative Value",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": -50
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": -50
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const balanceOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'get',
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a balance',
|
||||
action: 'Get a balance',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['balance'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,472 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const chargeOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'get',
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a charge',
|
||||
action: 'Create a charge',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a charge',
|
||||
action: 'Get a charge',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
description: 'Get many charges',
|
||||
action: 'Get many charges',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update a charge',
|
||||
action: 'Update a charge',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['charge'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const chargeFields: INodeProperties[] = [
|
||||
// ----------------------------------
|
||||
// charge: create
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Customer ID',
|
||||
name: 'customerId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the customer to be associated with this charge',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['charge'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Amount',
|
||||
name: 'amount',
|
||||
type: 'number',
|
||||
required: true,
|
||||
default: 0,
|
||||
description:
|
||||
'Amount in cents to be collected for this charge, e.g. enter <code>100</code> for $1.00',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 99999999,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['charge'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Currency Name or ID',
|
||||
name: 'currency',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getCurrencies',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description:
|
||||
'Three-letter ISO currency code, e.g. <code>USD</code> or <code>EUR</code>. It must be a <a href="https://stripe.com/docs/currencies">Stripe-supported currency</a>. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['charge'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Source ID',
|
||||
name: 'source',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: "ID of the customer's payment source to be charged",
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['charge'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['charge'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Arbitrary text to describe the charge to create',
|
||||
},
|
||||
{
|
||||
displayName: 'Metadata',
|
||||
name: 'metadata',
|
||||
type: 'fixedCollection',
|
||||
default: [],
|
||||
placeholder: 'Add Metadata Item',
|
||||
description: 'Set of key-value pairs to attach to the charge to create',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Metadata Properties',
|
||||
name: 'metadataProperties',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Receipt Email',
|
||||
name: 'receipt_email',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Email address to which the receipt for this charge will be sent',
|
||||
},
|
||||
{
|
||||
displayName: 'Shipping',
|
||||
name: 'shipping',
|
||||
type: 'fixedCollection',
|
||||
description: 'Shipping information for the charge',
|
||||
placeholder: 'Add Field',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: [],
|
||||
options: [
|
||||
{
|
||||
displayName: 'Shipping Properties',
|
||||
name: 'shippingProperties',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Recipient Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
description: 'Name of the person who will receive the shipment',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'address',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
placeholder: 'Add Field',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Details',
|
||||
name: 'details',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Line 1',
|
||||
name: 'line1',
|
||||
description: 'Address line 1 (e.g. street, PO Box, or company name)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Line 2',
|
||||
name: 'line2',
|
||||
description: 'Address line 2 (e.g. apartment, suite, unit, or building)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
description: 'City, district, suburb, town, or village',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
description: 'State, county, province, or region',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
description:
|
||||
'Two-letter country code (<a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2">ISO 3166-1 alpha-2</a>)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
name: 'postal_code',
|
||||
description: 'ZIP or postal code',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// charge: get
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Charge ID',
|
||||
name: 'chargeId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the charge to retrieve',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['charge'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// charge: getAll
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['charge'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 50,
|
||||
description: 'Max number of results to return',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 1000,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['charge'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// charge: update
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Charge ID',
|
||||
name: 'chargeId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the charge to update',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['charge'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['charge'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Arbitrary text to describe the charge to update',
|
||||
},
|
||||
{
|
||||
displayName: 'Metadata',
|
||||
name: 'metadata',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
placeholder: 'Add Metadata Item',
|
||||
description: 'Set of key-value pairs to attach to the charge to update',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Metadata Properties',
|
||||
name: 'metadataProperties',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Receipt Email',
|
||||
name: 'receipt_email',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The email address to which the receipt for this charge will be sent',
|
||||
},
|
||||
{
|
||||
displayName: 'Shipping',
|
||||
name: 'shipping',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
description: 'Shipping information for the charge',
|
||||
placeholder: 'Add Field',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Shipping Properties',
|
||||
name: 'shippingProperties',
|
||||
default: {},
|
||||
values: [
|
||||
{
|
||||
displayName: 'Recipient Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Recipient Address',
|
||||
name: 'address',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
placeholder: 'Add Address Details',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Details',
|
||||
name: 'details',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Line 1',
|
||||
name: 'line1',
|
||||
description: 'Address line 1 (e.g. street, PO Box, or company name)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Line 2',
|
||||
name: 'line2',
|
||||
description: 'Address line 2 (e.g. apartment, suite, unit, or building)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
description: 'City, district, suburb, town, or village',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
description: 'State, county, province, or region',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
description:
|
||||
'Two-letter country code (<a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2">ISO 3166-1 alpha-2</a>)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
name: 'postal_code',
|
||||
description: 'ZIP or postal code',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const couponOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'create',
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a coupon',
|
||||
action: 'Create a coupon',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
description: 'Get many coupons',
|
||||
action: 'Get many coupons',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['coupon'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const couponFields: INodeProperties[] = [
|
||||
// ----------------------------------
|
||||
// coupon: create
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Apply',
|
||||
name: 'duration',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: 'once',
|
||||
description: 'How long the discount will be in effect',
|
||||
options: [
|
||||
{
|
||||
name: 'Forever',
|
||||
value: 'forever',
|
||||
},
|
||||
{
|
||||
name: 'Once',
|
||||
value: 'once',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['coupon'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Discount Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: 'percent',
|
||||
description: 'Whether the coupon discount is a percentage or a fixed amount',
|
||||
options: [
|
||||
{
|
||||
name: 'Fixed Amount (in Cents)',
|
||||
value: 'fixedAmount',
|
||||
},
|
||||
{
|
||||
name: 'Percent',
|
||||
value: 'percent',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['coupon'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Amount Off',
|
||||
name: 'amountOff',
|
||||
type: 'number',
|
||||
required: true,
|
||||
default: 0,
|
||||
description:
|
||||
'Amount in cents to subtract from an invoice total, e.g. enter <code>100</code> for $1.00',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 99999999,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['coupon'],
|
||||
operation: ['create'],
|
||||
type: ['fixedAmount'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Currency Name or ID',
|
||||
name: 'currency',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getCurrencies',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description:
|
||||
'Three-letter ISO currency code, e.g. <code>USD</code> or <code>EUR</code>. It must be a <a href="https://stripe.com/docs/currencies">Stripe-supported currency</a>. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['coupon'],
|
||||
operation: ['create'],
|
||||
type: ['fixedAmount'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Percent Off',
|
||||
name: 'percentOff',
|
||||
type: 'number',
|
||||
required: true,
|
||||
default: 1,
|
||||
description: 'Percentage to apply with the coupon',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['coupon'],
|
||||
operation: ['create'],
|
||||
type: ['percent'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// coupon: getAll
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['coupon'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 50,
|
||||
description: 'Max number of results to return',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 1000,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['coupon'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const customerCardOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'get',
|
||||
options: [
|
||||
{
|
||||
name: 'Add',
|
||||
value: 'add',
|
||||
description: 'Add a customer card',
|
||||
action: 'Add a customer card',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a customer card',
|
||||
action: 'Get a customer card',
|
||||
},
|
||||
{
|
||||
name: 'Remove',
|
||||
value: 'remove',
|
||||
description: 'Remove a customer card',
|
||||
action: 'Remove a customer card',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customerCard'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const customerCardFields: INodeProperties[] = [
|
||||
// ----------------------------------
|
||||
// customerCard: add
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Customer ID',
|
||||
name: 'customerId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the customer to be associated with this card',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customerCard'],
|
||||
operation: ['add'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Card Token',
|
||||
name: 'token',
|
||||
type: 'string',
|
||||
typeOptions: { password: true },
|
||||
required: true,
|
||||
default: '',
|
||||
placeholder: 'tok_1IMfKdJhRTnqS5TKQVG1LI9o',
|
||||
description: 'Token representing sensitive card information',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customerCard'],
|
||||
operation: ['add'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// customerCard: remove
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Customer ID',
|
||||
name: 'customerId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the customer whose card to remove',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customerCard'],
|
||||
operation: ['remove'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Card ID',
|
||||
name: 'cardId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the card to remove',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customerCard'],
|
||||
operation: ['remove'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// customerCard: get
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Customer ID',
|
||||
name: 'customerId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the customer whose card to retrieve',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customerCard'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Source ID',
|
||||
name: 'sourceId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the source to retrieve',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customerCard'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,621 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const customerOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'get',
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a customer',
|
||||
action: 'Create a customer',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a customer',
|
||||
action: 'Delete a customer',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a customer',
|
||||
action: 'Get a customer',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
description: 'Get many customers',
|
||||
action: 'Get many customers',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update a customer',
|
||||
action: 'Update a customer',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customer'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const customerFields: INodeProperties[] = [
|
||||
// ----------------------------------
|
||||
// customer: create
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Full name or business name of the customer to create',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customer'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customer'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'address',
|
||||
type: 'fixedCollection',
|
||||
description: 'Address of the customer to create',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Details',
|
||||
name: 'details',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Line 1',
|
||||
name: 'line1',
|
||||
description: 'Address line 1 (e.g. street, PO Box, or company name)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Line 2',
|
||||
name: 'line2',
|
||||
description: 'Address line 2 (e.g. apartment, suite, unit, or building)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
description: 'City, district, suburb, town, or village',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
description: 'State, county, province, or region',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
description:
|
||||
'Two-letter country code (<a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2">ISO 3166-1 alpha-2</a>)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
name: 'postal_code',
|
||||
description: 'ZIP or postal code',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Arbitrary text to describe the customer to create',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
default: '',
|
||||
description: 'Email of the customer to create',
|
||||
},
|
||||
{
|
||||
displayName: 'Metadata',
|
||||
name: 'metadata',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
placeholder: 'Add Metadata Item',
|
||||
description: 'Set of key-value pairs to attach to the customer to create',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Metadata Properties',
|
||||
name: 'metadataProperties',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Telephone number of the customer to create',
|
||||
},
|
||||
{
|
||||
displayName: 'Shipping',
|
||||
name: 'shipping',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
description: 'Shipping information for the customer',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Field',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Shipping Properties',
|
||||
name: 'shippingProperties',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Recipient Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Recipient Address',
|
||||
name: 'address',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
placeholder: 'Add Address Details',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Details',
|
||||
name: 'details',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Line 1',
|
||||
name: 'line1',
|
||||
description: 'Address line 1 (e.g. street, PO Box, or company name)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Line 2',
|
||||
name: 'line2',
|
||||
description: 'Address line 2 (e.g. apartment, suite, unit, or building)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
description: 'City, district, suburb, town, or village',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
description: 'State, county, province, or region',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
description:
|
||||
'Two-letter country code (<a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2">ISO 3166-1 alpha-2</a>)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
name: 'postal_code',
|
||||
description: 'ZIP or postal code',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Recipient Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// customer: delete
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Customer ID',
|
||||
name: 'customerId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the customer to delete',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customer'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// customer: get
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Customer ID',
|
||||
name: 'customerId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the customer to retrieve',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customer'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// customer: getAll
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customer'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 50,
|
||||
description: 'Max number of results to return',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 1000,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customer'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Filter',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customer'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
default: '',
|
||||
description: "Customer's email to filter by",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// customer: update
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Customer ID',
|
||||
name: 'customerId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the customer to update',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customer'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['customer'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'address',
|
||||
type: 'fixedCollection',
|
||||
description: 'Address of the customer to update',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Details',
|
||||
name: 'details',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Line 1',
|
||||
name: 'line1',
|
||||
description: 'Address line 1 (e.g. street, PO Box, or company name)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Line 2',
|
||||
name: 'line2',
|
||||
description: 'Address line 2 (e.g. apartment, suite, unit, or building)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
description: 'City, district, suburb, town, or village',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
description: 'State, county, province, or region',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
description:
|
||||
'Two-letter country code (<a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2">ISO 3166-1 alpha-2</a>)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
name: 'postal_code',
|
||||
description: 'ZIP or postal code',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Arbitrary text to describe the customer to create',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
default: '',
|
||||
description: 'Email of the customer to create',
|
||||
},
|
||||
{
|
||||
displayName: 'Metadata',
|
||||
name: 'metadata',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
placeholder: 'Add Metadata Item',
|
||||
description: 'Set of key-value pairs to attach to the customer to create',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Metadata Properties',
|
||||
name: 'metadataProperties',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Full name or business name of the customer to create',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Telephone number of this customer',
|
||||
},
|
||||
{
|
||||
displayName: 'Shipping',
|
||||
name: 'shipping',
|
||||
type: 'fixedCollection',
|
||||
description: 'Shipping information for the customer',
|
||||
placeholder: 'Add Field',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Shipping Properties',
|
||||
name: 'shippingProperties',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Recipient Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the person who will receive the shipment',
|
||||
},
|
||||
{
|
||||
displayName: 'Recipient Address',
|
||||
name: 'address',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
placeholder: 'Add Address Details',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Details',
|
||||
name: 'details',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Line 1',
|
||||
name: 'line1',
|
||||
description: 'Address line 1 (e.g. street, PO Box, or company name)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Line 2',
|
||||
name: 'line2',
|
||||
description: 'Address line 2 (e.g. apartment, suite, unit, or building)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
description: 'City, district, suburb, town, or village',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
description: 'State, county, province, or region',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
description:
|
||||
'Two-letter country code (<a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2">ISO 3166-1 alpha-2</a>)',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
name: 'postal_code',
|
||||
description: 'ZIP or postal code',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Recipient Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Phone number of the person who will receive the shipment',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const meterEventOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'create',
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a meter event',
|
||||
action: 'Create a meter event',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['meterEvent'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const meterEventFields: INodeProperties[] = [
|
||||
// ----------------------------------
|
||||
// meterEvent: create
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Event Name',
|
||||
name: 'eventName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'The name of the meter event. Corresponds with the event_name field on a meter.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['meterEvent'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Customer ID',
|
||||
name: 'customerId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'The Stripe customer ID associated with this meter event',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['meterEvent'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'number',
|
||||
required: true,
|
||||
default: 1,
|
||||
description: 'The value of the meter event. Must be an integer. Can be positive or negative.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['meterEvent'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['meterEvent'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Identifier',
|
||||
name: 'identifier',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'A unique identifier for the event. If not provided, one will be generated. Uniqueness is enforced within a rolling 24 hour window.',
|
||||
},
|
||||
{
|
||||
displayName: 'Timestamp',
|
||||
name: 'timestamp',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description:
|
||||
'The time of the event. Measured in seconds since the Unix epoch. Must be within the past 35 calendar days or up to 5 minutes in the future. Defaults to current time if not specified.',
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Payload Properties',
|
||||
name: 'customPayload',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
placeholder: 'Add Custom Property',
|
||||
description:
|
||||
'Additional custom properties to include in the event payload. Use this for custom meter configurations with non-default payload keys.',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Properties',
|
||||
name: 'properties',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The property key',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The property value',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,214 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const sourceOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'get',
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a source',
|
||||
action: 'Create a source',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a source',
|
||||
action: 'Delete a source',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a source',
|
||||
action: 'Get a source',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['source'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const sourceFields: INodeProperties[] = [
|
||||
// ----------------------------------
|
||||
// source: create
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Customer ID',
|
||||
name: 'customerId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the customer to attach the source to',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['source'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: 'wechat',
|
||||
description: 'Type of source (payment instrument) to create',
|
||||
options: [
|
||||
{
|
||||
name: 'WeChat',
|
||||
value: 'wechat',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['source'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Amount',
|
||||
name: 'amount',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description:
|
||||
'Amount in cents to be collected for this charge, e.g. enter <code>100</code> for $1.00',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 99999999,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['source'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Currency Name or ID',
|
||||
name: 'currency',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getCurrencies',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Three-letter ISO currency code, e.g. <code>USD</code> or <code>EUR</code>. It must be a <a href="https://stripe.com/docs/currencies">Stripe-supported currency</a>. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['source'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['source'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Metadata',
|
||||
name: 'metadata',
|
||||
type: 'fixedCollection',
|
||||
placeholder: 'Add Metadata Item',
|
||||
description: 'Set of key-value pairs to attach to the source to create',
|
||||
default: {},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Metadata Properties',
|
||||
name: 'metadataProperties',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Statement Descriptor',
|
||||
name: 'statement_descriptor',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: "Arbitrary text to display on the customer's statement",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// source: delete
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Customer ID',
|
||||
name: 'customerId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the customer whose source to delete',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['source'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Source ID',
|
||||
name: 'sourceId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the source to delete',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['source'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// source: get
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Source ID',
|
||||
name: 'sourceId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'ID of the source to retrieve',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['source'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const tokenOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'create',
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a token',
|
||||
action: 'Create a token',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['token'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const tokenFields: INodeProperties[] = [
|
||||
// ----------------------------------
|
||||
// token: create
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: 'cardToken',
|
||||
description: 'Type of token to create',
|
||||
options: [
|
||||
{
|
||||
name: 'Card Token',
|
||||
value: 'cardToken',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['token'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Card Number',
|
||||
name: 'number',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['token'],
|
||||
operation: ['create'],
|
||||
type: ['cardToken'],
|
||||
},
|
||||
},
|
||||
placeholder: '4242424242424242',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'CVC',
|
||||
name: 'cvc',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['token'],
|
||||
operation: ['create'],
|
||||
type: ['cardToken'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: '314',
|
||||
description: 'Security code printed on the back of the card',
|
||||
},
|
||||
{
|
||||
displayName: 'Expiration Month',
|
||||
description: 'Number of the month when the card will expire',
|
||||
name: 'expirationMonth',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['token'],
|
||||
operation: ['create'],
|
||||
type: ['cardToken'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: '10',
|
||||
},
|
||||
{
|
||||
displayName: 'Expiration Year',
|
||||
description: 'Year when the card will expire',
|
||||
name: 'expirationYear',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['token'],
|
||||
operation: ['create'],
|
||||
type: ['cardToken'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: '2022',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './BalanceDescription';
|
||||
export * from './CustomerCardDescription';
|
||||
export * from './ChargeDescription';
|
||||
export * from './CouponDescription';
|
||||
export * from './CustomerDescription';
|
||||
export * from './MeterEventDescription';
|
||||
export * from './SourceDescription';
|
||||
export * from './TokenDescription';
|
||||
@@ -0,0 +1,143 @@
|
||||
import flow from 'lodash/flow';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import omit from 'lodash/omit';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodePropertyOptions,
|
||||
IHttpRequestMethods,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* Make an API request to Stripe
|
||||
*
|
||||
*/
|
||||
export async function stripeApiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
body: IDataObject,
|
||||
query?: IDataObject,
|
||||
) {
|
||||
const options = {
|
||||
method,
|
||||
form: body,
|
||||
qs: query,
|
||||
uri: `https://api.stripe.com/v1${endpoint}`,
|
||||
json: true,
|
||||
} satisfies IRequestOptions;
|
||||
|
||||
if (options.qs && Object.keys(options.qs).length === 0) {
|
||||
delete options.qs;
|
||||
}
|
||||
|
||||
return await this.helpers.requestWithAuthentication.call(this, 'stripeApi', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert n8n's address object into a Stripe API request shipping object.
|
||||
*/
|
||||
function adjustAddress(addressFields: { address: { details: IDataObject } }) {
|
||||
if (!addressFields.address) return addressFields;
|
||||
|
||||
return {
|
||||
...omit(addressFields, ['address']),
|
||||
address: addressFields.address.details,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert n8n's `fixedCollection` metadata object into a Stripe API request metadata object.
|
||||
*/
|
||||
export function adjustMetadata(fields: {
|
||||
metadata?: { metadataProperties: Array<{ key: string; value: string }> };
|
||||
}) {
|
||||
if (!fields.metadata || isEmpty(fields.metadata)) return fields;
|
||||
|
||||
const adjustedMetadata: Record<string, string> = {};
|
||||
|
||||
fields.metadata.metadataProperties.forEach((pair) => {
|
||||
adjustedMetadata[pair.key] = pair.value;
|
||||
});
|
||||
|
||||
return {
|
||||
...omit(fields, ['metadata']),
|
||||
metadata: adjustedMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert n8n's shipping object into a Stripe API request shipping object.
|
||||
*/
|
||||
function adjustShipping(shippingFields: {
|
||||
shipping?: { shippingProperties: Array<{ address: { details: IDataObject }; name: string }> };
|
||||
}) {
|
||||
const shippingProperties = shippingFields.shipping?.shippingProperties[0];
|
||||
|
||||
if (!shippingProperties?.address || isEmpty(shippingProperties.address)) return shippingFields;
|
||||
|
||||
return {
|
||||
...omit(shippingFields, ['shipping']),
|
||||
shipping: {
|
||||
...omit(shippingProperties, ['address']),
|
||||
address: shippingProperties.address.details,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Make n8n's charge fields compliant with the Stripe API request object.
|
||||
*/
|
||||
export const adjustChargeFields = flow([adjustShipping, adjustMetadata]);
|
||||
|
||||
/**
|
||||
* Make n8n's customer fields compliant with the Stripe API request object.
|
||||
*/
|
||||
export const adjustCustomerFields = flow([adjustShipping, adjustAddress, adjustMetadata]);
|
||||
|
||||
/**
|
||||
* Load a resource so it can be selected by name from a dropdown.
|
||||
*/
|
||||
export async function loadResource(
|
||||
this: ILoadOptionsFunctions,
|
||||
resource: 'charge' | 'customer' | 'source',
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const responseData = await stripeApiRequest.call(this, 'GET', `/${resource}s`, {}, {});
|
||||
|
||||
return responseData.data.map(({ name, id }: { name: string; id: string }) => ({
|
||||
name,
|
||||
value: id,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Stripe listing by returning all items or up to a limit.
|
||||
*/
|
||||
export async function handleListing(
|
||||
this: IExecuteFunctions,
|
||||
resource: string,
|
||||
i: number,
|
||||
qs: IDataObject = {},
|
||||
) {
|
||||
const returnData: IDataObject[] = [];
|
||||
let responseData;
|
||||
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
const limit = this.getNodeParameter('limit', i, 0);
|
||||
|
||||
do {
|
||||
responseData = await stripeApiRequest.call(this, 'GET', `/${resource}s`, {}, qs);
|
||||
returnData.push(...(responseData.data as IDataObject[]));
|
||||
|
||||
if (!returnAll && returnData.length >= limit) {
|
||||
return returnData.slice(0, limit);
|
||||
}
|
||||
|
||||
qs.starting_after = returnData[returnData.length - 1].id;
|
||||
} while (responseData.has_more);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="54 -80 360 360"><path d="M414 113.4c0-25.6-12.4-45.8-36.1-45.8-23.8 0-38.2 20.2-38.2 45.6 0 30.1 17 45.3 41.4 45.3 11.9 0 20.9-2.7 27.7-6.5v-20c-6.8 3.4-14.6 5.5-24.5 5.5-9.7 0-18.3-3.4-19.4-15.2h48.9c0-1.3.2-6.5.2-8.9m-49.4-9.5c0-11.3 6.9-16 13.2-16 6.1 0 12.6 4.7 12.6 16zm-63.5-36.3c-9.8 0-16.1 4.6-19.6 7.8l-1.3-6.2h-22v116.6l25-5.3.1-28.3c3.6 2.6 8.9 6.3 17.7 6.3 17.9 0 34.2-14.4 34.2-46.1-.1-29-16.6-44.8-34.1-44.8m-6 68.9c-5.9 0-9.4-2.1-11.8-4.7l-.1-37.1c2.6-2.9 6.2-4.9 11.9-4.9 9.1 0 15.4 10.2 15.4 23.3 0 13.4-6.2 23.4-15.4 23.4m-71.3-74.8 25.1-5.4V36l-25.1 5.3zm0 7.6h25.1v87.5h-25.1zm-26.9 7.4-1.6-7.4h-21.6v87.5h25V97.5c5.9-7.7 15.9-6.3 19-5.2v-23c-3.2-1.2-14.9-3.4-20.8 7.4m-50-29.1-24.4 5.2-.1 80.1c0 14.8 11.1 25.7 25.9 25.7 8.2 0 14.2-1.5 17.5-3.3V135c-3.2 1.3-19 5.9-19-8.9V90.6h19V69.3h-19zM79.3 94.7c0-3.9 3.2-5.4 8.5-5.4 7.6 0 17.2 2.3 24.8 6.4V72.2c-8.3-3.3-16.5-4.6-24.8-4.6C67.5 67.6 54 78.2 54 95.9c0 27.6 38 23.2 38 35.1 0 4.6-4 6.1-9.6 6.1-8.3 0-18.9-3.4-27.3-8v23.8c9.3 4 18.7 5.7 27.3 5.7 20.8 0 35.1-10.3 35.1-28.2-.1-29.8-38.2-24.5-38.2-35.7" style="fill-rule:evenodd;clip-rule:evenodd;fill:#635bff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Reference in New Issue
Block a user