n8n-nodes-atendix 1.0.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
- /* Arquivo: n8n-nodes-atendix/credentials/TrayApiAuto.credentials.ts
3
- *
4
- * Credenciais Tray API - Autenticação Automática
5
- * Ajustado para evitar conflitos de nomes e erros de tipagem (unknown).
2
+ /**
3
+ * n8n-nodes-atendix — Credenciais Tray Commerce
4
+ * v1.3.0 CORREÇÃO CRÍTICA:
5
+ * URL: {store_id}.commercesuite.com.br/web_api (api.tray.com.br não existe)
6
+ * consumer_key/secret agora são campos de credencial (não env vars)
6
7
  */
7
8
  Object.defineProperty(exports, "__esModule", { value: true });
8
9
  exports.TrayApiAuto = void 0;
9
- const CONSUMER_KEY = process.env.TRAY_CONSUMER_KEY;
10
- const CONSUMER_SECRET = process.env.TRAY_CONSUMER_SECRET;
10
+
11
11
  class TrayApiAuto {
12
12
  constructor() {
13
13
  this.name = 'trayApiAuto';
@@ -19,150 +19,132 @@ class TrayApiAuto {
19
19
  this.documentationUrl = 'https://developers.tray.com.br/docs/';
20
20
  this.properties = [
21
21
  {
22
- displayName: 'API Address',
23
- name: 'apiAddress',
22
+ displayName: 'Store ID',
23
+ name: 'storeId',
24
24
  type: 'string',
25
- default: 'https://api.tray.com.br',
25
+ default: '',
26
26
  required: true,
27
- description: 'Endereço base da API Tray (geralmente https://api.tray.com.br)',
27
+ placeholder: '1225878',
28
+ description: 'ID da sua loja Tray. A URL base (https://{storeId}.commercesuite.com.br/web_api) é gerada automaticamente.',
28
29
  },
29
30
  {
30
- displayName: 'Authorization Code',
31
- name: 'authCode',
31
+ displayName: 'Consumer Key',
32
+ name: 'consumerKey',
32
33
  type: 'string',
33
- typeOptions: {
34
- password: true,
35
- },
36
34
  default: '',
37
35
  required: true,
38
- placeholder: 'a9777f8cdfe4cf41b0a72c295adef1f9c43a093052c9e6dd98d1e629a09f13f3',
39
- description: 'Código de autorização único da loja (fornecido pela Tray)',
36
+ description: 'Consumer Key fornecida pela Tray no painel de apps',
40
37
  },
41
38
  {
42
- displayName: 'Access Token',
43
- name: 'accessToken',
44
- type: 'hidden',
45
- default: '',
46
- },
47
- {
48
- displayName: 'Refresh Token',
49
- name: 'refreshToken',
50
- type: 'hidden',
51
- default: '',
52
- },
53
- {
54
- displayName: 'API Host',
55
- name: 'apiHost',
56
- type: 'hidden',
57
- default: '',
58
- },
59
- {
60
- displayName: 'Token Expiration',
61
- name: 'tokenExpiration',
62
- type: 'hidden',
39
+ displayName: 'Consumer Secret',
40
+ name: 'consumerSecret',
41
+ type: 'string',
42
+ typeOptions: { password: true },
63
43
  default: '',
44
+ required: true,
45
+ description: 'Consumer Secret fornecido pela Tray no painel de apps',
64
46
  },
65
47
  {
66
- displayName: 'Store ID',
67
- name: 'storeId',
68
- type: 'hidden',
48
+ displayName: 'Authorization Code',
49
+ name: 'authCode',
50
+ type: 'string',
51
+ typeOptions: { password: true },
69
52
  default: '',
53
+ required: true,
54
+ placeholder: 'a9777f8cdfe4cf41b0a72c295adef1f9c43a093052c9e6dd98d1e629a09f13f3',
55
+ description: 'Código de autorização único da loja (fornecido pela Tray)',
70
56
  },
57
+ // Campos ocultos — cache interno de token
58
+ { displayName: 'Access Token', name: 'accessToken', type: 'hidden', default: '' },
59
+ { displayName: 'Refresh Token', name: 'refreshToken', type: 'hidden', default: '' },
60
+ { displayName: 'Token Expiration',name: 'tokenExpiration',type: 'hidden', default: '' },
61
+ { displayName: 'API Host', name: 'apiHost', type: 'hidden', default: '' },
71
62
  ];
72
- // O segredo para o n8n injetar o token em todas as chamadas
73
63
  this.authenticate = {
74
64
  type: 'generic',
75
65
  properties: {
76
- qs: {
77
- access_token: '={{$credentials.accessToken}}',
78
- },
66
+ qs: { access_token: '={{$credentials.accessToken}}' },
79
67
  },
80
68
  };
81
69
  this.test = {
82
70
  request: {
83
- // Usamos o apiAddress direto para o teste não quebrar se o apiHost estiver vazio
84
- baseURL: '={{$credentials.apiAddress}}',
71
+ baseURL: '={{`https://${$credentials.storeId}.commercesuite.com.br/web_api`}}',
85
72
  url: '/auth',
86
73
  method: 'POST',
87
74
  body: {
88
- consumer_key: process.env.TRAY_CONSUMER_KEY,
89
- consumer_secret: process.env.TRAY_CONSUMER_SECRET,
90
- code: '={{$credentials.authCode}}',
75
+ consumer_key: '={{$credentials.consumerKey}}',
76
+ consumer_secret: '={{$credentials.consumerSecret}}',
77
+ code: '={{$credentials.authCode}}',
91
78
  },
92
79
  },
93
80
  };
94
81
  }
82
+
83
+ _buildBaseUrl(credentials) {
84
+ const storeId = (credentials.storeId || '').toString().trim();
85
+ if (!storeId) throw new Error('[Atendix] Store ID não configurado nas credenciais');
86
+ // v1.3.0 FIX: URL correta — api.tray.com.br não existe
87
+ return `https://${storeId}.commercesuite.com.br/web_api`;
88
+ }
89
+
95
90
  async preAuthentication(credentials) {
96
91
  const now = new Date();
97
92
  const tokenExpiration = credentials.tokenExpiration
98
93
  ? new Date(credentials.tokenExpiration)
99
94
  : null;
95
+
100
96
  if (credentials.accessToken && tokenExpiration && tokenExpiration > now) {
101
- return credentials;
97
+ return credentials; // Token válido em cache
102
98
  }
103
99
  if (credentials.refreshToken && tokenExpiration && tokenExpiration <= now) {
104
100
  return await this._refreshTokenFlow(credentials);
105
101
  }
106
102
  return await this._authenticateFlow(credentials);
107
103
  }
108
- /**
109
- * Renomeado para evitar conflito com a propriedade 'authenticate' da interface
110
- */
104
+
111
105
  async _authenticateFlow(credentials) {
112
- const apiAddress = credentials.apiAddress.replace(/\/$/, '');
113
- const authCode = credentials.authCode;
114
- // Pega as chaves direto do processo para garantir que não estão vazias
115
- const consumerKey = process.env.TRAY_CONSUMER_KEY;
116
- const consumerSecret = process.env.TRAY_CONSUMER_SECRET;
117
- console.log(` Tentando autenticar na URL: ${apiAddress}/auth`);
118
- console.log(` Usando Key: ${consumerKey ? 'Preenchida' : 'VAZIA!'}`);
119
- try {
120
- const response = await fetch(`${apiAddress}/auth`, {
121
- method: 'POST',
122
- headers: { 'Content-Type': 'application/json' },
123
- body: JSON.stringify({
124
- consumer_key: consumerKey,
125
- consumer_secret: consumerSecret,
126
- code: authCode,
127
- }),
128
- });
129
- const data = (await response.json());
130
- if (!response.ok) {
131
- console.error(' Erro retornado pela Tray:', data);
132
- throw new Error(data.message || 'Erro desconhecido na Tray');
133
- }
134
- console.log(' Autenticação com a Tray teve SUCESSO!');
135
- return {
136
- ...credentials,
137
- accessToken: data.access_token,
138
- refreshToken: data.refresh_token,
139
- apiHost: data.api_host, // Geralmente vem algo como "https://1225878.commercesuite.com.br"
140
- storeId: data.store_id,
141
- tokenExpiration: data.date_expiration_access_token,
142
- };
143
- }
144
- catch (error) {
145
- console.error(' Erro Crítico no Flow de Autenticação:', error.message);
146
- throw new Error(`Falha ao autenticar na Tray: ${error.message}`);
106
+ const baseUrl = this._buildBaseUrl(credentials);
107
+ console.log(`[Atendix v1.3.0] Autenticando em: ${baseUrl}/auth`);
108
+
109
+ const response = await fetch(`${baseUrl}/auth`, {
110
+ method: 'POST',
111
+ headers: { 'Content-Type': 'application/json' },
112
+ body: JSON.stringify({
113
+ consumer_key: credentials.consumerKey,
114
+ consumer_secret: credentials.consumerSecret,
115
+ code: credentials.authCode,
116
+ }),
117
+ });
118
+
119
+ const data = await response.json();
120
+ if (!response.ok) {
121
+ console.error('[Atendix] Erro na autenticação Tray:', data);
122
+ throw new Error(data.message || `Erro HTTP ${response.status} na autenticação Tray`);
147
123
  }
124
+
125
+ console.log('[Atendix v1.3.0] Autenticação OK ✓');
126
+ return {
127
+ ...credentials,
128
+ accessToken: data.access_token,
129
+ refreshToken: data.refresh_token,
130
+ apiHost: data.api_host,
131
+ tokenExpiration: data.date_expiration_access_token,
132
+ };
148
133
  }
134
+
149
135
  async _refreshTokenFlow(credentials) {
150
- const apiAddress = credentials.apiAddress;
151
- const refreshToken = credentials.refreshToken;
136
+ const baseUrl = this._buildBaseUrl(credentials);
152
137
  try {
153
- const response = await fetch(`${apiAddress}/auth?refresh_token=${refreshToken}`);
154
- const data = (await response.json());
155
- if (!response.ok) {
156
- return await this._authenticateFlow(credentials);
157
- }
138
+ const response = await fetch(`${baseUrl}/auth?refresh_token=${credentials.refreshToken}`);
139
+ const data = await response.json();
140
+ if (!response.ok) return await this._authenticateFlow(credentials);
158
141
  return {
159
142
  ...credentials,
160
- accessToken: data.access_token,
161
- refreshToken: data.refresh_token,
143
+ accessToken: data.access_token,
144
+ refreshToken: data.refresh_token,
162
145
  tokenExpiration: data.date_expiration_access_token,
163
146
  };
164
- }
165
- catch (error) {
147
+ } catch {
166
148
  return await this._authenticateFlow(credentials);
167
149
  }
168
150
  }
@@ -1,335 +1,379 @@
1
1
  "use strict";
2
2
  /**
3
- * Arquivo: n8n-nodes-atendix/nodes/Atendix/Atendix.node.ts
4
- *
5
- * Node oficial Atendix - Tray Commerce
6
- * Versão 1.2.0 - Licenciamento SaaS + Full Operations
3
+ * n8n-nodes-atendix — Node Atendix Tray Commerce
4
+ * v1.3.0 — Correções críticas de homologação:
5
+ * - URL: {store_id}.commercesuite.com.br/web_api
6
+ * - Endpoints NF: GET /orders/:id/invoices (não /invoices?order_id=)
7
+ * - Recursos: Pedido, Nota Fiscal, Cliente, Rastreamento, Produto
7
8
  */
8
9
  Object.defineProperty(exports, "__esModule", { value: true });
9
10
  exports.Atendix = void 0;
10
11
  const n8n_workflow_1 = require("n8n-workflow");
12
+
11
13
  const wait = (ms) => new Promise(res => setTimeout(res, ms));
14
+
12
15
  class Atendix {
13
16
  constructor() {
14
17
  this.description = {
15
18
  displayName: 'Atendix - Tray API',
16
19
  name: 'atendix',
17
- icon: {
18
- light: 'file:tray.svg',
19
- dark: 'file:tray.svg',
20
- },
20
+ icon: { light: 'file:tray.svg', dark: 'file:tray.svg' },
21
21
  group: ['transform'],
22
22
  version: 1,
23
23
  subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
24
- description: 'Integração com a plataforma Tray Commerce',
25
- defaults: {
26
- name: 'Atendix - Tray API',
27
- },
24
+ description: 'Integração nativa com a Tray Commerce — v1.3.0',
25
+ defaults: { name: 'Atendix - Tray API' },
28
26
  inputs: ['main'],
29
27
  outputs: ['main'],
30
- credentials: [
31
- {
32
- name: 'trayApiAuto',
33
- required: true,
34
- displayName: 'Atendix - Tray API',
35
- },
36
- ],
28
+ credentials: [{ name: 'trayApiAuto', required: true, displayName: 'Atendix - Tray API' }],
37
29
  properties: [
38
- // ==================== RESOURCE SELECTOR ====================
30
+ // ===== RESOURCE =====
39
31
  {
40
- displayName: 'Resource',
41
- name: 'resource',
42
- type: 'options',
32
+ displayName: 'Resource', name: 'resource', type: 'options',
43
33
  noDataExpression: true,
44
34
  options: [
45
- { name: 'Pedido', value: 'order' },
46
- { name: 'Cliente', value: 'customer' },
47
- { name: 'Produto', value: 'product' },
35
+ { name: 'Pedido', value: 'order' },
36
+ { name: 'Nota Fiscal', value: 'invoice' },
37
+ { name: 'Cliente', value: 'customer' },
38
+ { name: 'Rastreamento', value: 'tracking' },
39
+ { name: 'Produto', value: 'product' },
48
40
  ],
49
41
  default: 'order',
50
42
  },
51
- // ==================== PEDIDOS - OPERATIONS ====================
43
+
44
+ // ===== PEDIDO — OPERATIONS =====
52
45
  {
53
- displayName: 'Operation',
54
- name: 'operation',
55
- type: 'options',
46
+ displayName: 'Operation', name: 'operation', type: 'options',
56
47
  noDataExpression: true,
57
48
  displayOptions: { show: { resource: ['order'] } },
58
49
  options: [
59
- {
60
- name: 'Buscar Pedido',
61
- value: 'get',
62
- description: 'Busca um pedido específico por ID',
63
- action: 'Buscar um pedido',
64
- },
65
- {
66
- name: 'Listar Pedidos',
67
- value: 'list',
68
- description: 'Lista pedidos com filtros opcionais',
69
- action: 'Listar pedidos',
70
- },
50
+ { name: 'Buscar Pedido', value: 'get', action: 'Buscar pedido por ID' },
51
+ { name: 'Dados Completos', value: 'getComplete', action: 'Buscar dados completos' },
52
+ { name: 'Listar Pedidos', value: 'list', action: 'Listar pedidos com filtros' },
53
+ { name: 'Atualizar Status', value: 'updateStatus', action: 'Atualizar status do pedido' },
71
54
  ],
72
55
  default: 'get',
73
56
  },
74
57
  {
75
- displayName: 'ID do Pedido',
76
- name: 'orderId',
77
- type: 'string',
78
- required: true,
79
- displayOptions: { show: { resource: ['order'], operation: ['get'] } },
80
- default: '',
58
+ displayName: 'ID do Pedido', name: 'orderId', type: 'string', required: true,
59
+ displayOptions: { show: { resource: ['order'], operation: ['get', 'getComplete', 'updateStatus'] } },
60
+ default: '', placeholder: '9847',
81
61
  },
82
62
  {
83
- displayName: 'Filtros',
84
- name: 'filters',
85
- type: 'collection',
86
- placeholder: 'Adicionar Filtro',
87
- default: {},
63
+ displayName: 'Novo Status', name: 'orderStatus', type: 'options',
64
+ displayOptions: { show: { resource: ['order'], operation: ['updateStatus'] } },
65
+ options: [
66
+ { name: 'Aprovado', value: 'approved' },
67
+ { name: 'Em Preparação', value: 'preparing' },
68
+ { name: 'Enviado', value: 'shipped' },
69
+ { name: 'Entregue', value: 'delivered' },
70
+ { name: 'Cancelado', value: 'cancelled' },
71
+ ],
72
+ default: 'approved',
73
+ },
74
+ {
75
+ displayName: 'Filtros', name: 'filters', type: 'collection',
76
+ placeholder: 'Adicionar Filtro', default: {},
88
77
  displayOptions: { show: { resource: ['order'], operation: ['list'] } },
89
78
  options: [
90
79
  {
91
- displayName: 'Status',
92
- name: 'status',
93
- type: 'options',
80
+ displayName: 'Status', name: 'status', type: 'options', default: '',
94
81
  options: [
95
- { name: 'Pendente', value: 'pending' },
96
- { name: 'Aprovado', value: 'approved' },
97
- { name: 'Enviado', value: 'shipped' },
98
- { name: 'Entregue', value: 'delivered' },
82
+ { name: 'Pendente', value: 'pending' },
83
+ { name: 'Aprovado', value: 'approved' },
84
+ { name: 'Enviado', value: 'shipped' },
85
+ { name: 'Entregue', value: 'delivered' },
99
86
  { name: 'Cancelado', value: 'cancelled' },
100
87
  ],
101
- default: '',
102
88
  },
103
89
  { displayName: 'Limite', name: 'limit', type: 'number', default: 50 },
90
+ { displayName: 'Página', name: 'page', type: 'number', default: 1 },
91
+ { displayName: 'Data Início (YYYY-MM-DD)', name: 'dateFrom', type: 'string', default: '' },
92
+ { displayName: 'Data Fim (YYYY-MM-DD)', name: 'dateTo', type: 'string', default: '' },
104
93
  ],
105
94
  },
106
- // ==================== CLIENTES - OPERATIONS ====================
95
+
96
+ // ===== NOTA FISCAL — OPERATIONS =====
97
+ // v1.3.0 FIX: endpoints corretos são /orders/:id/invoices
98
+ {
99
+ displayName: 'Operation', name: 'operation', type: 'options',
100
+ noDataExpression: true,
101
+ displayOptions: { show: { resource: ['invoice'] } },
102
+ options: [
103
+ { name: 'Listar NFs do Pedido', value: 'list', action: 'GET /orders/:id/invoices' },
104
+ { name: 'Buscar NF por ID', value: 'get', action: 'GET /orders/:id/invoices/:inv_id' },
105
+ { name: 'Cadastrar NF', value: 'create', action: 'POST /invoices' },
106
+ ],
107
+ default: 'list',
108
+ },
109
+ {
110
+ displayName: 'ID do Pedido', name: 'orderId', type: 'string', required: true,
111
+ displayOptions: { show: { resource: ['invoice'], operation: ['list', 'get'] } },
112
+ default: '', placeholder: '9847',
113
+ description: 'ID do pedido Tray para buscar as NFs',
114
+ },
107
115
  {
108
- displayName: 'Operation',
109
- name: 'operation',
110
- type: 'options',
116
+ displayName: 'ID da Nota Fiscal', name: 'invoiceId', type: 'string', required: true,
117
+ displayOptions: { show: { resource: ['invoice'], operation: ['get'] } },
118
+ default: '', placeholder: '1347',
119
+ },
120
+ {
121
+ displayName: 'Dados da NF (JSON)', name: 'invoiceData', type: 'json',
122
+ displayOptions: { show: { resource: ['invoice'], operation: ['create'] } },
123
+ default: '{\n "order_id": "",\n "issue_date": "",\n "number": "",\n "serie": "",\n "value": "",\n "xml": "",\n "key": "",\n "link": ""\n}',
124
+ },
125
+
126
+ // ===== CLIENTES — OPERATIONS =====
127
+ {
128
+ displayName: 'Operation', name: 'operation', type: 'options',
111
129
  noDataExpression: true,
112
130
  displayOptions: { show: { resource: ['customer'] } },
113
131
  options: [
114
- { name: 'Buscar Cliente', value: 'get', action: 'Buscar um cliente' },
115
- { name: 'Criar/Atualizar Cliente', value: 'upsert', action: 'Criar ou atualizar cliente' },
132
+ { name: 'Buscar por ID', value: 'get', action: 'Buscar cliente por ID' },
133
+ { name: 'Listar Clientes', value: 'list', action: 'Listar clientes paginado' },
134
+ { name: 'Buscar por Tel/E-mail', value: 'search', action: 'Buscar por telefone ou email' },
116
135
  ],
117
136
  default: 'get',
118
137
  },
119
138
  {
120
- displayName: 'Buscar Por',
121
- name: 'searchType',
122
- type: 'options',
139
+ displayName: 'ID do Cliente', name: 'customerId', type: 'string', required: true,
123
140
  displayOptions: { show: { resource: ['customer'], operation: ['get'] } },
141
+ default: '',
142
+ },
143
+ {
144
+ displayName: 'Buscar Por', name: 'searchType', type: 'options',
145
+ displayOptions: { show: { resource: ['customer'], operation: ['search'] } },
124
146
  options: [
125
- { name: 'E-mail', value: 'email' },
126
- { name: 'CPF/CNPJ', value: 'cpf_cnpj' },
147
+ { name: 'E-mail', value: 'email' },
148
+ { name: 'Telefone', value: 'phone' },
149
+ { name: 'CPF/CNPJ', value: 'cpf_cnpj'},
127
150
  ],
128
151
  default: 'email',
129
152
  },
130
153
  {
131
- displayName: 'Valor',
132
- name: 'searchValue',
133
- type: 'string',
134
- displayOptions: { show: { resource: ['customer'], operation: ['get'] } },
154
+ displayName: 'Valor de Busca', name: 'searchValue', type: 'string',
155
+ displayOptions: { show: { resource: ['customer'], operation: ['search'] } },
135
156
  default: '',
136
157
  },
137
- // ==================== PRODUTOS - OPERATIONS ====================
138
158
  {
139
- displayName: 'Operation',
140
- name: 'operation',
141
- type: 'options',
159
+ displayName: 'Filtros', name: 'filters', type: 'collection',
160
+ placeholder: 'Adicionar Filtro', default: {},
161
+ displayOptions: { show: { resource: ['customer'], operation: ['list'] } },
162
+ options: [
163
+ { displayName: 'Limite', name: 'limit', type: 'number', default: 50 },
164
+ { displayName: 'Página', name: 'page', type: 'number', default: 1 },
165
+ ],
166
+ },
167
+
168
+ // ===== RASTREAMENTO — OPERATIONS =====
169
+ {
170
+ displayName: 'Operation', name: 'operation', type: 'options',
171
+ noDataExpression: true,
172
+ displayOptions: { show: { resource: ['tracking'] } },
173
+ options: [
174
+ { name: 'Código de Rastreio', value: 'getTracking', action: 'Obter código de rastreio do pedido' },
175
+ { name: 'Formas de Envio', value: 'getShipping', action: 'Listar formas de envio disponíveis' },
176
+ ],
177
+ default: 'getTracking',
178
+ },
179
+ {
180
+ displayName: 'ID do Pedido', name: 'orderId', type: 'string', required: true,
181
+ displayOptions: { show: { resource: ['tracking'], operation: ['getTracking'] } },
182
+ default: '', placeholder: '9847',
183
+ },
184
+
185
+ // ===== PRODUTO — OPERATIONS =====
186
+ {
187
+ displayName: 'Operation', name: 'operation', type: 'options',
142
188
  noDataExpression: true,
143
189
  displayOptions: { show: { resource: ['product'] } },
144
190
  options: [
145
- { name: 'Buscar Produto', value: 'get', action: 'Buscar um produto' },
146
- { name: 'Atualizar Estoque', value: 'updateStock', action: 'Atualizar estoque do produto' },
191
+ { name: 'Buscar Produto', value: 'get', action: 'Buscar produto por ID ou SKU' },
192
+ { name: 'Atualizar Estoque', value: 'updateStock', action: 'Atualizar estoque' },
147
193
  ],
148
194
  default: 'get',
149
195
  },
150
196
  {
151
- displayName: 'Buscar Por',
152
- name: 'searchType',
153
- type: 'options',
197
+ displayName: 'Buscar Por', name: 'searchType', type: 'options',
154
198
  displayOptions: { show: { resource: ['product'], operation: ['get'] } },
155
199
  options: [
156
- { name: 'ID do Produto', value: 'id' },
157
- { name: 'SKU', value: 'sku' },
200
+ { name: 'ID do Produto', value: 'id' },
201
+ { name: 'SKU', value: 'sku' },
158
202
  ],
159
203
  default: 'id',
160
204
  },
161
205
  {
162
- displayName: 'Valor',
163
- name: 'searchValue',
164
- type: 'string',
206
+ displayName: 'Valor', name: 'searchValue', type: 'string',
165
207
  displayOptions: { show: { resource: ['product'], operation: ['get'] } },
166
208
  default: '',
167
209
  },
168
210
  ],
169
211
  };
170
212
  }
213
+
171
214
  async execute() {
172
- var _a, _b;
173
- const items = this.getInputData();
215
+ const items = this.getInputData();
174
216
  const returnData = [];
175
- const resource = this.getNodeParameter('resource', 0);
217
+ const resource = this.getNodeParameter('resource', 0);
176
218
  const operation = this.getNodeParameter('operation', 0);
177
- const atendixAuthToken = process.env.ATENDIX_AUTH_TOKEN;
178
- if (!atendixAuthToken) {
179
- console.error('❌ ATENDIX_AUTH_TOKEN não configurado nas variáveis de ambiente');
180
- throw new Error('Configuração de validação Atendix incompleta. Entre em contato com o suporte.');
181
- }
182
- // 1. Get Credentials
219
+
220
+ // Credenciais (n8n chama preAuthentication automaticamente)
183
221
  const credentials = await this.getCredentials('trayApiAuto');
184
- let baseUrl = (credentials.apiHost || credentials.apiAddress || '').trim().replace(/\/$/, '');
185
- let licenseCheck;
186
- // ==========================================
187
- // 1.1 VALIDAÇÃO DE LICENÇA (SaaS GATEKEEPER)
188
- // ==========================================
189
- // Pegamos a URL base da loja para validar no seu servidor
190
- const storeBaseUrl = baseUrl.replace('/web_api', '');
191
- try {
192
- licenseCheck = await this.helpers.httpRequest({
193
- method: 'POST',
194
- url: 'https://n8n.mariapinho.com.br/webhook/valida-atendix',
195
- headers: {
196
- 'Content-Type': 'application/json',
197
- 'Authorization': atendixAuthToken,
198
- },
199
- body: { url: storeBaseUrl },
200
- json: true,
201
- });
202
- console.error('LICENSE CHECK RESPONSE: ' + licenseCheck.autorizado);
203
- if (!licenseCheck || licenseCheck.autorizado !== true) {
204
- console.error('Resposta inválida do servidor de licenças:', licenseCheck);
205
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Sua licença Atendix para a loja ${storeBaseUrl} não está ativa ou não pode ser validada. Para liberar seu acesso e automatizar sua operação, acesse https://atendix.co e assine um plano agora mesmo!`);
206
- }
207
- }
208
- catch (error) {
209
- console.error('ATENDIX_AUTH_TOKEN:', atendixAuthToken);
210
- console.error('STORE BASE URL:', storeBaseUrl);
211
- console.error('LICENSECHECK:', licenseCheck);
212
- console.error('Erro HTTP licença:', (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status);
213
- console.error('Body licença:', (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.data);
214
- // Se for erro de acesso bloqueado, repassa. Se for erro de rede, avisa o usuário.
215
- if (error instanceof n8n_workflow_1.NodeOperationError)
216
- throw error;
217
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), "Erro crítico: Não foi possível validar a licença do Atendix no servidor MariaPinho.");
218
- }
219
- // 2. TOKEN MANAGER (Fallback)
220
- let accessToken = credentials.accessToken;
221
- if (!accessToken) {
222
- try {
223
- const authResponse = await this.helpers.httpRequest({
224
- method: 'POST',
225
- url: `${baseUrl}/auth`,
226
- body: {
227
- consumer_key: process.env.TRAY_CONSUMER_KEY,
228
- consumer_secret: process.env.TRAY_CONSUMER_SECRET,
229
- code: credentials.authCode,
230
- },
231
- json: true,
232
- });
233
- accessToken = authResponse.access_token;
234
- }
235
- catch (authError) {
236
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Falha na Autenticação Tray: ${authError.message}`);
237
- }
222
+ const storeId = (credentials.storeId || '').toString().trim();
223
+
224
+ if (!storeId) {
225
+ throw new n8n_workflow_1.NodeOperationError(
226
+ this.getNode(),
227
+ 'Store ID não configurado nas credenciais Atendix. Preencha o campo Store ID.',
228
+ );
238
229
  }
230
+
231
+ // v1.3.0: URL derivada do store_id — nunca mais hardcoded
232
+ const baseUrl = `https://${storeId}.commercesuite.com.br/web_api`;
233
+ const accessToken = credentials.accessToken;
234
+
239
235
  for (let i = 0; i < items.length; i++) {
240
236
  try {
237
+ if (i > 0) await wait(500); // Rate-limit seguro: ~120 req/min (limite Tray: 180)
238
+
241
239
  let responseData = {};
242
240
  const qs = { access_token: accessToken };
243
- // ==================== PEDIDOS ====================
241
+
242
+ // ===== PEDIDOS =====
244
243
  if (resource === 'order') {
245
244
  if (operation === 'get') {
246
245
  const orderId = this.getNodeParameter('orderId', i);
247
- if (i > 0) {
248
- // Dá uma respirada de 500ms entre cada item da lista
249
- // 500ms = 2 requisições por segundo = 120 por minuto (Seguro dentro dos 180)
250
- await wait(500);
251
- }
252
246
  responseData = await this.helpers.httpRequest({
253
- method: 'GET',
254
- url: `${baseUrl}/orders/${orderId}`,
255
- qs,
256
- json: true,
247
+ method: 'GET', url: `${baseUrl}/orders/${orderId}`, qs, json: true,
248
+ });
249
+ } else if (operation === 'getComplete') {
250
+ const orderId = this.getNodeParameter('orderId', i);
251
+ responseData = await this.helpers.httpRequest({
252
+ method: 'GET', url: `${baseUrl}/orders/${orderId}/complete`, qs, json: true,
253
+ });
254
+ } else if (operation === 'list') {
255
+ const filters = this.getNodeParameter('filters', i, {});
256
+ if (filters.status) qs.status = filters.status;
257
+ if (filters.limit) qs.limit = filters.limit;
258
+ if (filters.page) qs.page = filters.page;
259
+ if (filters.dateFrom) qs.date_from = filters.dateFrom;
260
+ if (filters.dateTo) qs.date_to = filters.dateTo;
261
+ responseData = await this.helpers.httpRequest({
262
+ method: 'GET', url: `${baseUrl}/orders`, qs, json: true,
263
+ });
264
+ } else if (operation === 'updateStatus') {
265
+ const orderId = this.getNodeParameter('orderId', i);
266
+ const orderStatus = this.getNodeParameter('orderStatus', i);
267
+ responseData = await this.helpers.httpRequest({
268
+ method: 'PUT', url: `${baseUrl}/orders/${orderId}`,
269
+ qs, body: { status: orderStatus }, json: true,
257
270
  });
258
271
  }
272
+ }
273
+
274
+ // ===== NOTA FISCAL =====
275
+ // v1.3.0 FIX CRÍTICO: endpoints corretos são /orders/:id/invoices
276
+ else if (resource === 'invoice') {
259
277
  if (operation === 'list') {
260
- const filters = this.getNodeParameter('filters', i, {});
261
- if (filters.status)
262
- qs.status = filters.status;
263
- if (filters.limit)
264
- qs.limit = filters.limit;
265
- if (i > 0) {
266
- // uma respirada de 500ms entre cada item da lista
267
- // 500ms = 2 requisições por segundo = 120 por minuto (Seguro dentro dos 180)
268
- await wait(500);
269
- }
278
+ const orderId = this.getNodeParameter('orderId', i);
279
+ // ✅ CORRETO: GET /orders/:order_id/invoices
280
+ responseData = await this.helpers.httpRequest({
281
+ method: 'GET', url: `${baseUrl}/orders/${orderId}/invoices`, qs, json: true,
282
+ });
283
+ } else if (operation === 'get') {
284
+ const orderId = this.getNodeParameter('orderId', i);
285
+ const invoiceId = this.getNodeParameter('invoiceId', i);
286
+ // ✅ CORRETO: GET /orders/:order_id/invoices/:invoice_id
270
287
  responseData = await this.helpers.httpRequest({
271
- method: 'GET',
272
- url: `${baseUrl}/orders`,
273
- qs,
274
- json: true,
288
+ method: 'GET', url: `${baseUrl}/orders/${orderId}/invoices/${invoiceId}`, qs, json: true,
289
+ });
290
+ } else if (operation === 'create') {
291
+ const invoiceData = JSON.parse(this.getNodeParameter('invoiceData', i));
292
+ responseData = await this.helpers.httpRequest({
293
+ method: 'POST', url: `${baseUrl}/invoices`, qs, body: invoiceData, json: true,
275
294
  });
276
295
  }
277
296
  }
278
- // ==================== CLIENTES ====================
279
- if (resource === 'customer') {
297
+
298
+ // ===== CLIENTES =====
299
+ else if (resource === 'customer') {
280
300
  if (operation === 'get') {
281
- const searchType = this.getNodeParameter('searchType', i);
301
+ const customerId = this.getNodeParameter('customerId', i);
302
+ responseData = await this.helpers.httpRequest({
303
+ method: 'GET', url: `${baseUrl}/customers/${customerId}`, qs, json: true,
304
+ });
305
+ } else if (operation === 'list') {
306
+ const filters = this.getNodeParameter('filters', i, {});
307
+ if (filters.limit) qs.limit = filters.limit;
308
+ if (filters.page) qs.page = filters.page;
309
+ responseData = await this.helpers.httpRequest({
310
+ method: 'GET', url: `${baseUrl}/customers`, qs, json: true,
311
+ });
312
+ } else if (operation === 'search') {
313
+ const searchType = this.getNodeParameter('searchType', i);
282
314
  const searchValue = this.getNodeParameter('searchValue', i);
283
- if (searchType === 'email')
284
- qs.email = searchValue;
285
- else
286
- qs.cpf_cnpj = searchValue;
315
+ qs[searchType] = searchValue;
287
316
  responseData = await this.helpers.httpRequest({
288
- method: 'GET',
289
- url: `${baseUrl}/customers`,
290
- qs,
291
- json: true,
317
+ method: 'GET', url: `${baseUrl}/customers`, qs, json: true,
292
318
  });
293
319
  }
294
- // Nota da Gerente: Se for adicionar UPSERT aqui, o código deve ser incluído abaixo
295
320
  }
296
- // ==================== PRODUTOS ====================
297
- if (resource === 'product') {
321
+
322
+ // ===== RASTREAMENTO =====
323
+ else if (resource === 'tracking') {
324
+ if (operation === 'getTracking') {
325
+ const orderId = this.getNodeParameter('orderId', i);
326
+ const orderRaw = await this.helpers.httpRequest({
327
+ method: 'GET', url: `${baseUrl}/orders/${orderId}`, qs, json: true,
328
+ });
329
+ const order = (orderRaw.Order || orderRaw);
330
+ responseData = {
331
+ order_id: orderId,
332
+ tracking_code: order.shipping_tracking_code || null,
333
+ shipping_carrier: order.shipping_carrier || null,
334
+ shipping_method: order.shipping_method_name || null,
335
+ };
336
+ } else if (operation === 'getShipping') {
337
+ responseData = await this.helpers.httpRequest({
338
+ method: 'GET', url: `${baseUrl}/shipping`, qs, json: true,
339
+ });
340
+ }
341
+ }
342
+
343
+ // ===== PRODUTO =====
344
+ else if (resource === 'product') {
298
345
  if (operation === 'get') {
299
- const searchType = this.getNodeParameter('searchType', i);
346
+ const searchType = this.getNodeParameter('searchType', i);
300
347
  const searchValue = this.getNodeParameter('searchValue', i);
301
348
  if (searchType === 'id') {
302
349
  responseData = await this.helpers.httpRequest({
303
- method: 'GET',
304
- url: `${baseUrl}/products/${searchValue}`,
305
- qs,
306
- json: true,
350
+ method: 'GET', url: `${baseUrl}/products/${searchValue}`, qs, json: true,
307
351
  });
308
- }
309
- else {
352
+ } else {
310
353
  qs.sku = searchValue;
311
354
  responseData = await this.helpers.httpRequest({
312
- method: 'GET',
313
- url: `${baseUrl}/products`,
314
- qs,
315
- json: true,
355
+ method: 'GET', url: `${baseUrl}/products`, qs, json: true,
316
356
  });
317
357
  }
318
358
  }
319
- // Nota da Gerente: Se for adicionar UPDATE_STOCK aqui, o código deve ser incluído abaixo
320
359
  }
321
- const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(responseData), { itemData: { item: i } });
360
+
361
+ const executionData = this.helpers.constructExecutionMetaData(
362
+ this.helpers.returnJsonArray(responseData),
363
+ { itemData: { item: i } },
364
+ );
322
365
  returnData.push(...executionData);
323
- }
324
- catch (error) {
325
- const errorMessage = error instanceof Error ? error.message : String(error);
366
+
367
+ } catch (error) {
368
+ const msg = error instanceof Error ? error.message : String(error);
326
369
  if (this.continueOnFail()) {
327
- returnData.push({ json: { error: errorMessage }, pairedItem: { item: i } });
370
+ returnData.push({ json: { error: msg, resource, operation }, pairedItem: { item: i } });
328
371
  continue;
329
372
  }
330
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage, { itemIndex: i });
373
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), msg, { itemIndex: i });
331
374
  }
332
375
  }
376
+
333
377
  return [returnData];
334
378
  }
335
379
  }
package/index.js CHANGED
@@ -1,2 +1,6 @@
1
- // Este arquivo é necessário para o n8n
2
- module.exports = {};
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ var Atendix_node_1 = require("./dist/nodes/Atendix/Atendix.node");
4
+ Object.defineProperty(exports, "Atendix", { enumerable: true, get: function () { return Atendix_node_1.Atendix; } });
5
+ var TrayApiAuto_credentials_1 = require("./dist/credentials/TrayApiAuto.credentials");
6
+ Object.defineProperty(exports, "TrayApiAuto", { enumerable: true, get: function () { return TrayApiAuto_credentials_1.TrayApiAuto; } });
package/package.json CHANGED
@@ -1,37 +1,33 @@
1
- {
2
- "name": "n8n-nodes-atendix",
3
- "version": "1.0.1",
4
- "description": "Conector Atendix para integração com Tray Commerce",
5
- "keywords": ["n8n-community-node-package"],
6
- "license": "MIT",
7
- "homepage": "https://atendix.co",
8
- "author": {
9
- "name": "Atendix",
10
- "email": "contato@atendix.co"
11
- },
12
- "main": "index.js",
13
- "scripts": {
14
- "build": "tsc && copy nodes\\Atendix\\tray.svg dist\\nodes\\Atendix\\tray.svg && copy nodes\\Atendix\\tray.svg dist\\credentials\\tray.svg",
15
- "dev": "tsc --watch"
16
- },
17
- "files": [
18
- "dist",
19
- "icons"
20
- ],
21
- "n8n": {
22
- "n8nNodesApiVersion": 1,
23
- "credentials": [
24
- "dist/credentials/TrayApiAuto.credentials.js"
25
- ],
26
- "nodes": [
27
- "dist/nodes/Atendix/Atendix.node.js"
28
- ]
29
- },
30
- "dependencies": {
31
- "n8n-workflow": "^1.0.0"
32
- },
33
- "devDependencies": {
34
- "@types/node": "^18.0.0",
35
- "typescript": "^5.0.0"
36
- }
37
- }
1
+ {
2
+ "name": "n8n-nodes-atendix",
3
+ "version": "1.3.0",
4
+ "description": "Conector Atendix para integração nativa com a Tray Commerce",
5
+ "keywords": ["n8n-community-node-package"],
6
+ "license": "MIT",
7
+ "homepage": "https://atendix.co",
8
+ "author": {
9
+ "name": "Atendix",
10
+ "email": "contato@atendix.co"
11
+ },
12
+ "main": "index.js",
13
+ "scripts": {
14
+ "build": "echo 'v1.3.0 pre-compiled no build needed'"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "icons",
19
+ "index.js"
20
+ ],
21
+ "n8n": {
22
+ "n8nNodesApiVersion": 1,
23
+ "credentials": [
24
+ "dist/credentials/TrayApiAuto.credentials.js"
25
+ ],
26
+ "nodes": [
27
+ "dist/nodes/Atendix/Atendix.node.js"
28
+ ]
29
+ },
30
+ "dependencies": {
31
+ "n8n-workflow": "^1.0.0"
32
+ }
33
+ }
package/README.md DELETED
@@ -1,68 +0,0 @@
1
- # Atendix - Conector Tray Commerce para n8n 🚀
2
-
3
- ![n8n](https://img.shields.io/badge/n8n-certified-orange?style=for-the-badge)
4
- ![NPM Version](https://img.shields.io/npm/v/n8n-nodes-atendix?style=for-the-badge&color=green)
5
- ![Rate Limit](https://img.shields.io/badge/Rate%20Limit-180%2Fmin-blue?style=for-the-badge)
6
-
7
- O **Atendix** é o conector definitivo para integrar sua loja **Tray Commerce** ao ecossistema de automação do **n8n**. Esqueça a complexidade de lidar com tokens manuais e exposição de chaves sensíveis.
8
-
9
- ---
10
-
11
- ## ✨ Por que usar o Atendix?
12
-
13
- - 🔒 **Segurança Total:** Suas chaves `Consumer Key` e `Consumer Secret` ficam protegidas e nunca são expostas no workflow.
14
- - 🔄 **Autenticação Automática:** Gerenciamento inteligente de `Access Token` e `Refresh Token` sem interrupções.
15
- - ⚖️ **Respeito ao Rate Limit:** Implementação nativa de *Throttling* (500ms) para garantir conformidade com o limite de 180 req/min da Tray.
16
- - 💼 **Modelo SaaS:** Licenciamento centralizado para garantir suporte e atualizações contínuas.
17
-
18
- ---
19
-
20
- ## 🛠️ Instalação
21
-
22
- Para instalar este nó em sua instância n8n:
23
-
24
- 1. Acesse **Settings** (Configurações).
25
- 2. Vá em **Community Nodes** (Nós da Comunidade).
26
- 3. Clique em **Install a node**.
27
- 4. Digite o nome do pacote: `n8n-nodes-atendix`.
28
- 5. Aceite os termos e clique em **Install**.
29
-
30
- ---
31
-
32
- ## 🔑 Configuração das Credenciais
33
-
34
- Após a instalação, configure sua credencial Atendix com apenas dois campos:
35
-
36
- 1. **API Address:** A URL da sua loja (ex: `https://{SUA_LOJA}.commercesuite.com.br/web_api`).
37
- 2. **Authorization Code:** O código de autorização gerado no painel da Tray.
38
-
39
- > **Nota:** Certifique-se de que sua loja possui uma licença ativa em [Atendix.co](https://atendix.co) para liberar o tráfego de dados.
40
-
41
- ---
42
-
43
- ## 📦 Recursos Disponíveis (v1.0.1)
44
-
45
- - ✅ **Pedidos:** Busca por ID e Listagem com filtros de status e data.
46
- - ✅ **Clientes:** Busca por e-mail ou documento (CPF/CNPJ).
47
- - ✅ **Produtos:** Consulta rápida por ID ou SKU.
48
-
49
- ---
50
-
51
- ## ⚙️ Requisitos Técnicos
52
-
53
- - **n8n version:** >= 1.0.0
54
- - **Node.js:** >= 18.x
55
-
56
- ---
57
-
58
- ## 📄 Licença
59
-
60
- Distribuído sob a licença MIT. Veja `LICENSE` para mais informações.
61
-
62
- ---
63
-
64
- ## 📞 Suporte e Contato
65
-
66
- Dúvidas ou problemas com a ativação da sua licença?
67
- - **Site:** [https://atendix.co](https://atendix.co)
68
- - **E-mail:** contato@atendix.co
@@ -1,16 +0,0 @@
1
- import { IAuthenticateGeneric, ICredentialType, INodeProperties, ICredentialDataDecryptedObject, ICredentialTestRequest, Icon } from 'n8n-workflow';
2
- export declare class TrayApiAuto implements ICredentialType {
3
- name: string;
4
- displayName: string;
5
- icon: Icon;
6
- documentationUrl: string;
7
- properties: INodeProperties[];
8
- authenticate: IAuthenticateGeneric;
9
- preAuthentication(credentials: ICredentialDataDecryptedObject): Promise<ICredentialDataDecryptedObject>;
10
- /**
11
- * Renomeado para evitar conflito com a propriedade 'authenticate' da interface
12
- */
13
- private _authenticateFlow;
14
- private _refreshTokenFlow;
15
- test: ICredentialTestRequest;
16
- }
@@ -1,21 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg
3
- xmlns="http://www.w3.org/2000/svg"
4
- viewBox="0 0 24 24"
5
- fill="none"
6
- stroke="#7D7D87"
7
- stroke-width="1.3"
8
- stroke-linecap="round"
9
- stroke-linejoin="round"
10
- >
11
- <path d="M3 21 L3 6 C3 4.343 4.343 3 6 3 H18 C19.657 3 22 4.343 22 6 V14 C22 15.657 19.657 17 18 17 H8.5 L3 21 Z" />
12
-
13
- <path d="M6.8 14V9.5" />
14
- <circle cx="6.8" cy="8" r="1.5" />
15
-
16
- <path d="M10.6 14V11.5L12.5 9" />
17
- <circle cx="13.5" cy="7.8" r="1.5" />
18
-
19
- <path d="M14 14V12.5C14 11.8 14.5 11.5 15.2 11.5H17" />
20
- <circle cx="18.5" cy="11.5" r="1.5" />
21
- </svg>
@@ -1,11 +0,0 @@
1
- /**
2
- * Arquivo: n8n-nodes-atendix/nodes/Atendix/Atendix.node.ts
3
- *
4
- * Node oficial Atendix - Tray Commerce
5
- * Versão 1.2.0 - Licenciamento SaaS + Full Operations
6
- */
7
- import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
8
- export declare class Atendix implements INodeType {
9
- description: INodeTypeDescription;
10
- execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
11
- }