n8n-nodes-plaid
Version:
n8n community node for Plaid financial data integration - the definitive financial node for n8n
171 lines (170 loc) • 6.38 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlaidHelpers = void 0;
exports.createLinkToken = createLinkToken;
exports.exchangePublicToken = exchangePublicToken;
exports.getAccessToken = getAccessToken;
exports.requiresAccessToken = requiresAccessToken;
const plaid_1 = require("plaid");
class PlaidHelpers {
/**
* Get Plaid environment URL
*/
static getEnvironmentUrl(environment) {
return environment === 'production'
? plaid_1.PlaidEnvironments.production
: plaid_1.PlaidEnvironments.sandbox;
}
/**
* Format transaction amount (Plaid returns negative for outflow)
*/
static formatTransactionAmount(amount) {
return {
amount: Math.abs(amount),
type: amount < 0 ? 'expense' : 'income',
};
}
/**
* Parse account IDs from comma-separated string
*/
static parseAccountIds(accountIds) {
if (!accountIds.trim())
return undefined;
return accountIds.split(',').map(id => id.trim()).filter(Boolean);
}
/**
* Format date for Plaid API (YYYY-MM-DD)
*/
static formatDate(dateString) {
return dateString.split('T')[0];
}
/**
* Enhanced categorization mapping
*/
static enhanceCategories(transaction) {
const categories = transaction.category || [];
const personalFinanceCategory = transaction.personal_finance_category;
return {
...transaction,
category_primary: categories[0] || 'Other',
category_secondary: categories[1] || '',
category_detailed: categories[2] || '',
category_full: categories.join(' > ') || 'Other',
enhanced_category: personalFinanceCategory?.primary || categories[0] || 'Other',
enhanced_subcategory: personalFinanceCategory?.detailed || categories[1] || '',
};
}
/**
* Detect recurring transactions
*/
static detectRecurring(description) {
const recurringKeywords = [
'netflix', 'spotify', 'subscription', 'monthly', 'annual',
'insurance', 'mortgage', 'rent', 'gym', 'membership',
'utilities', 'phone', 'internet', 'recurring'
];
const lowerDesc = description.toLowerCase();
return recurringKeywords.some(keyword => lowerDesc.includes(keyword));
}
/**
* Calculate spending score based on amount and category
*/
static calculateSpendingScore(amount, categories) {
const absAmount = Math.abs(amount);
let score = Math.min(absAmount / 100, 10); // Base score 0-10
// Adjust based on category
const categoryMultipliers = {
'Food and Drink': 1.0,
'Shops': 1.2,
'Recreation': 1.3,
'Transportation': 0.8,
'Healthcare': 0.7,
'Bills': 0.5,
'Transfer': 0.3,
};
const primaryCategory = categories?.[0];
const multiplier = categoryMultipliers[primaryCategory] || 1.0;
return Math.round(score * multiplier * 10) / 10;
}
}
exports.PlaidHelpers = PlaidHelpers;
/**
* Create a Link token for Plaid Link initialization
*/
async function createLinkToken(client, credentials, userId = 'default_user', products = ['transactions', 'auth']) {
const countryCodes = credentials.countryCodes
? credentials.countryCodes.split(',').map((code) => code.trim())
: ['US'];
// Convert string products to Products enum
const plaidProducts = products.map(product => {
switch (product.toLowerCase()) {
case 'transactions': return plaid_1.Products.Transactions;
case 'auth': return plaid_1.Products.Auth;
case 'identity': return plaid_1.Products.Identity;
case 'assets': return plaid_1.Products.Assets;
case 'investments': return plaid_1.Products.Investments;
case 'liabilities': return plaid_1.Products.Liabilities;
default: return plaid_1.Products.Transactions;
}
});
const request = {
client_name: credentials.clientName || 'n8n Plaid Integration',
country_codes: countryCodes,
language: credentials.language || 'en',
user: {
client_user_id: userId,
},
products: plaidProducts,
};
const response = await client.linkTokenCreate(request);
return response.data.link_token;
}
/**
* Exchange a public token for an access token
*/
async function exchangePublicToken(client, publicToken) {
const request = {
public_token: publicToken,
};
const response = await client.itemPublicTokenExchange(request);
return {
accessToken: response.data.access_token,
itemId: response.data.item_id,
};
}
/**
* Get the appropriate access token based on authentication method
*/
async function getAccessToken(client, credentials) {
const authMethod = credentials.authMethod || 'accessToken';
switch (authMethod) {
case 'accessToken':
if (!credentials.accessToken) {
throw new Error('Access token is required for legacy authentication method');
}
return credentials.accessToken;
case 'publicToken':
if (!credentials.publicToken) {
throw new Error('Public token is required for public token exchange method');
}
const { accessToken } = await exchangePublicToken(client, credentials.publicToken);
return accessToken;
case 'clientOnly':
throw new Error('Access token not available for client-only authentication method');
default:
throw new Error(`Unknown authentication method: ${authMethod}`);
}
}
/**
* Check if operation requires access token
*/
function requiresAccessToken(resource, operation) {
// Operations that don't require access token (only client credentials)
const noTokenOperations = [
{ resource: 'link', operation: 'createToken' },
{ resource: 'link', operation: 'exchangeToken' },
{ resource: 'institution', operation: 'search' },
{ resource: 'institution', operation: 'getById' },
];
return !noTokenOperations.some(op => op.resource === resource && op.operation === operation);
}