n8n-nodes-rd-station-crm
Version:
n8n community node for RD Station CRM: API v1 (private token) and API v2 (OAuth2) — contacts, deals, organizations, tasks, notes, pipelines, stages, products, custom fields, sources, campaigns, loss reasons, users, teams and a real webhook trigger.
667 lines • 27.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.dealDescription = void 0;
exports.executeDeal = executeDeal;
const transport_1 = require("../../transport");
// ---------------------------------------------------------------------------
// DEAL RESOURCE — mirrors the structure of the Contact template:
// 1. `dealDescription` = operations dropdown + fields (INodeProperties[])
// 2. `executeDeal(operation, i)` = programmatic dispatch using the transport helpers
// Keep displayNames Title Case, descriptions/actions Sentence case,
// options alphabetically sorted, loadOptions fields suffixed "Name or ID".
//
// Body-shape note (verified against developers.rdstation.com):
// CREATE POST /deals -> deal_stage_id goes INSIDE `deal`; deal_source/campaign/
// organization are TOP-LEVEL siblings ({ _id }).
// UPDATE PUT /deals/id -> deal_stage_id goes as a TOP-LEVEL sibling (NOT inside deal).
// ---------------------------------------------------------------------------
const showOnlyForDeals = {
resource: ['deal'],
};
const loadOptionsDescription = 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>';
exports.dealDescription = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: { show: showOnlyForDeals },
options: [
{
name: 'Create',
value: 'create',
action: 'Create a deal',
description: 'Create a new deal (sales opportunity) in RD Station CRM and return the created deal with its ID',
},
{
name: 'Get',
value: 'get',
action: 'Get a deal',
description: 'Retrieve a single deal from RD Station CRM by its unique deal ID, returning all deal fields',
},
{
name: 'Get Contacts',
value: 'getContacts',
action: 'Get contacts of a deal',
description: 'Retrieve the contacts (people) associated with a specific deal in RD Station CRM',
},
{
name: 'Get Many',
value: 'getMany',
action: 'Get many deals',
description: 'Retrieve a paginated list of deals from RD Station CRM, with optional filters and sorting',
},
{
name: 'Mark as Lost',
value: 'markAsLost',
action: 'Mark a deal as lost',
description: 'Mark a deal as lost in RD Station CRM, recording the loss reason and an optional note',
},
{
name: 'Mark as Won',
value: 'markAsWon',
action: 'Mark a deal as won',
description: 'Mark a deal as won in RD Station CRM, closing the negotiation as a successful sale',
},
{
name: 'Move to Stage',
value: 'moveToStage',
action: 'Move a deal to a stage',
description: 'Move a deal to a different stage of the sales funnel (pipeline) in RD Station CRM',
},
{
name: 'Update',
value: 'update',
action: 'Update a deal',
description: 'Update the fields of an existing deal in RD Station CRM, identified by its deal ID',
},
],
default: 'create',
},
// ----- ID field (get / getContacts / markAsLost / markAsWon / moveToStage / update) -----
{
displayName: 'Deal ID',
name: 'dealId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
...showOnlyForDeals,
operation: ['get', 'getContacts', 'markAsLost', 'markAsWon', 'moveToStage', 'update'],
},
},
description: 'Unique ID of the deal to act on, as returned by a Create or Get Many deal operation',
},
// ----- name (create) -----
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
default: '',
displayOptions: { show: { ...showOnlyForDeals, operation: ['create'] } },
description: 'Name or title of the deal (sales opportunity), for example Website redesign project',
},
// ----- Pipeline (create + moveToStage): scopes the Deal Stage list to one funnel -----
{
displayName: 'Pipeline Name or ID',
name: 'pipelineId',
type: 'options',
default: '',
description: 'Sales funnel (pipeline) whose stages the Deal Stage field lists. ' + loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getPipelines' },
displayOptions: { show: { ...showOnlyForDeals, operation: ['create', 'moveToStage'] } },
},
// ----- Deal Stage (create, optional): filtered by the selected Pipeline -----
{
displayName: 'Deal Stage Name or ID',
name: 'dealStageId',
type: 'options',
default: '',
description: 'Stage of the selected pipeline to place the new deal in; leave empty to use the account default. ' +
loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getStages', loadOptionsDependsOn: ['pipelineId'] },
displayOptions: { show: { ...showOnlyForDeals, operation: ['create'] } },
},
// ----- Deal Stage (moveToStage, required): filtered by the selected Pipeline -----
{
displayName: 'Deal Stage Name or ID',
name: 'dealStageId',
type: 'options',
required: true,
default: '',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getStages', loadOptionsDependsOn: ['pipelineId'] },
displayOptions: { show: { ...showOnlyForDeals, operation: ['moveToStage'] } },
},
// ----- Loss reason / note (markAsLost) -----
{
displayName: 'Deal Lost Reason Name or ID',
name: 'dealLostReasonId',
type: 'options',
required: true,
default: '',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getLossReasons' },
displayOptions: { show: { ...showOnlyForDeals, operation: ['markAsLost'] } },
},
{
displayName: 'Deal Lost Note',
name: 'dealLostNote',
type: 'string',
default: '',
description: 'Optional free-text note describing why the deal was lost',
displayOptions: { show: { ...showOnlyForDeals, operation: ['markAsLost'] } },
},
// ----- Custom Fields (create + update) -----
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
description: 'Custom fields to set on the deal, each pairing a custom field with its value',
type: 'fixedCollection',
typeOptions: { multipleValues: true },
placeholder: 'Add Custom Field',
default: {},
displayOptions: { show: { ...showOnlyForDeals, operation: ['create', 'update'] } },
options: [
{
name: 'field',
displayName: 'Field',
values: [
{
displayName: 'Field Name or ID',
name: 'fieldId',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getDealCustomFields' },
default: '',
},
{
displayName: 'Value',
name: 'value',
description: 'Value to set for the selected custom field, where the format depends on the field type, e.g. a text, number or date',
type: 'string',
default: '',
},
],
},
],
},
// ----- Products (create, optional) -----
{
displayName: 'Products',
name: 'dealProductsUi',
description: 'Product line items to attach to the deal on creation, each with product, quantity and price',
type: 'fixedCollection',
typeOptions: { multipleValues: true },
placeholder: 'Add Product',
default: {},
displayOptions: { show: { ...showOnlyForDeals, operation: ['create'] } },
options: [
{
name: 'product',
displayName: 'Product',
values: [
{
displayName: 'Amount',
name: 'amount',
type: 'number',
default: 1,
description: 'Quantity of this product on the deal, as a number, for example 2',
},
{
displayName: 'Discount',
name: 'discount',
description: 'Discount applied to this product, where interpretation depends on the discount type, e.g. 10',
type: 'number',
default: 0,
},
{
displayName: 'Discount Type',
name: 'discount_type',
description: 'How the discount is interpreted. One of: amount, percent',
type: 'options',
options: [
{ name: 'Amount', value: 'amount' },
{ name: 'Percentage', value: 'percent' },
],
default: 'percent',
},
{
displayName: 'Price',
name: 'price',
description: 'Unit price of the product in the account currency as a decimal number, e.g. 1500.00',
type: 'number',
default: 0,
},
{
displayName: 'Product Name or ID',
name: 'product_id',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getProducts' },
default: '',
},
{
displayName: 'Recurrence',
name: 'recurrence',
description: 'Recurrence value of the product line as a number, e.g. 0',
type: 'number',
default: 0,
},
{
displayName: 'Total',
name: 'total',
description: 'Total value of the product line in the account currency as a decimal number, e.g. 3000.00',
type: 'number',
default: 0,
},
],
},
],
},
// ----- Additional / Update fields -----
{
displayName: 'Additional Fields',
name: 'additionalFields',
description: 'Optional additional deal fields to set when creating the deal',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: { show: { ...showOnlyForDeals, operation: ['create'] } },
options: [
{
displayName: 'Campaign Name or ID',
name: 'campaignId',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getCampaigns' },
default: '',
},
{
displayName: 'Deal Source Name or ID',
name: 'dealSourceId',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getSources' },
default: '',
},
{
displayName: 'Organization Name or ID',
name: 'organizationId',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getOrganizations' },
default: '',
},
{
displayName: 'Prediction Date',
name: 'predictionDate',
type: 'dateTime',
default: '',
description: 'Estimated close date of the deal in ISO 8601 format, e.g. 2026-07-08 or 2026-07-08T14:30:00Z',
},
{
displayName: 'Rating',
name: 'rating',
description: 'Deal rating as an integer score, e.g. 3',
type: 'number',
default: 0,
},
{
displayName: 'User Name or ID',
name: 'userId',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getUsers' },
default: '',
},
],
},
{
displayName: 'Update Fields',
name: 'updateFields',
description: 'Deal fields to update, where only the provided fields are changed',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: { show: { ...showOnlyForDeals, operation: ['update'] } },
options: [
{
displayName: 'Deal Stage Name or ID',
name: 'dealStageId',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getStages' },
default: '',
},
{ displayName: 'Name', name: 'name', type: 'string', default: '', description: 'Updated name or title of the deal, e.g. Website redesign project' },
{
displayName: 'Organization Name or ID',
name: 'organizationId',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getOrganizations' },
default: '',
},
{
displayName: 'Prediction Date',
name: 'predictionDate',
type: 'dateTime',
default: '',
description: 'Estimated close date of the deal in ISO 8601 format, e.g. 2026-07-08 or 2026-07-08T14:30:00Z',
},
{
displayName: 'Rating',
name: 'rating',
description: 'Deal rating as an integer score, e.g. 3',
type: 'number',
default: 0,
},
{
displayName: 'User Name or ID',
name: 'userId',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getUsers' },
default: '',
},
],
},
// ----- Get Many / Get Contacts controls -----
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: { show: { ...showOnlyForDeals, operation: ['getMany', 'getContacts'] } },
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
typeOptions: { minValue: 1 },
description: 'Max number of results to return',
displayOptions: {
show: { ...showOnlyForDeals, operation: ['getMany', 'getContacts'], returnAll: [false] },
},
},
{
displayName: 'Filters',
name: 'filters',
description: 'Optional filters to narrow which deals are returned',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: { show: { ...showOnlyForDeals, operation: ['getMany'] } },
options: [
{
displayName: 'Campaign Name or ID',
name: 'campaign_id',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getCampaigns' },
default: '',
},
{
displayName: 'Closed At',
name: 'closed_at',
type: 'boolean',
default: false,
description: 'Whether to return only deals that are already closed (won or lost)',
},
{
displayName: 'Deal Lost Reason Name or ID',
name: 'deal_lost_reason_id',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getLossReasons' },
default: '',
},
{
displayName: 'Deal Pipeline Name or ID',
name: 'deal_pipeline_id',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getPipelines' },
default: '',
},
{
displayName: 'Deal Stage Name or ID',
name: 'deal_stage_id',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getStages' },
default: '',
},
{
displayName: 'Exact Name',
name: 'exact_name',
type: 'boolean',
default: false,
description: 'Whether to match the Name filter exactly instead of as a partial substring',
},
{
displayName: 'Hold',
name: 'hold',
type: 'boolean',
default: false,
description: 'Whether to return only deals that are paused (on hold)',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Filter deals whose name contains this text, or matches exactly when Exact Name is enabled',
},
{
displayName: 'Organization ID',
name: 'organization',
type: 'string',
default: '',
description: 'Filter deals by the ID of the organization (company) they belong to',
},
{
displayName: 'Product Presence',
name: 'product_presence',
type: 'string',
default: '',
description: 'Filter deals by product presence, using true for any product, false for none, or a comma-separated list of product IDs',
},
{
displayName: 'User Name or ID',
name: 'user_id',
type: 'options',
description: loadOptionsDescription,
typeOptions: { loadOptionsMethod: 'getUsers' },
default: '',
},
{
displayName: 'Win',
name: 'win',
type: 'options',
options: [
{ name: 'All', value: 'all' },
{ name: 'Lost', value: 'false' },
{ name: 'Open', value: 'null' },
{ name: 'Won', value: 'true' },
],
default: 'all',
description: 'Filter deals by outcome. One of: all (any), false (lost), null (open), true (won)',
},
],
},
{
displayName: 'Options',
name: 'options',
description: 'Additional options that control how the returned deals are sorted',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: { show: { ...showOnlyForDeals, operation: ['getMany'] } },
options: [
{
displayName: 'Sort By',
name: 'order',
description: 'Field to sort the deals by. One of: created_at, name, prediction_date, rating, updated_at',
type: 'options',
options: [
{ name: 'Created At', value: 'created_at' },
{ name: 'Name', value: 'name' },
{ name: 'Prediction Date', value: 'prediction_date' },
{ name: 'Rating', value: 'rating' },
{ name: 'Updated At', value: 'updated_at' },
],
default: 'created_at',
},
{
displayName: 'Sort Direction',
name: 'direction',
description: 'Direction to sort the deals. One of: asc, desc',
type: 'options',
options: [
{ name: 'Ascending', value: 'asc' },
{ name: 'Descending', value: 'desc' },
],
default: 'desc',
},
],
},
];
function readDealCustomFields(i) {
const customFieldsUi = this.getNodeParameter('customFieldsUi', i, {});
if (Array.isArray(customFieldsUi.field)) {
return customFieldsUi.field.map((f) => ({
custom_field_id: f.fieldId,
value: f.value,
}));
}
return undefined;
}
async function executeDeal(operation, i) {
if (operation === 'create') {
const name = this.getNodeParameter('name', i);
const dealStageId = this.getNodeParameter('dealStageId', i, '');
const additionalFields = this.getNodeParameter('additionalFields', i, {});
const deal = { name };
// pipelineId is a UI-only filter for the stage list; RD infers the pipeline from the stage.
if (dealStageId)
deal.deal_stage_id = dealStageId;
if (additionalFields.userId)
deal.user_id = additionalFields.userId;
if (additionalFields.rating !== undefined && additionalFields.rating !== '') {
deal.rating = additionalFields.rating;
}
if (additionalFields.predictionDate)
deal.prediction_date = additionalFields.predictionDate;
const dealCustomFields = readDealCustomFields.call(this, i);
if (dealCustomFields)
deal.deal_custom_fields = dealCustomFields;
const body = { deal };
if (additionalFields.dealSourceId)
body.deal_source = { _id: additionalFields.dealSourceId };
if (additionalFields.campaignId)
body.campaign = { _id: additionalFields.campaignId };
if (additionalFields.organizationId)
body.organization = { _id: additionalFields.organizationId };
const dealProductsUi = this.getNodeParameter('dealProductsUi', i, {});
if (Array.isArray(dealProductsUi.product)) {
body.deal_products = dealProductsUi.product.map((p) => ({
product_id: p.product_id,
amount: p.amount,
price: p.price,
discount: p.discount,
discount_type: p.discount_type,
recurrence: p.recurrence,
total: p.total,
}));
}
return transport_1.rdCrmApiRequest.call(this, 'POST', '/deals', body);
}
if (operation === 'get') {
const dealId = this.getNodeParameter('dealId', i);
return transport_1.rdCrmApiRequest.call(this, 'GET', `/deals/${dealId}`);
}
if (operation === 'getContacts') {
const dealId = this.getNodeParameter('dealId', i);
const returnAll = this.getNodeParameter('returnAll', i);
if (returnAll) {
return transport_1.rdCrmApiRequestAllItems.call(this, 'contacts', 'GET', `/deals/${dealId}/contacts`);
}
const limit = this.getNodeParameter('limit', i);
const response = await transport_1.rdCrmApiRequest.call(this, 'GET', `/deals/${dealId}/contacts`, {}, { limit });
return response.contacts ?? response;
}
if (operation === 'getMany') {
const returnAll = this.getNodeParameter('returnAll', i);
const filters = this.getNodeParameter('filters', i, {});
const options = this.getNodeParameter('options', i, {});
const qs = { ...filters, ...options };
// win: 'all' omits the filter; 'null' sends a real JSON null (open deals),
// 'true'/'false' send real booleans (won/lost).
if (qs.win === undefined || qs.win === 'all') {
delete qs.win;
}
else if (qs.win === 'null') {
qs.win = null;
}
else {
qs.win = qs.win === 'true';
}
if (returnAll) {
return transport_1.rdCrmApiRequestAllItems.call(this, 'deals', 'GET', '/deals', {}, qs);
}
qs.limit = this.getNodeParameter('limit', i);
const response = await transport_1.rdCrmApiRequest.call(this, 'GET', '/deals', {}, qs);
return response.deals ?? response;
}
if (operation === 'update') {
const dealId = this.getNodeParameter('dealId', i);
const updateFields = this.getNodeParameter('updateFields', i, {});
const deal = {};
if (updateFields.name)
deal.name = updateFields.name;
if (updateFields.userId)
deal.user_id = updateFields.userId;
if (updateFields.rating !== undefined && updateFields.rating !== '') {
deal.rating = updateFields.rating;
}
if (updateFields.predictionDate)
deal.prediction_date = updateFields.predictionDate;
if (updateFields.organizationId)
deal.organization_id = updateFields.organizationId;
const dealCustomFields = readDealCustomFields.call(this, i);
if (dealCustomFields)
deal.deal_custom_fields = dealCustomFields;
const body = { deal };
// On UPDATE, deal_stage_id is a TOP-LEVEL sibling (not inside `deal`).
if (updateFields.dealStageId)
body.deal_stage_id = updateFields.dealStageId;
return transport_1.rdCrmApiRequest.call(this, 'PUT', `/deals/${dealId}`, body);
}
if (operation === 'markAsWon') {
const dealId = this.getNodeParameter('dealId', i);
return transport_1.rdCrmApiRequest.call(this, 'PUT', `/deals/${dealId}`, { deal: { win: true } });
}
if (operation === 'markAsLost') {
const dealId = this.getNodeParameter('dealId', i);
const dealLostReasonId = this.getNodeParameter('dealLostReasonId', i);
const dealLostNote = this.getNodeParameter('dealLostNote', i, '');
const deal = { win: false, deal_lost_reason_id: dealLostReasonId };
if (dealLostNote)
deal.deal_lost_note = dealLostNote;
return transport_1.rdCrmApiRequest.call(this, 'PUT', `/deals/${dealId}`, { deal });
}
if (operation === 'moveToStage') {
const dealId = this.getNodeParameter('dealId', i);
const dealStageId = this.getNodeParameter('dealStageId', i);
// deal_stage_id is a TOP-LEVEL sibling (no `deal` wrapper).
return transport_1.rdCrmApiRequest.call(this, 'PUT', `/deals/${dealId}`, { deal_stage_id: dealStageId });
}
return {};
}
//# sourceMappingURL=Deal.js.map