feat(Stripe Node): Add meter events for billing (#21962)

This commit is contained in:
Jason Schell 2025-11-18 17:23:43 +04:00 committed by GitHub
parent 2b03b5bcf2
commit 5319bc8659
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 723 additions and 0 deletions

View File

@ -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()) {

View File

@ -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 });
});

View File

@ -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
}
}
]
}
}

View File

@ -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
}
}
]
}
}

View File

@ -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
}
}
]
}
}

View File

@ -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
}
}
]
}
}

View File

@ -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
}
}
]
}
}

View File

@ -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
}
}
]
}
}

View File

@ -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',
},
],
},
],
},
],
},
];

View File

@ -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';