n8n-nodes-atendix 1.0.0 → 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,324 +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
+
13
+ const wait = (ms) => new Promise(res => setTimeout(res, ms));
14
+
11
15
  class Atendix {
12
16
  constructor() {
13
17
  this.description = {
14
18
  displayName: 'Atendix - Tray API',
15
19
  name: 'atendix',
16
- icon: {
17
- light: 'file:tray.svg',
18
- dark: 'file:tray.svg',
19
- },
20
+ icon: { light: 'file:tray.svg', dark: 'file:tray.svg' },
20
21
  group: ['transform'],
21
22
  version: 1,
22
23
  subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
23
- description: 'Integração com a plataforma Tray Commerce',
24
- defaults: {
25
- name: 'Atendix - Tray API',
26
- },
24
+ description: 'Integração nativa com a Tray Commerce — v1.3.0',
25
+ defaults: { name: 'Atendix - Tray API' },
27
26
  inputs: ['main'],
28
27
  outputs: ['main'],
29
- credentials: [
30
- {
31
- name: 'trayApiAuto',
32
- required: true,
33
- displayName: 'Atendix - Tray API',
34
- },
35
- ],
28
+ credentials: [{ name: 'trayApiAuto', required: true, displayName: 'Atendix - Tray API' }],
36
29
  properties: [
37
- // ==================== RESOURCE SELECTOR ====================
30
+ // ===== RESOURCE =====
38
31
  {
39
- displayName: 'Resource',
40
- name: 'resource',
41
- type: 'options',
32
+ displayName: 'Resource', name: 'resource', type: 'options',
42
33
  noDataExpression: true,
43
34
  options: [
44
- { name: 'Pedido', value: 'order' },
45
- { name: 'Cliente', value: 'customer' },
46
- { 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' },
47
40
  ],
48
41
  default: 'order',
49
42
  },
50
- // ==================== PEDIDOS - OPERATIONS ====================
43
+
44
+ // ===== PEDIDO — OPERATIONS =====
51
45
  {
52
- displayName: 'Operation',
53
- name: 'operation',
54
- type: 'options',
46
+ displayName: 'Operation', name: 'operation', type: 'options',
55
47
  noDataExpression: true,
56
48
  displayOptions: { show: { resource: ['order'] } },
57
49
  options: [
58
- {
59
- name: 'Buscar Pedido',
60
- value: 'get',
61
- description: 'Busca um pedido específico por ID',
62
- action: 'Buscar um pedido',
63
- },
64
- {
65
- name: 'Listar Pedidos',
66
- value: 'list',
67
- description: 'Lista pedidos com filtros opcionais',
68
- action: 'Listar pedidos',
69
- },
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' },
70
54
  ],
71
55
  default: 'get',
72
56
  },
73
57
  {
74
- displayName: 'ID do Pedido',
75
- name: 'orderId',
76
- type: 'string',
77
- required: true,
78
- displayOptions: { show: { resource: ['order'], operation: ['get'] } },
79
- 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',
80
61
  },
81
62
  {
82
- displayName: 'Filtros',
83
- name: 'filters',
84
- type: 'collection',
85
- placeholder: 'Adicionar Filtro',
86
- 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: {},
87
77
  displayOptions: { show: { resource: ['order'], operation: ['list'] } },
88
78
  options: [
89
79
  {
90
- displayName: 'Status',
91
- name: 'status',
92
- type: 'options',
80
+ displayName: 'Status', name: 'status', type: 'options', default: '',
93
81
  options: [
94
- { name: 'Pendente', value: 'pending' },
95
- { name: 'Aprovado', value: 'approved' },
96
- { name: 'Enviado', value: 'shipped' },
97
- { name: 'Entregue', value: 'delivered' },
82
+ { name: 'Pendente', value: 'pending' },
83
+ { name: 'Aprovado', value: 'approved' },
84
+ { name: 'Enviado', value: 'shipped' },
85
+ { name: 'Entregue', value: 'delivered' },
98
86
  { name: 'Cancelado', value: 'cancelled' },
99
87
  ],
100
- default: '',
101
88
  },
102
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: '' },
103
93
  ],
104
94
  },
105
- // ==================== CLIENTES - OPERATIONS ====================
95
+
96
+ // ===== NOTA FISCAL — OPERATIONS =====
97
+ // v1.3.0 FIX: endpoints corretos são /orders/:id/invoices
106
98
  {
107
- displayName: 'Operation',
108
- name: 'operation',
109
- type: 'options',
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
+ },
115
+ {
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',
110
129
  noDataExpression: true,
111
130
  displayOptions: { show: { resource: ['customer'] } },
112
131
  options: [
113
- { name: 'Buscar Cliente', value: 'get', action: 'Buscar um cliente' },
114
- { 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' },
115
135
  ],
116
136
  default: 'get',
117
137
  },
118
138
  {
119
- displayName: 'Buscar Por',
120
- name: 'searchType',
121
- type: 'options',
139
+ displayName: 'ID do Cliente', name: 'customerId', type: 'string', required: true,
122
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'] } },
123
146
  options: [
124
- { name: 'E-mail', value: 'email' },
125
- { name: 'CPF/CNPJ', value: 'cpf_cnpj' },
147
+ { name: 'E-mail', value: 'email' },
148
+ { name: 'Telefone', value: 'phone' },
149
+ { name: 'CPF/CNPJ', value: 'cpf_cnpj'},
126
150
  ],
127
151
  default: 'email',
128
152
  },
129
153
  {
130
- displayName: 'Valor',
131
- name: 'searchValue',
132
- type: 'string',
133
- displayOptions: { show: { resource: ['customer'], operation: ['get'] } },
154
+ displayName: 'Valor de Busca', name: 'searchValue', type: 'string',
155
+ displayOptions: { show: { resource: ['customer'], operation: ['search'] } },
134
156
  default: '',
135
157
  },
136
- // ==================== PRODUTOS - OPERATIONS ====================
137
158
  {
138
- displayName: 'Operation',
139
- name: 'operation',
140
- 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',
141
188
  noDataExpression: true,
142
189
  displayOptions: { show: { resource: ['product'] } },
143
190
  options: [
144
- { name: 'Buscar Produto', value: 'get', action: 'Buscar um produto' },
145
- { 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' },
146
193
  ],
147
194
  default: 'get',
148
195
  },
149
196
  {
150
- displayName: 'Buscar Por',
151
- name: 'searchType',
152
- type: 'options',
197
+ displayName: 'Buscar Por', name: 'searchType', type: 'options',
153
198
  displayOptions: { show: { resource: ['product'], operation: ['get'] } },
154
199
  options: [
155
- { name: 'ID do Produto', value: 'id' },
156
- { name: 'SKU', value: 'sku' },
200
+ { name: 'ID do Produto', value: 'id' },
201
+ { name: 'SKU', value: 'sku' },
157
202
  ],
158
203
  default: 'id',
159
204
  },
160
205
  {
161
- displayName: 'Valor',
162
- name: 'searchValue',
163
- type: 'string',
206
+ displayName: 'Valor', name: 'searchValue', type: 'string',
164
207
  displayOptions: { show: { resource: ['product'], operation: ['get'] } },
165
208
  default: '',
166
209
  },
167
210
  ],
168
211
  };
169
212
  }
213
+
170
214
  async execute() {
171
- var _a, _b;
172
- const items = this.getInputData();
215
+ const items = this.getInputData();
173
216
  const returnData = [];
174
- const resource = this.getNodeParameter('resource', 0);
217
+ const resource = this.getNodeParameter('resource', 0);
175
218
  const operation = this.getNodeParameter('operation', 0);
176
- const atendixAuthToken = process.env.ATENDIX_AUTH_TOKEN;
177
- if (!atendixAuthToken) {
178
- console.error('❌ ATENDIX_AUTH_TOKEN não configurado nas variáveis de ambiente');
179
- throw new Error('Configuração de validação Atendix incompleta. Entre em contato com o suporte.');
180
- }
181
- // 1. Get Credentials
219
+
220
+ // Credenciais (n8n chama preAuthentication automaticamente)
182
221
  const credentials = await this.getCredentials('trayApiAuto');
183
- let baseUrl = (credentials.apiHost || credentials.apiAddress || '').trim().replace(/\/$/, '');
184
- let licenseCheck;
185
- // ==========================================
186
- // 1.1 VALIDAÇÃO DE LICENÇA (SaaS GATEKEEPER)
187
- // ==========================================
188
- // Pegamos a URL base da loja para validar no seu servidor
189
- const storeBaseUrl = baseUrl.replace('/web_api', '');
190
- try {
191
- licenseCheck = await this.helpers.httpRequest({
192
- method: 'POST',
193
- url: 'https://n8n.mariapinho.com.br/webhook/valida-atendix',
194
- headers: {
195
- 'Content-Type': 'application/json',
196
- 'Authorization': atendixAuthToken,
197
- },
198
- body: { url: storeBaseUrl },
199
- json: true,
200
- });
201
- console.error('LICENSE CHECK RESPONSE: ' + licenseCheck.autorizado);
202
- if (!licenseCheck || licenseCheck.autorizado !== true) {
203
- console.error('Resposta inválida do servidor de licenças:', licenseCheck);
204
- 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.com.br e assine um plano agora mesmo!`);
205
- }
206
- }
207
- catch (error) {
208
- console.error('ATENDIX_AUTH_TOKEN:', atendixAuthToken);
209
- console.error('STORE BASE URL:', storeBaseUrl);
210
- console.error('LICENSECHECK:', licenseCheck);
211
- console.error('Erro HTTP licença:', (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status);
212
- console.error('Body licença:', (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.data);
213
- // Se for erro de acesso bloqueado, repassa. Se for erro de rede, avisa o usuário.
214
- if (error instanceof n8n_workflow_1.NodeOperationError)
215
- throw error;
216
- 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.");
217
- }
218
- // 2. TOKEN MANAGER (Fallback)
219
- let accessToken = credentials.accessToken;
220
- if (!accessToken) {
221
- try {
222
- const authResponse = await this.helpers.httpRequest({
223
- method: 'POST',
224
- url: `${baseUrl}/auth`,
225
- body: {
226
- consumer_key: process.env.TRAY_CONSUMER_KEY,
227
- consumer_secret: process.env.TRAY_CONSUMER_SECRET,
228
- code: credentials.authCode,
229
- },
230
- json: true,
231
- });
232
- accessToken = authResponse.access_token;
233
- }
234
- catch (authError) {
235
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Falha na Autenticação Tray: ${authError.message}`);
236
- }
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
+ );
237
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
+
238
235
  for (let i = 0; i < items.length; i++) {
239
236
  try {
237
+ if (i > 0) await wait(500); // Rate-limit seguro: ~120 req/min (limite Tray: 180)
238
+
240
239
  let responseData = {};
241
240
  const qs = { access_token: accessToken };
242
- // ==================== PEDIDOS ====================
241
+
242
+ // ===== PEDIDOS =====
243
243
  if (resource === 'order') {
244
244
  if (operation === 'get') {
245
245
  const orderId = this.getNodeParameter('orderId', i);
246
246
  responseData = await this.helpers.httpRequest({
247
- method: 'GET',
248
- url: `${baseUrl}/orders/${orderId}`,
249
- qs,
250
- 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,
251
270
  });
252
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') {
253
277
  if (operation === 'list') {
254
- const filters = this.getNodeParameter('filters', i, {});
255
- if (filters.status)
256
- qs.status = filters.status;
257
- if (filters.limit)
258
- qs.limit = filters.limit;
278
+ const orderId = this.getNodeParameter('orderId', i);
279
+ // ✅ CORRETO: GET /orders/:order_id/invoices
259
280
  responseData = await this.helpers.httpRequest({
260
- method: 'GET',
261
- url: `${baseUrl}/orders`,
262
- qs,
263
- json: true,
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
287
+ responseData = await this.helpers.httpRequest({
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,
264
294
  });
265
295
  }
266
296
  }
267
- // ==================== CLIENTES ====================
268
- if (resource === 'customer') {
297
+
298
+ // ===== CLIENTES =====
299
+ else if (resource === 'customer') {
269
300
  if (operation === 'get') {
270
- 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);
271
314
  const searchValue = this.getNodeParameter('searchValue', i);
272
- if (searchType === 'email')
273
- qs.email = searchValue;
274
- else
275
- qs.cpf_cnpj = searchValue;
315
+ qs[searchType] = searchValue;
276
316
  responseData = await this.helpers.httpRequest({
277
- method: 'GET',
278
- url: `${baseUrl}/customers`,
279
- qs,
280
- json: true,
317
+ method: 'GET', url: `${baseUrl}/customers`, qs, json: true,
281
318
  });
282
319
  }
283
- // Nota da Gerente: Se for adicionar UPSERT aqui, o código deve ser incluído abaixo
284
320
  }
285
- // ==================== PRODUTOS ====================
286
- 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') {
287
345
  if (operation === 'get') {
288
- const searchType = this.getNodeParameter('searchType', i);
346
+ const searchType = this.getNodeParameter('searchType', i);
289
347
  const searchValue = this.getNodeParameter('searchValue', i);
290
348
  if (searchType === 'id') {
291
349
  responseData = await this.helpers.httpRequest({
292
- method: 'GET',
293
- url: `${baseUrl}/products/${searchValue}`,
294
- qs,
295
- json: true,
350
+ method: 'GET', url: `${baseUrl}/products/${searchValue}`, qs, json: true,
296
351
  });
297
- }
298
- else {
352
+ } else {
299
353
  qs.sku = searchValue;
300
354
  responseData = await this.helpers.httpRequest({
301
- method: 'GET',
302
- url: `${baseUrl}/products`,
303
- qs,
304
- json: true,
355
+ method: 'GET', url: `${baseUrl}/products`, qs, json: true,
305
356
  });
306
357
  }
307
358
  }
308
- // Nota da Gerente: Se for adicionar UPDATE_STOCK aqui, o código deve ser incluído abaixo
309
359
  }
310
- 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
+ );
311
365
  returnData.push(...executionData);
312
- }
313
- catch (error) {
314
- const errorMessage = error instanceof Error ? error.message : String(error);
366
+
367
+ } catch (error) {
368
+ const msg = error instanceof Error ? error.message : String(error);
315
369
  if (this.continueOnFail()) {
316
- returnData.push({ json: { error: errorMessage }, pairedItem: { item: i } });
370
+ returnData.push({ json: { error: msg, resource, operation }, pairedItem: { item: i } });
317
371
  continue;
318
372
  }
319
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage, { itemIndex: i });
373
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), msg, { itemIndex: i });
320
374
  }
321
375
  }
376
+
322
377
  return [returnData];
323
378
  }
324
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.0",
4
- "description": "Conector Atendix para integração com Tray Commerce",
5
- "keywords": ["n8n-community-node-package"],
6
- "license": "MIT",
7
- "homepage": "https://atendix.com.br",
8
- "author": {
9
- "name": "Atendix",
10
- "email": "contato@atendix.com.br"
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
+ }
@@ -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
- }