From 5319bc8659a1b4c3fa35abb0c7248e7548e97dea Mon Sep 17 00:00:00 2001 From: Jason Schell <44125163+jason-schell@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:23:43 +0400 Subject: [PATCH] feat(Stripe Node): Add meter events for billing (#21962) --- .../nodes-base/nodes/Stripe/Stripe.node.ts | 70 +++++++++ .../__tests__/workflow/MeterEvent.test.ts | 97 +++++++++++++ .../Workflow_Stripe_MeterEvent_Basic.json | 63 ++++++++ ...kflow_Stripe_MeterEvent_CustomPayload.json | 79 ++++++++++ ...low_Stripe_MeterEvent_GuardCustomerId.json | 73 ++++++++++ ...Workflow_Stripe_MeterEvent_GuardValue.json | 73 ++++++++++ ...Workflow_Stripe_MeterEvent_Identifier.json | 67 +++++++++ .../Workflow_Stripe_MeterEvent_Negative.json | 63 ++++++++ .../descriptions/MeterEventDescription.ts | 137 ++++++++++++++++++ .../nodes/Stripe/descriptions/index.ts | 1 + 10 files changed, 723 insertions(+) create mode 100644 packages/nodes-base/nodes/Stripe/__tests__/workflow/MeterEvent.test.ts create mode 100644 packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Basic.json create mode 100644 packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_CustomPayload.json create mode 100644 packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_GuardCustomerId.json create mode 100644 packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_GuardValue.json create mode 100644 packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Identifier.json create mode 100644 packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Negative.json create mode 100644 packages/nodes-base/nodes/Stripe/descriptions/MeterEventDescription.ts diff --git a/packages/nodes-base/nodes/Stripe/Stripe.node.ts b/packages/nodes-base/nodes/Stripe/Stripe.node.ts index 281f97537ea..e925ea8a6af 100644 --- a/packages/nodes-base/nodes/Stripe/Stripe.node.ts +++ b/packages/nodes-base/nodes/Stripe/Stripe.node.ts @@ -20,6 +20,8 @@ import { customerCardOperations, customerFields, customerOperations, + meterEventFields, + meterEventOperations, sourceFields, sourceOperations, tokenFields, @@ -82,6 +84,10 @@ export class Stripe implements INodeType { name: 'Customer Card', value: 'customerCard', }, + { + name: 'Meter Event', + value: 'meterEvent', + }, { name: 'Source', value: 'source', @@ -102,6 +108,8 @@ export class Stripe implements INodeType { ...couponFields, ...customerOperations, ...customerFields, + ...meterEventOperations, + ...meterEventFields, ...sourceOperations, ...sourceFields, ...tokenOperations, @@ -461,6 +469,68 @@ export class Stripe implements INodeType { 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()) { diff --git a/packages/nodes-base/nodes/Stripe/__tests__/workflow/MeterEvent.test.ts b/packages/nodes-base/nodes/Stripe/__tests__/workflow/MeterEvent.test.ts new file mode 100644 index 00000000000..ae57e4591db --- /dev/null +++ b/packages/nodes-base/nodes/Stripe/__tests__/workflow/MeterEvent.test.ts @@ -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 }); +}); diff --git a/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Basic.json b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Basic.json new file mode 100644 index 00000000000..353d7fbe5a9 --- /dev/null +++ b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Basic.json @@ -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 + } + } + ] + } +} diff --git a/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_CustomPayload.json b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_CustomPayload.json new file mode 100644 index 00000000000..6b74cb159a1 --- /dev/null +++ b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_CustomPayload.json @@ -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 + } + } + ] + } +} diff --git a/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_GuardCustomerId.json b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_GuardCustomerId.json new file mode 100644 index 00000000000..0b860d1db4a --- /dev/null +++ b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_GuardCustomerId.json @@ -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 + } + } + ] + } +} diff --git a/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_GuardValue.json b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_GuardValue.json new file mode 100644 index 00000000000..ae7571a7297 --- /dev/null +++ b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_GuardValue.json @@ -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 + } + } + ] + } +} diff --git a/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Identifier.json b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Identifier.json new file mode 100644 index 00000000000..527c2d85551 --- /dev/null +++ b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Identifier.json @@ -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 + } + } + ] + } +} diff --git a/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Negative.json b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Negative.json new file mode 100644 index 00000000000..f6dd28ba8f5 --- /dev/null +++ b/packages/nodes-base/nodes/Stripe/__tests__/workflow/Workflow_Stripe_MeterEvent_Negative.json @@ -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 + } + } + ] + } +} diff --git a/packages/nodes-base/nodes/Stripe/descriptions/MeterEventDescription.ts b/packages/nodes-base/nodes/Stripe/descriptions/MeterEventDescription.ts new file mode 100644 index 00000000000..27898aa4664 --- /dev/null +++ b/packages/nodes-base/nodes/Stripe/descriptions/MeterEventDescription.ts @@ -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', + }, + ], + }, + ], + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Stripe/descriptions/index.ts b/packages/nodes-base/nodes/Stripe/descriptions/index.ts index cadbadefbdd..966b9be410e 100644 --- a/packages/nodes-base/nodes/Stripe/descriptions/index.ts +++ b/packages/nodes-base/nodes/Stripe/descriptions/index.ts @@ -3,5 +3,6 @@ export * from './CustomerCardDescription'; export * from './ChargeDescription'; export * from './CouponDescription'; export * from './CustomerDescription'; +export * from './MeterEventDescription'; export * from './SourceDescription'; export * from './TokenDescription';