sienge-ecbiesek-mcp 1.1.5__py3-none-any.whl → 1.2.0__py3-none-any.whl

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.

Potentially problematic release.


This version of sienge-ecbiesek-mcp might be problematic. Click here for more details.

sienge_mcp/server.py CHANGED
@@ -2,14 +2,27 @@
2
2
  """
3
3
  SIENGE MCP COMPLETO - FastMCP com Autenticação Flexível
4
4
  Suporta Bearer Token e Basic Auth
5
+ CORREÇÕES IMPLEMENTADAS:
6
+ 1. Separação entre camada MCP e camada de serviços
7
+ 2. Alias compatíveis com checklist
8
+ 3. Normalização de parâmetros (camelCase + arrays)
9
+ 4. Bulk-data assíncrono com polling
10
+ 5. Observabilidade mínima (X-Request-ID, cache, logs)
11
+ 6. Ajustes de compatibilidade pontuais
5
12
  """
6
13
 
7
14
  from fastmcp import FastMCP
8
15
  import httpx
9
- from typing import Dict, List, Optional, Any
16
+ from typing import Dict, List, Optional, Any, Union
10
17
  import os
11
18
  from dotenv import load_dotenv
12
- from datetime import datetime
19
+ from datetime import datetime, timedelta
20
+ import uuid
21
+ import asyncio
22
+ import json
23
+ import time
24
+ import logging
25
+ from functools import wraps
13
26
 
14
27
  # Carrega as variáveis de ambiente
15
28
  load_dotenv()
@@ -24,61 +37,273 @@ SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
24
37
  SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
25
38
  REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
26
39
 
40
+ # Cache simples em memória
41
+ _cache = {}
42
+ CACHE_TTL = 300 # 5 minutos
43
+
44
+ # Configurar logging estruturado
45
+ logging.basicConfig(
46
+ level=logging.INFO,
47
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
48
+ )
49
+ logger = logging.getLogger("sienge-mcp")
50
+
27
51
 
28
52
  class SiengeAPIError(Exception):
29
53
  """Exceção customizada para erros da API do Sienge"""
30
-
31
54
  pass
32
55
 
33
56
 
57
+ # ============ HELPERS DE NORMALIZAÇÃO E OBSERVABILIDADE ============
58
+
59
+ def _camel(s: str) -> str:
60
+ """Converte snake_case para camelCase"""
61
+ if '_' not in s:
62
+ return s
63
+ parts = s.split('_')
64
+ return parts[0] + ''.join(x.capitalize() for x in parts[1:])
65
+
66
+
67
+ def to_query(params: dict) -> dict:
68
+ """
69
+ Converte parâmetros para query string normalizada:
70
+ - snake_case → camelCase
71
+ - listas/tuplas → CSV string
72
+ - booleanos → 'true'/'false' (minúsculo)
73
+ - remove valores None
74
+ """
75
+ if not params:
76
+ return {}
77
+
78
+ out = {}
79
+ for k, v in params.items():
80
+ if v is None:
81
+ continue
82
+ key = _camel(k)
83
+ if isinstance(v, (list, tuple)):
84
+ out[key] = ','.join(map(str, v))
85
+ elif isinstance(v, bool):
86
+ out[key] = 'true' if v else 'false'
87
+ else:
88
+ out[key] = v
89
+ return out
90
+
91
+
92
+ def to_camel_json(obj: Any) -> Any:
93
+ """
94
+ Normaliza payload JSON recursivamente:
95
+ - snake_case → camelCase nas chaves
96
+ - remove valores None
97
+ - mantém estrutura de listas e objetos aninhados
98
+ """
99
+ if isinstance(obj, dict):
100
+ return {_camel(k): to_camel_json(v) for k, v in obj.items() if v is not None}
101
+ elif isinstance(obj, list):
102
+ return [to_camel_json(x) for x in obj]
103
+ else:
104
+ return obj
105
+
106
+
107
+ def _normalize_url(base_url: str, subdomain: str) -> str:
108
+ """Normaliza URL evitando //public/api quando subdomain está vazio"""
109
+ if not subdomain or subdomain.strip() == "":
110
+ return f"{base_url.rstrip('/')}/public/api"
111
+ return f"{base_url.rstrip('/')}/{subdomain.strip()}/public/api"
112
+
113
+
114
+ def _parse_numeric_value(value: Any) -> float:
115
+ """Sanitiza valores numéricos, lidando com vírgulas decimais"""
116
+ if value is None:
117
+ return 0.0
118
+
119
+ if isinstance(value, (int, float)):
120
+ return float(value)
121
+
122
+ # Se for string, tentar converter
123
+ str_value = str(value).strip()
124
+ if not str_value:
125
+ return 0.0
126
+
127
+ # Trocar vírgula por ponto decimal
128
+ str_value = str_value.replace(',', '.')
129
+
130
+ try:
131
+ return float(str_value)
132
+ except (ValueError, TypeError):
133
+ logger.warning(f"Não foi possível converter '{value}' para número")
134
+ return 0.0
135
+
136
+
137
+ def _get_cache_key(endpoint: str, params: dict = None) -> str:
138
+ """Gera chave de cache baseada no endpoint e parâmetros"""
139
+ cache_params = json.dumps(params or {}, sort_keys=True)
140
+ return f"{endpoint}:{hash(cache_params)}"
141
+
142
+
143
+ def _set_cache(key: str, data: Any) -> None:
144
+ """Armazena dados no cache com TTL"""
145
+ _cache[key] = {
146
+ "data": data,
147
+ "timestamp": time.time(),
148
+ "ttl": CACHE_TTL
149
+ }
150
+
151
+
152
+ def _get_cache(key: str) -> Optional[Dict]:
153
+ """Recupera dados do cache se ainda válidos - CORRIGIDO: mantém shape original"""
154
+ if key not in _cache:
155
+ return None
156
+
157
+ cached = _cache[key]
158
+ if time.time() - cached["timestamp"] > cached["ttl"]:
159
+ del _cache[key] # Remove cache expirado
160
+ return None
161
+
162
+ # cached["data"] já é o "result" completo salvo em _set_cache
163
+ result = dict(cached["data"]) # cópia rasa
164
+ result["cache"] = {
165
+ "hit": True,
166
+ "ttl_s": cached["ttl"] - (time.time() - cached["timestamp"])
167
+ }
168
+ return result
169
+
170
+
171
+ def _log_request(method: str, endpoint: str, status_code: int, latency: float, request_id: str) -> None:
172
+ """Log estruturado das requisições"""
173
+ logger.info(
174
+ f"HTTP {method} {endpoint} - Status: {status_code} - "
175
+ f"Latency: {latency:.3f}s - RequestID: {request_id}"
176
+ )
177
+
178
+
179
+ def _extract_items_and_total(resp_data: Any) -> tuple:
180
+ """
181
+ Extrai items e total count de resposta padronizada da API Sienge
182
+ Retorna: (items_list, total_count)
183
+ """
184
+ items = resp_data.get("results", []) if isinstance(resp_data, dict) else (resp_data or [])
185
+ meta = resp_data.get("resultSetMetadata", {}) if isinstance(resp_data, dict) else {}
186
+ total = meta.get("count", len(items))
187
+ return items, total
188
+
189
+
34
190
  async def make_sienge_request(
35
- method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None
191
+ method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None, use_cache: bool = True
36
192
  ) -> Dict:
37
193
  """
38
194
  Função auxiliar para fazer requisições à API do Sienge (v1)
39
195
  Suporta tanto Bearer Token quanto Basic Auth
196
+ MELHORADO: Observabilidade, cache, normalização de parâmetros
40
197
  """
198
+ start_time = time.time()
199
+ req_id = str(uuid.uuid4())
200
+
41
201
  try:
202
+ # Normalizar parâmetros
203
+ normalized_params = to_query(params) if params else None
204
+
205
+ # Verificar cache para operações GET
206
+ cache_key = None
207
+ if method.upper() == "GET" and use_cache and endpoint in ["/customer-types", "/creditors", "/customers"]:
208
+ cache_key = _get_cache_key(endpoint, normalized_params)
209
+ cached_result = _get_cache(cache_key)
210
+ if cached_result:
211
+ cached_result["request_id"] = req_id
212
+ return cached_result
213
+
42
214
  async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
43
- headers = {"Content-Type": "application/json", "Accept": "application/json"}
215
+ headers = {
216
+ "Content-Type": "application/json",
217
+ "Accept": "application/json",
218
+ "X-Request-ID": req_id
219
+ }
44
220
 
45
- # Configurar autenticação e URL
221
+ # Configurar autenticação e URL (corrigindo URLs duplas)
46
222
  auth = None
223
+ base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
47
224
 
48
225
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
49
- # Bearer Token (Recomendado)
50
226
  headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
51
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
227
+ url = f"{base_normalized}/v1{endpoint}"
52
228
  elif SIENGE_USERNAME and SIENGE_PASSWORD:
53
- # Basic Auth usando httpx.BasicAuth
54
229
  auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
55
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
230
+ url = f"{base_normalized}/v1{endpoint}"
56
231
  else:
57
232
  return {
58
233
  "success": False,
59
234
  "error": "No Authentication",
60
235
  "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
236
+ "request_id": req_id
61
237
  }
62
238
 
63
- response = await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
239
+ response = await client.request(
240
+ method=method,
241
+ url=url,
242
+ headers=headers,
243
+ params=normalized_params,
244
+ json=json_data,
245
+ auth=auth
246
+ )
247
+
248
+ latency = time.time() - start_time
249
+ _log_request(method, endpoint, response.status_code, latency, req_id)
64
250
 
65
251
  if response.status_code in [200, 201]:
66
252
  try:
67
- return {"success": True, "data": response.json(), "status_code": response.status_code}
68
- except BaseException:
69
- return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code}
253
+ data = response.json()
254
+ result = {
255
+ "success": True,
256
+ "data": data,
257
+ "status_code": response.status_code,
258
+ "request_id": req_id,
259
+ "latency_ms": round(latency * 1000, 2)
260
+ }
261
+
262
+ # Armazenar no cache se aplicável
263
+ if cache_key and method.upper() == "GET":
264
+ _set_cache(cache_key, result)
265
+ result["cache"] = {"hit": False, "ttl_s": CACHE_TTL}
266
+
267
+ return result
268
+
269
+ except Exception:
270
+ return {
271
+ "success": True,
272
+ "data": {"message": "Success"},
273
+ "status_code": response.status_code,
274
+ "request_id": req_id,
275
+ "latency_ms": round(latency * 1000, 2)
276
+ }
70
277
  else:
71
278
  return {
72
279
  "success": False,
73
280
  "error": f"HTTP {response.status_code}",
74
281
  "message": response.text,
75
282
  "status_code": response.status_code,
283
+ "request_id": req_id,
284
+ "latency_ms": round(latency * 1000, 2)
76
285
  }
77
286
 
78
287
  except httpx.TimeoutException:
79
- return {"success": False, "error": "Timeout", "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"}
288
+ latency = time.time() - start_time
289
+ _log_request(method, endpoint, 408, latency, req_id)
290
+ return {
291
+ "success": False,
292
+ "error": "Timeout",
293
+ "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s",
294
+ "request_id": req_id,
295
+ "latency_ms": round(latency * 1000, 2)
296
+ }
80
297
  except Exception as e:
81
- return {"success": False, "error": str(e), "message": f"Erro na requisição: {str(e)}"}
298
+ latency = time.time() - start_time
299
+ _log_request(method, endpoint, 500, latency, req_id)
300
+ return {
301
+ "success": False,
302
+ "error": str(e),
303
+ "message": f"Erro na requisição: {str(e)}",
304
+ "request_id": req_id,
305
+ "latency_ms": round(latency * 1000, 2)
306
+ }
82
307
 
83
308
 
84
309
  async def make_sienge_bulk_request(
@@ -87,48 +312,348 @@ async def make_sienge_bulk_request(
87
312
  """
88
313
  Função auxiliar para fazer requisições à API bulk-data do Sienge
89
314
  Suporta tanto Bearer Token quanto Basic Auth
315
+ MELHORADO: Observabilidade e normalização de parâmetros
90
316
  """
317
+ start_time = time.time()
318
+ req_id = str(uuid.uuid4())
319
+
91
320
  try:
321
+ # Normalizar parâmetros
322
+ normalized_params = to_query(params) if params else None
323
+
92
324
  async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
93
- headers = {"Content-Type": "application/json", "Accept": "application/json"}
325
+ headers = {
326
+ "Content-Type": "application/json",
327
+ "Accept": "application/json",
328
+ "X-Request-ID": req_id
329
+ }
94
330
 
95
- # Configurar autenticação e URL para bulk-data
331
+ # Configurar autenticação e URL para bulk-data (corrigindo URLs duplas)
96
332
  auth = None
333
+ base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
97
334
 
98
335
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
99
- # Bearer Token (Recomendado)
100
336
  headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
101
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/bulk-data/v1{endpoint}"
337
+ url = f"{base_normalized}/bulk-data/v1{endpoint}"
102
338
  elif SIENGE_USERNAME and SIENGE_PASSWORD:
103
- # Basic Auth usando httpx.BasicAuth
104
339
  auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
105
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/bulk-data/v1{endpoint}"
340
+ url = f"{base_normalized}/bulk-data/v1{endpoint}"
106
341
  else:
107
342
  return {
108
343
  "success": False,
109
344
  "error": "No Authentication",
110
345
  "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
346
+ "request_id": req_id
111
347
  }
112
348
 
113
- response = await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
349
+ response = await client.request(
350
+ method=method,
351
+ url=url,
352
+ headers=headers,
353
+ params=normalized_params,
354
+ json=json_data,
355
+ auth=auth
356
+ )
357
+
358
+ latency = time.time() - start_time
359
+ _log_request(method, f"bulk-data{endpoint}", response.status_code, latency, req_id)
114
360
 
115
- if response.status_code in [200, 201]:
361
+ if response.status_code in [200, 201, 202]:
116
362
  try:
117
- return {"success": True, "data": response.json(), "status_code": response.status_code}
118
- except BaseException:
119
- return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code}
363
+ return {
364
+ "success": True,
365
+ "data": response.json(),
366
+ "status_code": response.status_code,
367
+ "request_id": req_id,
368
+ "latency_ms": round(latency * 1000, 2)
369
+ }
370
+ except Exception:
371
+ return {
372
+ "success": True,
373
+ "data": {"message": "Success"},
374
+ "status_code": response.status_code,
375
+ "request_id": req_id,
376
+ "latency_ms": round(latency * 1000, 2)
377
+ }
120
378
  else:
121
379
  return {
122
380
  "success": False,
123
381
  "error": f"HTTP {response.status_code}",
124
382
  "message": response.text,
125
383
  "status_code": response.status_code,
384
+ "request_id": req_id,
385
+ "latency_ms": round(latency * 1000, 2)
126
386
  }
127
387
 
128
388
  except httpx.TimeoutException:
129
- return {"success": False, "error": "Timeout", "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"}
389
+ latency = time.time() - start_time
390
+ _log_request(method, f"bulk-data{endpoint}", 408, latency, req_id)
391
+ return {
392
+ "success": False,
393
+ "error": "Timeout",
394
+ "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s",
395
+ "request_id": req_id,
396
+ "latency_ms": round(latency * 1000, 2)
397
+ }
130
398
  except Exception as e:
131
- return {"success": False, "error": str(e), "message": f"Erro na requisição bulk-data: {str(e)}"}
399
+ latency = time.time() - start_time
400
+ _log_request(method, f"bulk-data{endpoint}", 500, latency, req_id)
401
+ return {
402
+ "success": False,
403
+ "error": str(e),
404
+ "message": f"Erro na requisição bulk-data: {str(e)}",
405
+ "request_id": req_id,
406
+ "latency_ms": round(latency * 1000, 2)
407
+ }
408
+
409
+
410
+ async def _fetch_bulk_with_polling(method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Dict:
411
+ """
412
+ Faz requisição bulk com polling automático para requests assíncronos (202)
413
+ """
414
+ correlation_id = str(uuid.uuid4())
415
+
416
+ # Fazer requisição inicial
417
+ result = await make_sienge_bulk_request(method, endpoint, params, json_data)
418
+
419
+ # Se não foi 202 ou não tem identifier, retornar resultado direto
420
+ if result.get("status_code") != 202:
421
+ return result
422
+
423
+ data = result.get("data", {})
424
+ if not isinstance(data, dict) or not data.get("identifier"):
425
+ return result
426
+
427
+ # Processar requisição assíncrona com polling
428
+ identifier = data["identifier"]
429
+ request_id = result.get("request_id")
430
+
431
+ logger.info(f"Iniciando polling para bulk request - Identifier: {identifier} - RequestID: {request_id}")
432
+
433
+ max_attempts = 30 # Máximo 5 minutos (30 * 10s)
434
+ attempt = 0
435
+ backoff_delay = 2 # Começar com 2 segundos
436
+
437
+ while attempt < max_attempts:
438
+ attempt += 1
439
+ await asyncio.sleep(backoff_delay)
440
+
441
+ # Verificar status do processamento
442
+ status_result = await make_sienge_bulk_request("GET", f"/async/{identifier}")
443
+
444
+ if not status_result["success"]:
445
+ logger.error(f"Erro ao verificar status do bulk request {identifier}: {status_result.get('error')}")
446
+ break
447
+
448
+ status_data = status_result.get("data", {})
449
+ status = status_data.get("status", "unknown")
450
+
451
+ logger.info(f"Polling attempt {attempt} - Status: {status} - Identifier: {identifier}")
452
+
453
+ if status == "completed":
454
+ # Buscar resultados finais
455
+ all_chunks = []
456
+ chunk_count = status_data.get("chunk_count", 1)
457
+
458
+ chunks_downloaded = 0
459
+ for chunk_num in range(chunk_count):
460
+ try:
461
+ # CORRIGIDO: endpoint aninhado sob /async
462
+ chunk_result = await make_sienge_bulk_request("GET", f"/async/{identifier}/result/{chunk_num}")
463
+ if chunk_result["success"]:
464
+ chunk_data = chunk_result.get("data", {}).get("data", [])
465
+ if isinstance(chunk_data, list):
466
+ all_chunks.extend(chunk_data)
467
+ chunks_downloaded += 1
468
+ except Exception as e:
469
+ logger.warning(f"Erro ao buscar chunk {chunk_num}: {e}")
470
+
471
+ return {
472
+ "success": True,
473
+ "data": all_chunks,
474
+ "async_identifier": identifier,
475
+ "correlation_id": correlation_id,
476
+ "chunks_downloaded": chunks_downloaded,
477
+ "rows_returned": len(all_chunks),
478
+ "polling_attempts": attempt,
479
+ "request_id": request_id
480
+ }
481
+
482
+ elif status == "failed" or status == "error":
483
+ return {
484
+ "success": False,
485
+ "error": "Bulk processing failed",
486
+ "message": status_data.get("error_message", "Processamento bulk falhou"),
487
+ "async_identifier": identifier,
488
+ "correlation_id": correlation_id,
489
+ "polling_attempts": attempt,
490
+ "request_id": request_id
491
+ }
492
+
493
+ # Aumentar delay progressivamente (backoff exponencial limitado)
494
+ backoff_delay = min(backoff_delay * 1.5, 10) # Máximo 10 segundos
495
+
496
+ # Timeout do polling
497
+ return {
498
+ "success": False,
499
+ "error": "Polling timeout",
500
+ "message": f"Processamento bulk não completou em {max_attempts} tentativas",
501
+ "async_identifier": identifier,
502
+ "correlation_id": correlation_id,
503
+ "polling_attempts": attempt,
504
+ "request_id": request_id
505
+ }
506
+
507
+
508
+ # ============ CAMADA DE SERVIÇOS (FUNÇÕES INTERNAS) ============
509
+
510
+ async def _svc_get_customer_types() -> Dict:
511
+ """Serviço interno: buscar tipos de clientes"""
512
+ return await make_sienge_request("GET", "/customer-types", use_cache=True)
513
+
514
+
515
+ async def _svc_get_customers(*, limit: int = 50, offset: int = 0, search: str = None, customer_type_id: str = None) -> Dict:
516
+ """Serviço interno: buscar clientes"""
517
+ params = {"limit": min(limit or 50, 200), "offset": offset or 0}
518
+ if search:
519
+ params["search"] = search
520
+ if customer_type_id:
521
+ params["customer_type_id"] = customer_type_id
522
+
523
+ return await make_sienge_request("GET", "/customers", params=params)
524
+
525
+
526
+ async def _svc_get_creditors(*, limit: int = 50, offset: int = 0, search: str = None) -> Dict:
527
+ """Serviço interno: buscar credores/fornecedores"""
528
+ params = {"limit": min(limit or 50, 200), "offset": offset or 0}
529
+ if search:
530
+ params["search"] = search
531
+
532
+ return await make_sienge_request("GET", "/creditors", params=params)
533
+
534
+
535
+ async def _svc_get_creditor_bank_info(*, creditor_id: str) -> Dict:
536
+ """Serviço interno: informações bancárias de credor"""
537
+ return await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
538
+
539
+
540
+ async def _svc_get_projects(*, limit: int = 100, offset: int = 0, company_id: int = None,
541
+ enterprise_type: int = None, receivable_register: str = None,
542
+ only_buildings_enabled: bool = False) -> Dict:
543
+ """Serviço interno: buscar empreendimentos/projetos"""
544
+ params = {"limit": min(limit or 100, 200), "offset": offset or 0}
545
+
546
+ if company_id:
547
+ params["company_id"] = company_id
548
+ if enterprise_type:
549
+ params["type"] = enterprise_type
550
+ if receivable_register:
551
+ params["receivable_register"] = receivable_register
552
+ if only_buildings_enabled:
553
+ params["only_buildings_enabled_for_integration"] = only_buildings_enabled
554
+
555
+ return await make_sienge_request("GET", "/enterprises", params=params)
556
+
557
+
558
+ async def _svc_get_bills(*, start_date: str = None, end_date: str = None, creditor_id: str = None,
559
+ status: str = None, limit: int = 50) -> Dict:
560
+ """Serviço interno: buscar títulos a pagar"""
561
+ # Se start_date não fornecido, usar últimos 30 dias
562
+ if not start_date:
563
+ start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
564
+
565
+ # Se end_date não fornecido, usar hoje
566
+ if not end_date:
567
+ end_date = datetime.now().strftime("%Y-%m-%d")
568
+
569
+ params = {"start_date": start_date, "end_date": end_date, "limit": min(limit or 50, 200)}
570
+
571
+ if creditor_id:
572
+ params["creditor_id"] = creditor_id
573
+ if status:
574
+ params["status"] = status
575
+
576
+ return await make_sienge_request("GET", "/bills", params=params)
577
+
578
+
579
+ async def _svc_get_accounts_receivable(*, start_date: str, end_date: str, selection_type: str = "D",
580
+ company_id: int = None, cost_centers_id: List[int] = None,
581
+ correction_indexer_id: int = None, correction_date: str = None,
582
+ change_start_date: str = None, completed_bills: str = None,
583
+ origins_ids: List[str] = None, bearers_id_in: List[int] = None,
584
+ bearers_id_not_in: List[int] = None) -> Dict:
585
+ """Serviço interno: buscar contas a receber via bulk-data"""
586
+ params = {"start_date": start_date, "end_date": end_date, "selection_type": selection_type}
587
+
588
+ if company_id:
589
+ params["company_id"] = company_id
590
+ if cost_centers_id:
591
+ params["cost_centers_id"] = cost_centers_id
592
+ if correction_indexer_id:
593
+ params["correction_indexer_id"] = correction_indexer_id
594
+ if correction_date:
595
+ params["correction_date"] = correction_date
596
+ if change_start_date:
597
+ params["change_start_date"] = change_start_date
598
+ if completed_bills:
599
+ params["completed_bills"] = completed_bills
600
+ if origins_ids:
601
+ params["origins_ids"] = origins_ids
602
+ if bearers_id_in:
603
+ params["bearers_id_in"] = bearers_id_in
604
+ if bearers_id_not_in:
605
+ params["bearers_id_not_in"] = bearers_id_not_in
606
+
607
+ return await _fetch_bulk_with_polling("GET", "/income", params=params)
608
+
609
+
610
+ async def _svc_get_accounts_receivable_by_bills(*, bills_ids: List[int], correction_indexer_id: int = None,
611
+ correction_date: str = None) -> Dict:
612
+ """Serviço interno: buscar contas a receber por títulos específicos"""
613
+ params = {"bills_ids": bills_ids}
614
+
615
+ if correction_indexer_id:
616
+ params["correction_indexer_id"] = correction_indexer_id
617
+ if correction_date:
618
+ params["correction_date"] = correction_date
619
+
620
+ return await _fetch_bulk_with_polling("GET", "/income/by-bills", params=params)
621
+
622
+
623
+ async def _svc_get_purchase_orders(*, purchase_order_id: str = None, status: str = None,
624
+ date_from: str = None, limit: int = 50) -> Dict:
625
+ """Serviço interno: buscar pedidos de compra"""
626
+ if purchase_order_id:
627
+ return await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}")
628
+
629
+ params = {"limit": min(limit or 50, 200)}
630
+ if status:
631
+ params["status"] = status
632
+ if date_from:
633
+ params["date_from"] = date_from
634
+
635
+ return await make_sienge_request("GET", "/purchase-orders", params=params)
636
+
637
+
638
+ async def _svc_get_purchase_requests(*, purchase_request_id: str = None, limit: int = 50, status: str = None) -> Dict:
639
+ """Serviço interno: buscar solicitações de compra"""
640
+ if purchase_request_id:
641
+ return await make_sienge_request("GET", f"/purchase-requests/{purchase_request_id}")
642
+
643
+ params = {"limit": min(limit or 50, 200)}
644
+ if status:
645
+ params["status"] = status
646
+
647
+ return await make_sienge_request("GET", "/purchase-requests", params=params)
648
+
649
+
650
+ async def _svc_get_purchase_invoices(*, limit: int = 50, date_from: str = None) -> Dict:
651
+ """Serviço interno: listar notas fiscais de compra"""
652
+ params = {"limit": min(limit or 50, 200)}
653
+ if date_from:
654
+ params["date_from"] = date_from
655
+
656
+ return await make_sienge_request("GET", "/purchase-invoices", params=params)
132
657
 
133
658
 
134
659
  # ============ CONEXÃO E TESTE ============
@@ -138,8 +663,8 @@ async def make_sienge_bulk_request(
138
663
  async def test_sienge_connection() -> Dict:
139
664
  """Testa a conexão com a API do Sienge"""
140
665
  try:
141
- # Tentar endpoint mais simples primeiro
142
- result = await make_sienge_request("GET", "/customer-types")
666
+ # Usar serviço interno
667
+ result = await _svc_get_customer_types()
143
668
 
144
669
  if result["success"]:
145
670
  auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
@@ -149,6 +674,9 @@ async def test_sienge_connection() -> Dict:
149
674
  "api_status": "Online",
150
675
  "auth_method": auth_method,
151
676
  "timestamp": datetime.now().isoformat(),
677
+ "request_id": result.get("request_id"),
678
+ "latency_ms": result.get("latency_ms"),
679
+ "cache": result.get("cache")
152
680
  }
153
681
  else:
154
682
  return {
@@ -157,6 +685,7 @@ async def test_sienge_connection() -> Dict:
157
685
  "error": result.get("error"),
158
686
  "details": result.get("message"),
159
687
  "timestamp": datetime.now().isoformat(),
688
+ "request_id": result.get("request_id")
160
689
  }
161
690
  except Exception as e:
162
691
  return {
@@ -183,14 +712,13 @@ async def get_sienge_customers(
183
712
  search: Buscar por nome ou documento
184
713
  customer_type_id: Filtrar por tipo de cliente
185
714
  """
186
- params = {"limit": min(limit or 50, 200), "offset": offset or 0}
187
-
188
- if search:
189
- params["search"] = search
190
- if customer_type_id:
191
- params["customer_type_id"] = customer_type_id
192
-
193
- result = await make_sienge_request("GET", "/customers", params=params)
715
+ # Usar serviço interno
716
+ result = await _svc_get_customers(
717
+ limit=limit or 50,
718
+ offset=offset or 0,
719
+ search=search,
720
+ customer_type_id=customer_type_id
721
+ )
194
722
 
195
723
  if result["success"]:
196
724
  data = result["data"]
@@ -203,7 +731,13 @@ async def get_sienge_customers(
203
731
  "message": f"✅ Encontrados {len(customers)} clientes (total: {total_count})",
204
732
  "customers": customers,
205
733
  "count": len(customers),
206
- "filters_applied": params,
734
+ "total_count": total_count,
735
+ "filters_applied": {
736
+ "limit": limit, "offset": offset, "search": search, "customer_type_id": customer_type_id
737
+ },
738
+ "request_id": result.get("request_id"),
739
+ "latency_ms": result.get("latency_ms"),
740
+ "cache": result.get("cache")
207
741
  }
208
742
 
209
743
  return {
@@ -211,13 +745,15 @@ async def get_sienge_customers(
211
745
  "message": "❌ Erro ao buscar clientes",
212
746
  "error": result.get("error"),
213
747
  "details": result.get("message"),
748
+ "request_id": result.get("request_id")
214
749
  }
215
750
 
216
751
 
217
752
  @mcp.tool
218
753
  async def get_sienge_customer_types() -> Dict:
219
754
  """Lista tipos de clientes disponíveis"""
220
- result = await make_sienge_request("GET", "/customer-types")
755
+ # Usar serviço interno
756
+ result = await _svc_get_customer_types()
221
757
 
222
758
  if result["success"]:
223
759
  data = result["data"]
@@ -230,6 +766,10 @@ async def get_sienge_customer_types() -> Dict:
230
766
  "message": f"✅ Encontrados {len(customer_types)} tipos de clientes (total: {total_count})",
231
767
  "customer_types": customer_types,
232
768
  "count": len(customer_types),
769
+ "total_count": total_count,
770
+ "request_id": result.get("request_id"),
771
+ "latency_ms": result.get("latency_ms"),
772
+ "cache": result.get("cache")
233
773
  }
234
774
 
235
775
  return {
@@ -237,6 +777,193 @@ async def get_sienge_customer_types() -> Dict:
237
777
  "message": "❌ Erro ao buscar tipos de clientes",
238
778
  "error": result.get("error"),
239
779
  "details": result.get("message"),
780
+ "request_id": result.get("request_id")
781
+ }
782
+
783
+
784
+ # ============ ALIAS COMPATÍVEIS COM CHECKLIST ============
785
+
786
+ @mcp.tool
787
+ async def get_sienge_enterprises(
788
+ limit: int = 100, offset: int = 0, company_id: int = None, enterprise_type: int = None
789
+ ) -> Dict:
790
+ """
791
+ ALIAS: get_sienge_projects → get_sienge_enterprises
792
+ Busca empreendimentos/obras (compatibilidade com checklist)
793
+ """
794
+ return await get_sienge_projects(
795
+ limit=limit,
796
+ offset=offset,
797
+ company_id=company_id,
798
+ enterprise_type=enterprise_type
799
+ )
800
+
801
+
802
+ @mcp.tool
803
+ async def get_sienge_suppliers(
804
+ limit: int = 50,
805
+ offset: int = 0,
806
+ search: str = None
807
+ ) -> Dict:
808
+ """
809
+ ALIAS: get_sienge_creditors → get_sienge_suppliers
810
+ Busca fornecedores (compatibilidade com checklist)
811
+ """
812
+ return await get_sienge_creditors(limit=limit, offset=offset, search=search)
813
+
814
+
815
+ @mcp.tool
816
+ async def search_sienge_finances(
817
+ period_start: str,
818
+ period_end: str,
819
+ account_type: Optional[str] = None,
820
+ cost_center: Optional[str] = None, # ignorado por enquanto (não suportado na API atual)
821
+ amount_filter: Optional[str] = None,
822
+ customer_creditor: Optional[str] = None
823
+ ) -> Dict:
824
+ """
825
+ ALIAS: search_sienge_financial_data → search_sienge_finances
826
+ - account_type: receivable | payable | both
827
+ - amount_filter: "100..500", ">=1000", "<=500", ">100", "<200", "=750"
828
+ - customer_creditor: termo de busca (cliente/credor)
829
+ """
830
+ # 1) mapear tipo
831
+ search_type = (account_type or "both").lower()
832
+ if search_type not in {"receivable", "payable", "both"}:
833
+ search_type = "both"
834
+
835
+ # 2) parse de faixa de valores
836
+ amount_min = amount_max = None
837
+ if amount_filter:
838
+ s = amount_filter.replace(" ", "")
839
+ try:
840
+ if ".." in s:
841
+ lo, hi = s.split("..", 1)
842
+ amount_min = float(lo) if lo else None
843
+ amount_max = float(hi) if hi else None
844
+ elif s.startswith(">="):
845
+ amount_min = float(s[2:])
846
+ elif s.startswith("<="):
847
+ amount_max = float(s[2:])
848
+ elif s.startswith(">"):
849
+ # >x → min = x (estrito não suportado; aproximamos)
850
+ amount_min = float(s[1:])
851
+ elif s.startswith("<"):
852
+ amount_max = float(s[1:])
853
+ elif s.startswith("="):
854
+ v = float(s[1:])
855
+ amount_min = v
856
+ amount_max = v
857
+ else:
858
+ # número puro → min
859
+ amount_min = float(s)
860
+ except ValueError:
861
+ # filtro inválido → ignora silenciosamente
862
+ amount_min = amount_max = None
863
+
864
+ return await search_sienge_financial_data(
865
+ period_start=period_start,
866
+ period_end=period_end,
867
+ search_type=search_type,
868
+ amount_min=amount_min,
869
+ amount_max=amount_max,
870
+ customer_creditor_search=customer_creditor
871
+ )
872
+
873
+
874
+ @mcp.tool
875
+ async def get_sienge_accounts_payable(
876
+ start_date: str = None, end_date: str = None, creditor_id: str = None,
877
+ status: str = None, limit: int = 50
878
+ ) -> Dict:
879
+ """
880
+ ALIAS: get_sienge_bills → get_sienge_accounts_payable
881
+ Busca contas a pagar (compatibilidade com checklist)
882
+ """
883
+ return await get_sienge_bills(
884
+ start_date=start_date,
885
+ end_date=end_date,
886
+ creditor_id=creditor_id,
887
+ status=status,
888
+ limit=limit
889
+ )
890
+
891
+
892
+ @mcp.tool
893
+ async def list_sienge_purchase_invoices(limit: int = 50, date_from: str = None) -> Dict:
894
+ """
895
+ Lista notas fiscais de compra (versão list/plural esperada pelo checklist)
896
+
897
+ Args:
898
+ limit: Máximo de registros (padrão: 50, máx: 200)
899
+ date_from: Data inicial (YYYY-MM-DD)
900
+ """
901
+ # Usar serviço interno
902
+ result = await _svc_get_purchase_invoices(limit=limit, date_from=date_from)
903
+
904
+ if result["success"]:
905
+ data = result["data"]
906
+ invoices = data.get("results", []) if isinstance(data, dict) else data
907
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
908
+ total_count = metadata.get("count", len(invoices))
909
+
910
+ return {
911
+ "success": True,
912
+ "message": f"✅ Encontradas {len(invoices)} notas fiscais de compra (total: {total_count})",
913
+ "purchase_invoices": invoices,
914
+ "count": len(invoices),
915
+ "total_count": total_count,
916
+ "filters_applied": {"limit": limit, "date_from": date_from},
917
+ "request_id": result.get("request_id"),
918
+ "latency_ms": result.get("latency_ms"),
919
+ "cache": result.get("cache")
920
+ }
921
+
922
+ return {
923
+ "success": False,
924
+ "message": "❌ Erro ao buscar notas fiscais de compra",
925
+ "error": result.get("error"),
926
+ "details": result.get("message"),
927
+ "request_id": result.get("request_id")
928
+ }
929
+
930
+
931
+ @mcp.tool
932
+ async def list_sienge_purchase_requests(limit: int = 50, status: str = None) -> Dict:
933
+ """
934
+ Lista solicitações de compra (versão list/plural esperada pelo checklist)
935
+
936
+ Args:
937
+ limit: Máximo de registros (padrão: 50, máx: 200)
938
+ status: Status da solicitação
939
+ """
940
+ # Usar serviço interno
941
+ result = await _svc_get_purchase_requests(limit=limit, status=status)
942
+
943
+ if result["success"]:
944
+ data = result["data"]
945
+ requests = data.get("results", []) if isinstance(data, dict) else data
946
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
947
+ total_count = metadata.get("count", len(requests))
948
+
949
+ return {
950
+ "success": True,
951
+ "message": f"✅ Encontradas {len(requests)} solicitações de compra (total: {total_count})",
952
+ "purchase_requests": requests,
953
+ "count": len(requests),
954
+ "total_count": total_count,
955
+ "filters_applied": {"limit": limit, "status": status},
956
+ "request_id": result.get("request_id"),
957
+ "latency_ms": result.get("latency_ms"),
958
+ "cache": result.get("cache")
959
+ }
960
+
961
+ return {
962
+ "success": False,
963
+ "message": "❌ Erro ao buscar solicitações de compra",
964
+ "error": result.get("error"),
965
+ "details": result.get("message"),
966
+ "request_id": result.get("request_id")
240
967
  }
241
968
 
242
969
 
@@ -253,11 +980,12 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
253
980
  offset: Pular registros (padrão: 0)
254
981
  search: Buscar por nome
255
982
  """
256
- params = {"limit": min(limit or 50, 200), "offset": offset or 0}
257
- if search:
258
- params["search"] = search
259
-
260
- result = await make_sienge_request("GET", "/creditors", params=params)
983
+ # Usar serviço interno
984
+ result = await _svc_get_creditors(
985
+ limit=limit or 50,
986
+ offset=offset or 0,
987
+ search=search
988
+ )
261
989
 
262
990
  if result["success"]:
263
991
  data = result["data"]
@@ -270,6 +998,11 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
270
998
  "message": f"✅ Encontrados {len(creditors)} credores (total: {total_count})",
271
999
  "creditors": creditors,
272
1000
  "count": len(creditors),
1001
+ "total_count": total_count,
1002
+ "filters_applied": {"limit": limit, "offset": offset, "search": search},
1003
+ "request_id": result.get("request_id"),
1004
+ "latency_ms": result.get("latency_ms"),
1005
+ "cache": result.get("cache")
273
1006
  }
274
1007
 
275
1008
  return {
@@ -277,6 +1010,7 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
277
1010
  "message": "❌ Erro ao buscar credores",
278
1011
  "error": result.get("error"),
279
1012
  "details": result.get("message"),
1013
+ "request_id": result.get("request_id")
280
1014
  }
281
1015
 
282
1016
 
@@ -288,7 +1022,8 @@ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
288
1022
  Args:
289
1023
  creditor_id: ID do credor (obrigatório)
290
1024
  """
291
- result = await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
1025
+ # Usar serviço interno
1026
+ result = await _svc_get_creditor_bank_info(creditor_id=creditor_id)
292
1027
 
293
1028
  if result["success"]:
294
1029
  return {
@@ -296,6 +1031,8 @@ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
296
1031
  "message": f"✅ Informações bancárias do credor {creditor_id}",
297
1032
  "creditor_id": creditor_id,
298
1033
  "bank_info": result["data"],
1034
+ "request_id": result.get("request_id"),
1035
+ "latency_ms": result.get("latency_ms")
299
1036
  }
300
1037
 
301
1038
  return {
@@ -303,6 +1040,7 @@ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
303
1040
  "message": f"❌ Erro ao buscar info bancária do credor {creditor_id}",
304
1041
  "error": result.get("error"),
305
1042
  "details": result.get("message"),
1043
+ "request_id": result.get("request_id")
306
1044
  }
307
1045
 
308
1046
 
@@ -326,6 +1064,7 @@ async def get_sienge_accounts_receivable(
326
1064
  ) -> Dict:
327
1065
  """
328
1066
  Consulta parcelas do contas a receber via API bulk-data
1067
+ MELHORADO: Suporte a polling assíncrono para requests 202
329
1068
 
330
1069
  Args:
331
1070
  start_date: Data de início do período (YYYY-MM-DD) - OBRIGATÓRIO
@@ -341,48 +1080,61 @@ async def get_sienge_accounts_receivable(
341
1080
  bearers_id_in: Filtrar parcelas com códigos de portador específicos
342
1081
  bearers_id_not_in: Filtrar parcelas excluindo códigos de portador específicos
343
1082
  """
344
- params = {"startDate": start_date, "endDate": end_date, "selectionType": selection_type}
345
-
346
- if company_id:
347
- params["companyId"] = company_id
348
- if cost_centers_id:
349
- params["costCentersId"] = cost_centers_id
350
- if correction_indexer_id:
351
- params["correctionIndexerId"] = correction_indexer_id
352
- if correction_date:
353
- params["correctionDate"] = correction_date
354
- if change_start_date:
355
- params["changeStartDate"] = change_start_date
356
- if completed_bills:
357
- params["completedBills"] = completed_bills
358
- if origins_ids:
359
- params["originsIds"] = origins_ids
360
- if bearers_id_in:
361
- params["bearersIdIn"] = bearers_id_in
362
- if bearers_id_not_in:
363
- params["bearersIdNotIn"] = bearers_id_not_in
364
-
365
- result = await make_sienge_bulk_request("GET", "/income", params=params)
1083
+ # Usar serviço interno com polling assíncrono
1084
+ result = await _svc_get_accounts_receivable(
1085
+ start_date=start_date,
1086
+ end_date=end_date,
1087
+ selection_type=selection_type,
1088
+ company_id=company_id,
1089
+ cost_centers_id=cost_centers_id,
1090
+ correction_indexer_id=correction_indexer_id,
1091
+ correction_date=correction_date,
1092
+ change_start_date=change_start_date,
1093
+ completed_bills=completed_bills,
1094
+ origins_ids=origins_ids,
1095
+ bearers_id_in=bearers_id_in,
1096
+ bearers_id_not_in=bearers_id_not_in
1097
+ )
366
1098
 
367
1099
  if result["success"]:
368
- data = result["data"]
369
- income_data = data.get("data", []) if isinstance(data, dict) else data
370
-
371
- return {
1100
+ # Para requests normais (200) e assíncronos processados
1101
+ income_data = result.get("data", [])
1102
+
1103
+ response = {
372
1104
  "success": True,
373
1105
  "message": f"✅ Encontradas {len(income_data)} parcelas a receber",
374
1106
  "income_data": income_data,
375
1107
  "count": len(income_data),
376
1108
  "period": f"{start_date} a {end_date}",
377
1109
  "selection_type": selection_type,
378
- "filters": params,
1110
+ "request_id": result.get("request_id"),
1111
+ "latency_ms": result.get("latency_ms")
379
1112
  }
1113
+
1114
+ # Se foi processamento assíncrono, incluir informações extras
1115
+ if result.get("async_identifier"):
1116
+ response.update({
1117
+ "async_processing": {
1118
+ "identifier": result.get("async_identifier"),
1119
+ "correlation_id": result.get("correlation_id"),
1120
+ "chunks_downloaded": result.get("chunks_downloaded"),
1121
+ "rows_returned": result.get("rows_returned"),
1122
+ "polling_attempts": result.get("polling_attempts")
1123
+ }
1124
+ })
1125
+
1126
+ return response
380
1127
 
381
1128
  return {
382
1129
  "success": False,
383
1130
  "message": "❌ Erro ao buscar parcelas a receber",
384
1131
  "error": result.get("error"),
385
1132
  "details": result.get("message"),
1133
+ "request_id": result.get("request_id"),
1134
+ "async_info": {
1135
+ "identifier": result.get("async_identifier"),
1136
+ "polling_attempts": result.get("polling_attempts")
1137
+ } if result.get("async_identifier") else None
386
1138
  }
387
1139
 
388
1140
 
@@ -392,39 +1144,57 @@ async def get_sienge_accounts_receivable_by_bills(
392
1144
  ) -> Dict:
393
1145
  """
394
1146
  Consulta parcelas dos títulos informados via API bulk-data
1147
+ MELHORADO: Suporte a polling assíncrono para requests 202
395
1148
 
396
1149
  Args:
397
1150
  bills_ids: Lista de códigos dos títulos - OBRIGATÓRIO
398
1151
  correction_indexer_id: Código do indexador de correção
399
1152
  correction_date: Data para correção do indexador (YYYY-MM-DD)
400
1153
  """
401
- params = {"billsIds": bills_ids}
402
-
403
- if correction_indexer_id:
404
- params["correctionIndexerId"] = correction_indexer_id
405
- if correction_date:
406
- params["correctionDate"] = correction_date
407
-
408
- result = await make_sienge_bulk_request("GET", "/income/by-bills", params=params)
1154
+ # Usar serviço interno com polling assíncrono
1155
+ result = await _svc_get_accounts_receivable_by_bills(
1156
+ bills_ids=bills_ids,
1157
+ correction_indexer_id=correction_indexer_id,
1158
+ correction_date=correction_date
1159
+ )
409
1160
 
410
1161
  if result["success"]:
411
- data = result["data"]
412
- income_data = data.get("data", []) if isinstance(data, dict) else data
413
-
414
- return {
1162
+ income_data = result.get("data", [])
1163
+
1164
+ response = {
415
1165
  "success": True,
416
1166
  "message": f"✅ Encontradas {len(income_data)} parcelas dos títulos informados",
417
1167
  "income_data": income_data,
418
1168
  "count": len(income_data),
419
1169
  "bills_consulted": bills_ids,
420
- "filters": params,
1170
+ "request_id": result.get("request_id"),
1171
+ "latency_ms": result.get("latency_ms")
421
1172
  }
1173
+
1174
+ # Se foi processamento assíncrono, incluir informações extras
1175
+ if result.get("async_identifier"):
1176
+ response.update({
1177
+ "async_processing": {
1178
+ "identifier": result.get("async_identifier"),
1179
+ "correlation_id": result.get("correlation_id"),
1180
+ "chunks_downloaded": result.get("chunks_downloaded"),
1181
+ "rows_returned": result.get("rows_returned"),
1182
+ "polling_attempts": result.get("polling_attempts")
1183
+ }
1184
+ })
1185
+
1186
+ return response
422
1187
 
423
1188
  return {
424
1189
  "success": False,
425
1190
  "message": "❌ Erro ao buscar parcelas dos títulos informados",
426
1191
  "error": result.get("error"),
427
1192
  "details": result.get("message"),
1193
+ "request_id": result.get("request_id"),
1194
+ "async_info": {
1195
+ "identifier": result.get("async_identifier"),
1196
+ "polling_attempts": result.get("polling_attempts")
1197
+ } if result.get("async_identifier") else None
428
1198
  }
429
1199
 
430
1200
 
@@ -446,26 +1216,14 @@ async def get_sienge_bills(
446
1216
  status: Status do título (ex: open, paid, cancelled)
447
1217
  limit: Máximo de registros (padrão: 50, máx: 200)
448
1218
  """
449
- from datetime import datetime, timedelta
450
-
451
- # Se start_date não fornecido, usar últimos 30 dias
452
- if not start_date:
453
- start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
454
-
455
- # Se end_date não fornecido, usar hoje
456
- if not end_date:
457
- end_date = datetime.now().strftime("%Y-%m-%d")
458
-
459
- # Parâmetros obrigatórios
460
- params = {"startDate": start_date, "endDate": end_date, "limit": min(limit or 50, 200)} # OBRIGATÓRIO pela API
461
-
462
- # Parâmetros opcionais
463
- if creditor_id:
464
- params["creditor_id"] = creditor_id
465
- if status:
466
- params["status"] = status
467
-
468
- result = await make_sienge_request("GET", "/bills", params=params)
1219
+ # Usar serviço interno
1220
+ result = await _svc_get_bills(
1221
+ start_date=start_date,
1222
+ end_date=end_date,
1223
+ creditor_id=creditor_id,
1224
+ status=status,
1225
+ limit=limit or 50
1226
+ )
469
1227
 
470
1228
  if result["success"]:
471
1229
  data = result["data"]
@@ -473,14 +1231,33 @@ async def get_sienge_bills(
473
1231
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
474
1232
  total_count = metadata.get("count", len(bills))
475
1233
 
1234
+ # Aplicar parsing numérico nos valores
1235
+ for bill in bills:
1236
+ if "amount" in bill:
1237
+ bill["amount"] = _parse_numeric_value(bill["amount"])
1238
+ if "paid_amount" in bill:
1239
+ bill["paid_amount"] = _parse_numeric_value(bill["paid_amount"])
1240
+ if "remaining_amount" in bill:
1241
+ bill["remaining_amount"] = _parse_numeric_value(bill["remaining_amount"])
1242
+
1243
+ # Usar datas padrão se não fornecidas
1244
+ actual_start = start_date or (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
1245
+ actual_end = end_date or datetime.now().strftime("%Y-%m-%d")
1246
+
476
1247
  return {
477
1248
  "success": True,
478
- "message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {start_date} a {end_date}",
1249
+ "message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {actual_start} a {actual_end}",
479
1250
  "bills": bills,
480
1251
  "count": len(bills),
481
1252
  "total_count": total_count,
482
- "period": {"start_date": start_date, "end_date": end_date},
483
- "filters": params,
1253
+ "period": {"start_date": actual_start, "end_date": actual_end},
1254
+ "filters_applied": {
1255
+ "start_date": actual_start, "end_date": actual_end,
1256
+ "creditor_id": creditor_id, "status": status, "limit": limit
1257
+ },
1258
+ "request_id": result.get("request_id"),
1259
+ "latency_ms": result.get("latency_ms"),
1260
+ "cache": result.get("cache")
484
1261
  }
485
1262
 
486
1263
  return {
@@ -488,6 +1265,7 @@ async def get_sienge_bills(
488
1265
  "message": "❌ Erro ao buscar títulos a pagar",
489
1266
  "error": result.get("error"),
490
1267
  "details": result.get("message"),
1268
+ "request_id": result.get("request_id")
491
1269
  }
492
1270
 
493
1271
 
@@ -533,6 +1311,8 @@ async def get_sienge_purchase_orders(
533
1311
  "message": f"✅ Encontrados {len(orders)} pedidos de compra",
534
1312
  "purchase_orders": orders,
535
1313
  "count": len(orders),
1314
+ "request_id": result.get("request_id"),
1315
+ "latency_ms": result.get("latency_ms")
536
1316
  }
537
1317
 
538
1318
  return {
@@ -540,6 +1320,8 @@ async def get_sienge_purchase_orders(
540
1320
  "message": "❌ Erro ao buscar pedidos de compra",
541
1321
  "error": result.get("error"),
542
1322
  "details": result.get("message"),
1323
+ "request_id": result.get("request_id"),
1324
+ "latency_ms": result.get("latency_ms")
543
1325
  }
544
1326
 
545
1327
 
@@ -631,14 +1413,18 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
631
1413
  "date": datetime.now().strftime("%Y-%m-%d"),
632
1414
  }
633
1415
 
634
- result = await make_sienge_request("POST", "/purchase-requests", json_data=request_data)
1416
+ # CORRIGIDO: Normalizar JSON payload
1417
+ json_data = to_camel_json(request_data)
1418
+ result = await make_sienge_request("POST", "/purchase-requests", json_data=json_data)
635
1419
 
636
1420
  if result["success"]:
637
1421
  return {
638
1422
  "success": True,
639
1423
  "message": "✅ Solicitação de compra criada com sucesso",
640
- "request_id": result["data"].get("id"),
1424
+ "request_id": result.get("request_id"),
1425
+ "purchase_request_id": result["data"].get("id"),
641
1426
  "data": result["data"],
1427
+ "latency_ms": result.get("latency_ms")
642
1428
  }
643
1429
 
644
1430
  return {
@@ -646,6 +1432,7 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
646
1432
  "message": "❌ Erro ao criar solicitação de compra",
647
1433
  "error": result.get("error"),
648
1434
  "details": result.get("message"),
1435
+ "request_id": result.get("request_id")
649
1436
  }
650
1437
 
651
1438
 
@@ -731,13 +1518,13 @@ async def create_sienge_purchase_invoice(
731
1518
  notes: Observações (opcional)
732
1519
  """
733
1520
  invoice_data = {
734
- "documentId": document_id,
1521
+ "document_id": document_id,
735
1522
  "number": number,
736
- "supplierId": supplier_id,
737
- "companyId": company_id,
738
- "movementTypeId": movement_type_id,
739
- "movementDate": movement_date,
740
- "issueDate": issue_date,
1523
+ "supplier_id": supplier_id,
1524
+ "company_id": company_id,
1525
+ "movement_type_id": movement_type_id,
1526
+ "movement_date": movement_date,
1527
+ "issue_date": issue_date,
741
1528
  }
742
1529
 
743
1530
  if series:
@@ -745,7 +1532,7 @@ async def create_sienge_purchase_invoice(
745
1532
  if notes:
746
1533
  invoice_data["notes"] = notes
747
1534
 
748
- result = await make_sienge_request("POST", "/purchase-invoices", json_data=invoice_data)
1535
+ result = await make_sienge_request("POST", "/purchase-invoices", json_data=to_camel_json(invoice_data))
749
1536
 
750
1537
  if result["success"]:
751
1538
  return {"success": True, "message": f"✅ Nota fiscal {number} criada com sucesso", "invoice": result["data"]}
@@ -782,14 +1569,14 @@ async def add_items_to_purchase_invoice(
782
1569
  copy_attachments_purchase_orders: Copiar anexos dos pedidos de compra
783
1570
  """
784
1571
  item_data = {
785
- "deliveriesOrder": deliveries_order,
786
- "copyNotesPurchaseOrders": copy_notes_purchase_orders,
787
- "copyNotesResources": copy_notes_resources,
788
- "copyAttachmentsPurchaseOrders": copy_attachments_purchase_orders,
1572
+ "deliveries_order": deliveries_order,
1573
+ "copy_notes_purchase_orders": copy_notes_purchase_orders,
1574
+ "copy_notes_resources": copy_notes_resources,
1575
+ "copy_attachments_purchase_orders": copy_attachments_purchase_orders,
789
1576
  }
790
1577
 
791
1578
  result = await make_sienge_request(
792
- "POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=item_data
1579
+ "POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=to_camel_json(item_data)
793
1580
  )
794
1581
 
795
1582
  if result["success"]:
@@ -950,6 +1737,7 @@ async def get_sienge_projects(
950
1737
  ) -> Dict:
951
1738
  """
952
1739
  Busca empreendimentos/obras no Sienge
1740
+ CORRIGIDO: Mapeamento correto da chave de resposta
953
1741
 
954
1742
  Args:
955
1743
  limit: Máximo de registros (padrão: 100, máximo: 200)
@@ -959,31 +1747,39 @@ async def get_sienge_projects(
959
1747
  receivable_register: Filtro de registro de recebíveis (B3, CERC)
960
1748
  only_buildings_enabled: Retornar apenas obras habilitadas para integração orçamentária
961
1749
  """
962
- params = {"limit": min(limit or 100, 200), "offset": offset or 0}
963
-
964
- if company_id:
965
- params["companyId"] = company_id
966
- if enterprise_type:
967
- params["type"] = enterprise_type
968
- if receivable_register:
969
- params["receivableRegister"] = receivable_register
970
- if only_buildings_enabled:
971
- params["onlyBuildingsEnabledForIntegration"] = only_buildings_enabled
972
-
973
- result = await make_sienge_request("GET", "/enterprises", params=params)
1750
+ # Usar serviço interno
1751
+ result = await _svc_get_projects(
1752
+ limit=limit or 100,
1753
+ offset=offset or 0,
1754
+ company_id=company_id,
1755
+ enterprise_type=enterprise_type,
1756
+ receivable_register=receivable_register,
1757
+ only_buildings_enabled=only_buildings_enabled or False
1758
+ )
974
1759
 
975
1760
  if result["success"]:
976
1761
  data = result["data"]
1762
+ # CORREÇÃO: API retorna em "results", mas paginador espera em "enterprises"
977
1763
  enterprises = data.get("results", []) if isinstance(data, dict) else data
978
1764
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
1765
+ total_count = metadata.get("count", len(enterprises))
979
1766
 
980
1767
  return {
981
1768
  "success": True,
982
- "message": f"✅ Encontrados {len(enterprises)} empreendimentos",
983
- "enterprises": enterprises,
1769
+ "message": f"✅ Encontrados {len(enterprises)} empreendimentos (total: {total_count})",
1770
+ "enterprises": enterprises, # Manter consistência para paginador
1771
+ "projects": enterprises, # Alias para compatibilidade
984
1772
  "count": len(enterprises),
1773
+ "total_count": total_count,
985
1774
  "metadata": metadata,
986
- "filters": params,
1775
+ "filters_applied": {
1776
+ "limit": limit, "offset": offset, "company_id": company_id,
1777
+ "enterprise_type": enterprise_type, "receivable_register": receivable_register,
1778
+ "only_buildings_enabled": only_buildings_enabled
1779
+ },
1780
+ "request_id": result.get("request_id"),
1781
+ "latency_ms": result.get("latency_ms"),
1782
+ "cache": result.get("cache")
987
1783
  }
988
1784
 
989
1785
  return {
@@ -991,6 +1787,7 @@ async def get_sienge_projects(
991
1787
  "message": "❌ Erro ao buscar empreendimentos",
992
1788
  "error": result.get("error"),
993
1789
  "details": result.get("message"),
1790
+ "request_id": result.get("request_id")
994
1791
  }
995
1792
 
996
1793
 
@@ -1065,6 +1862,9 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0)
1065
1862
  "message": f"✅ Encontradas {len(units)} unidades (total: {total_count})",
1066
1863
  "units": units,
1067
1864
  "count": len(units),
1865
+ "total_count": total_count,
1866
+ "request_id": result.get("request_id"),
1867
+ "latency_ms": result.get("latency_ms")
1068
1868
  }
1069
1869
 
1070
1870
  return {
@@ -1072,6 +1872,8 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0)
1072
1872
  "message": "❌ Erro ao buscar unidades",
1073
1873
  "error": result.get("error"),
1074
1874
  "details": result.get("message"),
1875
+ "request_id": result.get("request_id"),
1876
+ "latency_ms": result.get("latency_ms")
1075
1877
  }
1076
1878
 
1077
1879
 
@@ -1210,35 +2012,44 @@ async def search_sienge_data(
1210
2012
 
1211
2013
 
1212
2014
  async def _search_specific_entity(entity_type: str, query: str, limit: int, filters: Dict) -> Dict:
1213
- """Função auxiliar para buscar em uma entidade específica"""
2015
+ """
2016
+ Função auxiliar para buscar em uma entidade específica
2017
+ CORRIGIDO: Usa serviços internos, nunca outras tools
2018
+ """
1214
2019
 
1215
2020
  if entity_type == "customers":
1216
- result = await get_sienge_customers(limit=limit, search=query)
2021
+ result = await _svc_get_customers(limit=limit, search=query)
1217
2022
  if result["success"]:
2023
+ data = result["data"]
2024
+ customers = data.get("results", []) if isinstance(data, dict) else data
1218
2025
  return {
1219
2026
  "success": True,
1220
- "data": result["customers"],
1221
- "count": result["count"],
2027
+ "data": customers,
2028
+ "count": len(customers),
1222
2029
  "entity_type": "customers"
1223
2030
  }
1224
2031
 
1225
2032
  elif entity_type == "creditors":
1226
- result = await get_sienge_creditors(limit=limit, search=query)
2033
+ result = await _svc_get_creditors(limit=limit, search=query)
1227
2034
  if result["success"]:
2035
+ data = result["data"]
2036
+ creditors = data.get("results", []) if isinstance(data, dict) else data
1228
2037
  return {
1229
2038
  "success": True,
1230
- "data": result["creditors"],
1231
- "count": result["count"],
2039
+ "data": creditors,
2040
+ "count": len(creditors),
1232
2041
  "entity_type": "creditors"
1233
2042
  }
1234
2043
 
1235
2044
  elif entity_type == "projects" or entity_type == "enterprises":
1236
2045
  # Para projetos, usar filtros mais específicos se disponível
1237
2046
  company_id = filters.get("company_id")
1238
- result = await get_sienge_projects(limit=limit, company_id=company_id)
2047
+ result = await _svc_get_projects(limit=limit, company_id=company_id)
1239
2048
  if result["success"]:
2049
+ data = result["data"]
2050
+ projects = data.get("results", []) if isinstance(data, dict) else data
2051
+
1240
2052
  # Filtrar por query se fornecida
1241
- projects = result["enterprises"]
1242
2053
  if query:
1243
2054
  projects = [
1244
2055
  p for p in projects
@@ -1257,23 +2068,27 @@ async def _search_specific_entity(entity_type: str, query: str, limit: int, filt
1257
2068
  # Para títulos, usar data padrão se não especificada
1258
2069
  start_date = filters.get("start_date")
1259
2070
  end_date = filters.get("end_date")
1260
- result = await get_sienge_bills(
2071
+ result = await _svc_get_bills(
1261
2072
  start_date=start_date,
1262
2073
  end_date=end_date,
1263
2074
  limit=limit
1264
2075
  )
1265
2076
  if result["success"]:
2077
+ data = result["data"]
2078
+ bills = data.get("results", []) if isinstance(data, dict) else data
1266
2079
  return {
1267
2080
  "success": True,
1268
- "data": result["bills"],
1269
- "count": result["count"],
2081
+ "data": bills,
2082
+ "count": len(bills),
1270
2083
  "entity_type": "bills"
1271
2084
  }
1272
2085
 
1273
2086
  elif entity_type == "purchase_orders":
1274
- result = await get_sienge_purchase_orders(limit=limit)
2087
+ result = await _svc_get_purchase_orders(limit=limit)
1275
2088
  if result["success"]:
1276
- orders = result["purchase_orders"]
2089
+ data = result["data"]
2090
+ orders = data.get("results", []) if isinstance(data, dict) else data
2091
+
1277
2092
  # Filtrar por query se fornecida
1278
2093
  if query:
1279
2094
  orders = [
@@ -1316,28 +2131,28 @@ async def list_sienge_entities() -> Dict:
1316
2131
  "name": "Credores/Fornecedores",
1317
2132
  "description": "Fornecedores e credores cadastrados",
1318
2133
  "search_fields": ["nome", "documento"],
1319
- "tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info"]
2134
+ "tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info", "get_sienge_suppliers"]
1320
2135
  },
1321
2136
  {
1322
2137
  "type": "projects",
1323
2138
  "name": "Empreendimentos/Obras",
1324
2139
  "description": "Projetos e obras cadastrados",
1325
2140
  "search_fields": ["código", "descrição", "nome"],
1326
- "tools": ["get_sienge_projects", "get_sienge_enterprise_by_id"]
2141
+ "tools": ["get_sienge_projects", "get_sienge_enterprises", "get_sienge_enterprise_by_id"]
1327
2142
  },
1328
2143
  {
1329
2144
  "type": "bills",
1330
2145
  "name": "Títulos a Pagar",
1331
2146
  "description": "Contas a pagar e títulos financeiros",
1332
2147
  "search_fields": ["número", "credor", "valor"],
1333
- "tools": ["get_sienge_bills"]
2148
+ "tools": ["get_sienge_bills", "get_sienge_accounts_payable"]
1334
2149
  },
1335
2150
  {
1336
2151
  "type": "purchase_orders",
1337
2152
  "name": "Pedidos de Compra",
1338
2153
  "description": "Pedidos de compra e solicitações",
1339
2154
  "search_fields": ["id", "descrição", "status"],
1340
- "tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests"]
2155
+ "tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests", "list_sienge_purchase_requests"]
1341
2156
  },
1342
2157
  {
1343
2158
  "type": "invoices",
@@ -1358,7 +2173,14 @@ async def list_sienge_entities() -> Dict:
1358
2173
  "name": "Financeiro",
1359
2174
  "description": "Contas a receber e movimentações financeiras",
1360
2175
  "search_fields": ["período", "cliente", "valor"],
1361
- "tools": ["get_sienge_accounts_receivable"]
2176
+ "tools": ["get_sienge_accounts_receivable", "search_sienge_financial_data", "search_sienge_finances"]
2177
+ },
2178
+ {
2179
+ "type": "suppliers",
2180
+ "name": "Fornecedores",
2181
+ "description": "Fornecedores e credores cadastrados",
2182
+ "search_fields": ["código", "nome", "razão social"],
2183
+ "tools": ["get_sienge_suppliers"]
1362
2184
  }
1363
2185
  ]
1364
2186
 
@@ -1401,41 +2223,70 @@ async def get_sienge_data_paginated(
1401
2223
 
1402
2224
  filters = filters or {}
1403
2225
 
1404
- # Mapear para os tools existentes com offset
2226
+ # CORRIGIDO: Mapear para serviços internos, não tools
1405
2227
  if entity_type == "customers":
1406
2228
  search = filters.get("search")
1407
2229
  customer_type_id = filters.get("customer_type_id")
1408
- result = await get_sienge_customers(
2230
+ result = await _svc_get_customers(
1409
2231
  limit=page_size,
1410
2232
  offset=offset,
1411
2233
  search=search,
1412
2234
  customer_type_id=customer_type_id
1413
2235
  )
2236
+ # CORRIGIDO: Extrair items e total corretamente
2237
+ if result["success"]:
2238
+ data = result["data"]
2239
+ items, total = _extract_items_and_total(data)
2240
+ result["customers"] = items
2241
+ result["count"] = len(items)
2242
+ result["total_count"] = total
1414
2243
 
1415
2244
  elif entity_type == "creditors":
1416
2245
  search = filters.get("search")
1417
- result = await get_sienge_creditors(
2246
+ result = await _svc_get_creditors(
1418
2247
  limit=page_size,
1419
2248
  offset=offset,
1420
2249
  search=search
1421
2250
  )
2251
+ # CORRIGIDO: Extrair items e total corretamente
2252
+ if result["success"]:
2253
+ data = result["data"]
2254
+ items, total = _extract_items_and_total(data)
2255
+ result["creditors"] = items
2256
+ result["count"] = len(items)
2257
+ result["total_count"] = total
1422
2258
 
1423
2259
  elif entity_type == "projects":
1424
- result = await get_sienge_projects(
2260
+ result = await _svc_get_projects(
1425
2261
  limit=page_size,
1426
2262
  offset=offset,
1427
2263
  company_id=filters.get("company_id"),
1428
2264
  enterprise_type=filters.get("enterprise_type")
1429
2265
  )
2266
+ # CORRIGIDO: Extrair items e total corretamente
2267
+ if result["success"]:
2268
+ data = result["data"]
2269
+ items, total = _extract_items_and_total(data)
2270
+ result["projects"] = items
2271
+ result["enterprises"] = items # Para compatibilidade
2272
+ result["count"] = len(items)
2273
+ result["total_count"] = total
1430
2274
 
1431
2275
  elif entity_type == "bills":
1432
- result = await get_sienge_bills(
2276
+ result = await _svc_get_bills(
1433
2277
  start_date=filters.get("start_date"),
1434
2278
  end_date=filters.get("end_date"),
1435
2279
  creditor_id=filters.get("creditor_id"),
1436
2280
  status=filters.get("status"),
1437
2281
  limit=page_size
1438
2282
  )
2283
+ # CORRIGIDO: Extrair items e total corretamente
2284
+ if result["success"]:
2285
+ data = result["data"]
2286
+ items, total = _extract_items_and_total(data)
2287
+ result["bills"] = items
2288
+ result["count"] = len(items)
2289
+ result["total_count"] = total
1439
2290
 
1440
2291
  else:
1441
2292
  return {
@@ -1499,20 +2350,21 @@ async def search_sienge_financial_data(
1499
2350
  # Buscar contas a receber
1500
2351
  if search_type in ["receivable", "both"]:
1501
2352
  try:
1502
- receivable_result = await get_sienge_accounts_receivable(
2353
+ # CORRIGIDO: Usar serviço interno
2354
+ receivable_result = await _svc_get_accounts_receivable(
1503
2355
  start_date=period_start,
1504
2356
  end_date=period_end,
1505
2357
  selection_type="D" # Por vencimento
1506
2358
  )
1507
2359
 
1508
2360
  if receivable_result["success"]:
1509
- receivable_data = receivable_result["income_data"]
2361
+ receivable_data = receivable_result.get("data", [])
1510
2362
 
1511
- # Aplicar filtros de valor se especificados
2363
+ # CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
1512
2364
  if amount_min is not None or amount_max is not None:
1513
2365
  filtered_data = []
1514
2366
  for item in receivable_data:
1515
- amount = float(item.get("amount", 0) or 0)
2367
+ amount = _parse_numeric_value(item.get("amount", 0))
1516
2368
  if amount_min is not None and amount < amount_min:
1517
2369
  continue
1518
2370
  if amount_max is not None and amount > amount_max:
@@ -1545,20 +2397,22 @@ async def search_sienge_financial_data(
1545
2397
  # Buscar contas a pagar
1546
2398
  if search_type in ["payable", "both"]:
1547
2399
  try:
1548
- payable_result = await get_sienge_bills(
2400
+ # CORRIGIDO: Usar serviço interno
2401
+ payable_result = await _svc_get_bills(
1549
2402
  start_date=period_start,
1550
2403
  end_date=period_end,
1551
2404
  limit=100
1552
2405
  )
1553
2406
 
1554
2407
  if payable_result["success"]:
1555
- payable_data = payable_result["bills"]
2408
+ data = payable_result["data"]
2409
+ payable_data = data.get("results", []) if isinstance(data, dict) else data
1556
2410
 
1557
- # Aplicar filtros de valor se especificados
2411
+ # CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
1558
2412
  if amount_min is not None or amount_max is not None:
1559
2413
  filtered_data = []
1560
2414
  for item in payable_data:
1561
- amount = float(item.get("amount", 0) or 0)
2415
+ amount = _parse_numeric_value(item.get("amount", 0))
1562
2416
  if amount_min is not None and amount < amount_min:
1563
2417
  continue
1564
2418
  if amount_max is not None and amount > amount_max:
@@ -1650,23 +2504,26 @@ async def get_sienge_dashboard_summary() -> Dict:
1650
2504
 
1651
2505
  # 2. Contar clientes (amostra)
1652
2506
  try:
1653
- customers_result = await get_sienge_customers(limit=1)
1654
- if customers_result["success"]:
1655
- dashboard_data["customers_available"] = True
1656
- else:
1657
- dashboard_data["customers_available"] = False
2507
+ # CORRIGIDO: Usar serviço interno
2508
+ customers_result = await _svc_get_customers(limit=1)
2509
+ dashboard_data["customers"] = {"available": customers_result["success"]}
1658
2510
  except Exception as e:
1659
2511
  errors.append(f"Clientes: {str(e)}")
1660
- dashboard_data["customers_available"] = False
2512
+ dashboard_data["customers"] = {"available": False}
1661
2513
 
1662
2514
  # 3. Contar projetos (amostra)
1663
2515
  try:
1664
- projects_result = await get_sienge_projects(limit=5)
2516
+ # CORRIGIDO: Usar serviço interno
2517
+ projects_result = await _svc_get_projects(limit=5)
1665
2518
  if projects_result["success"]:
2519
+ data = projects_result["data"]
2520
+ enterprises = data.get("results", []) if isinstance(data, dict) else data
2521
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
2522
+
1666
2523
  dashboard_data["projects"] = {
1667
2524
  "available": True,
1668
- "sample_count": len(projects_result["enterprises"]),
1669
- "total_count": projects_result.get("metadata", {}).get("count", "N/A")
2525
+ "sample_count": len(enterprises),
2526
+ "total_count": metadata.get("count", "N/A")
1670
2527
  }
1671
2528
  else:
1672
2529
  dashboard_data["projects"] = {"available": False}
@@ -1676,16 +2533,21 @@ async def get_sienge_dashboard_summary() -> Dict:
1676
2533
 
1677
2534
  # 4. Títulos a pagar do mês atual
1678
2535
  try:
1679
- bills_result = await get_sienge_bills(
2536
+ # CORRIGIDO: Usar serviço interno
2537
+ bills_result = await _svc_get_bills(
1680
2538
  start_date=current_month_start,
1681
2539
  end_date=current_month_end,
1682
2540
  limit=10
1683
2541
  )
1684
2542
  if bills_result["success"]:
2543
+ data = bills_result["data"]
2544
+ bills = data.get("results", []) if isinstance(data, dict) else data
2545
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
2546
+
1685
2547
  dashboard_data["monthly_bills"] = {
1686
2548
  "available": True,
1687
- "count": len(bills_result["bills"]),
1688
- "total_count": bills_result.get("total_count", len(bills_result["bills"]))
2549
+ "count": len(bills),
2550
+ "total_count": metadata.get("count", len(bills))
1689
2551
  }
1690
2552
  else:
1691
2553
  dashboard_data["monthly_bills"] = {"available": False}
@@ -1695,11 +2557,15 @@ async def get_sienge_dashboard_summary() -> Dict:
1695
2557
 
1696
2558
  # 5. Tipos de clientes
1697
2559
  try:
1698
- customer_types_result = await get_sienge_customer_types()
2560
+ # CORRIGIDO: Usar serviço interno
2561
+ customer_types_result = await _svc_get_customer_types()
1699
2562
  if customer_types_result["success"]:
2563
+ data = customer_types_result["data"]
2564
+ customer_types = data.get("results", []) if isinstance(data, dict) else data
2565
+
1700
2566
  dashboard_data["customer_types"] = {
1701
2567
  "available": True,
1702
- "count": len(customer_types_result["customer_types"])
2568
+ "count": len(customer_types)
1703
2569
  }
1704
2570
  else:
1705
2571
  dashboard_data["customer_types"] = {"available": False}
@@ -1737,6 +2603,21 @@ def add(a: int, b: int) -> int:
1737
2603
  return a + b
1738
2604
 
1739
2605
 
2606
+ def _mask(s: str) -> str:
2607
+ """Mascara dados sensíveis mantendo apenas o início e fim"""
2608
+ if not s:
2609
+ return None
2610
+ if len(s) == 1:
2611
+ return s + "*"
2612
+ if len(s) == 2:
2613
+ return s
2614
+ if len(s) <= 4:
2615
+ return s[:2] + "*" * (len(s) - 2)
2616
+ # Para strings > 4: usar no máximo 4 asteriscos no meio
2617
+ middle_asterisks = min(4, len(s) - 4)
2618
+ return s[:2] + "*" * middle_asterisks + s[-2:]
2619
+
2620
+
1740
2621
  def _get_auth_info_internal() -> Dict:
1741
2622
  """Função interna para verificar configuração de autenticação"""
1742
2623
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
@@ -1747,7 +2628,7 @@ def _get_auth_info_internal() -> Dict:
1747
2628
  "configured": True,
1748
2629
  "base_url": SIENGE_BASE_URL,
1749
2630
  "subdomain": SIENGE_SUBDOMAIN,
1750
- "username": SIENGE_USERNAME,
2631
+ "username": _mask(SIENGE_USERNAME) if SIENGE_USERNAME else None,
1751
2632
  }
1752
2633
  else:
1753
2634
  return {