mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-01 17:27:14 +02:00
feat(Stripe Node): Add meter events for billing (#21962)
This commit is contained in:
parent
2b03b5bcf2
commit
5319bc8659
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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,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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user