@tiflux/mcp
Version:
TiFlux MCP Server - Model Context Protocol integration for Claude Code and other AI clients
484 lines (406 loc) • 14 kB
JavaScript
/**
* ClientMapper - Transformação de dados entre API e domínio para clientes
*
* Responsabilidades:
* - Converter dados da API TiFlux para formato interno
* - Normalizar campos e estruturas de cliente
* - Padronizar dados de contato e documentos
* - Garantir compatibilidade entre versões
*/
class ClientMapper {
constructor(container) {
this.container = container;
this.logger = container.resolve('logger');
this.config = container.resolve('config');
}
/**
* Mapeia dados de cliente da API para formato interno
*/
mapFromAPI(apiData) {
if (!apiData) {
return null;
}
try {
// A API pode retornar o cliente dentro de { client: {...} } ou direto
const clientData = apiData.client || apiData;
const mapped = {
// Identificadores
id: this._extractId(clientData.id),
uuid: this._extractString(clientData.uuid),
// Dados básicos
name: this._extractString(clientData.name) || 'Cliente N/A',
display_name: this._extractString(clientData.display_name || clientData.name),
// Tipo de cliente (pessoa física/jurídica)
type: this._mapClientType(clientData.type || clientData.client_type),
// Status
active: this._extractBoolean(clientData.active, true), // Default: ativo
// Documentos
document: this._extractString(clientData.document || clientData.cpf_cnpj),
document_type: this._mapDocumentType(clientData.document_type),
// Dados de contato
email: this._extractEmail(clientData.email),
phone: this._extractPhone(clientData.phone || clientData.telephone),
mobile: this._extractPhone(clientData.mobile || clientData.mobile_phone),
// Endereço
address: this._mapAddress(clientData.address || clientData),
// Dados empresariais (se aplicável)
company: this._mapCompanyData(clientData),
// Responsável/contato principal
contact_person: this._mapContactPerson(clientData.contact || clientData.responsible),
// Configurações
settings: this._mapClientSettings(clientData.settings || clientData.preferences),
// Datas
created_at: this._mapDateTime(clientData.created_at),
updated_at: this._mapDateTime(clientData.updated_at),
last_activity_at: this._mapDateTime(clientData.last_activity_at || clientData.last_login),
// Metadados
source: this._extractString(clientData.source),
notes: this._extractString(clientData.notes || clientData.observations),
// URLs e referências
url: this._buildClientURL(clientData.id),
portal_url: this._buildPortalURL(clientData.id || clientData.uuid)
};
// Remove campos null/undefined
return this._cleanObject(mapped);
} catch (error) {
this.logger.error('Failed to map client from API', {
error: error.message,
clientId: apiData.id || apiData.client?.id
});
throw new Error(`Falha ao mapear dados do cliente: ${error.message}`);
}
}
/**
* Mapeia resultados de busca da API
*/
mapSearchResultsFromAPI(apiResponse) {
try {
let clients = [];
// A API pode retornar diferentes formatos de busca
if (Array.isArray(apiResponse)) {
clients = apiResponse;
} else if (apiResponse.clients && Array.isArray(apiResponse.clients)) {
clients = apiResponse.clients;
} else if (apiResponse.data && Array.isArray(apiResponse.data)) {
clients = apiResponse.data;
} else if (apiResponse.results && Array.isArray(apiResponse.results)) {
clients = apiResponse.results;
}
// Mapeia cada cliente
const mappedClients = clients.map(client => {
try {
return this.mapFromAPI(client);
} catch (error) {
this.logger.warn('Failed to map individual client in search results', {
clientId: client.id,
error: error.message
});
// Retorna dados básicos em caso de erro
return {
id: client.id,
name: client.name || 'Erro ao carregar',
active: true,
email: client.email || null,
phone: client.phone || null
};
}
}).filter(client => client !== null);
this.logger.debug('Client search results mapped', {
originalCount: clients.length,
mappedCount: mappedClients.length
});
return mappedClients;
} catch (error) {
this.logger.error('Failed to map client search results from API', {
error: error.message
});
throw new Error(`Falha ao mapear resultados de busca: ${error.message}`);
}
}
/**
* Mapeia lista paginada de clientes
*/
mapListFromAPI(apiResponse) {
try {
const searchResults = this.mapSearchResultsFromAPI(apiResponse);
// Extrai informações de paginação se disponíveis
let pagination = null;
if (apiResponse.pagination) {
pagination = this._mapPagination(apiResponse.pagination);
} else if (apiResponse.meta) {
pagination = this._mapPagination(apiResponse.meta);
}
return {
clients: searchResults,
pagination,
total_count: searchResults.length
};
} catch (error) {
this.logger.error('Failed to map client list from API', {
error: error.message
});
throw new Error(`Falha ao mapear lista de clientes: ${error.message}`);
}
}
/**
* Mapeia tipo de cliente
*/
_mapClientType(type) {
if (!type) return null;
const typeStr = type.toString().toLowerCase();
const typeMap = {
'pf': 'pessoa_fisica',
'pj': 'pessoa_juridica',
'person': 'pessoa_fisica',
'company': 'pessoa_juridica',
'individual': 'pessoa_fisica',
'business': 'pessoa_juridica'
};
return typeMap[typeStr] || typeStr;
}
/**
* Mapeia tipo de documento
*/
_mapDocumentType(docType) {
if (!docType) return null;
const typeStr = docType.toString().toLowerCase();
const docTypeMap = {
'cpf': 'cpf',
'cnpj': 'cnpj',
'rg': 'rg',
'passport': 'passaporte',
'other': 'outro'
};
return docTypeMap[typeStr] || typeStr;
}
/**
* Mapeia dados de endereço
*/
_mapAddress(addressData) {
if (!addressData) return null;
// Verifica se há dados de endereço
const hasAddressFields = ['street', 'address', 'city', 'state', 'zipcode', 'country']
.some(field => addressData[field]);
if (!hasAddressFields) return null;
return {
street: this._extractString(addressData.street || addressData.address),
number: this._extractString(addressData.number || addressData.address_number),
complement: this._extractString(addressData.complement),
neighborhood: this._extractString(addressData.neighborhood || addressData.district),
city: this._extractString(addressData.city),
state: this._extractString(addressData.state || addressData.uf),
zipcode: this._extractString(addressData.zipcode || addressData.cep),
country: this._extractString(addressData.country || 'Brasil'),
full_address: this._buildFullAddress(addressData)
};
}
/**
* Mapeia dados empresariais
*/
_mapCompanyData(clientData) {
// Só mapeia se for pessoa jurídica ou tiver dados empresariais
const isCompany = this._mapClientType(clientData.type) === 'pessoa_juridica' ||
clientData.cnpj || clientData.company_name;
if (!isCompany) return null;
return {
legal_name: this._extractString(clientData.legal_name || clientData.company_name),
trade_name: this._extractString(clientData.trade_name || clientData.fantasy_name),
cnpj: this._extractString(clientData.cnpj),
state_registration: this._extractString(clientData.state_registration || clientData.ie),
municipal_registration: this._extractString(clientData.municipal_registration || clientData.im),
activity: this._extractString(clientData.activity || clientData.business_activity)
};
}
/**
* Mapeia pessoa de contato
*/
_mapContactPerson(contactData) {
if (!contactData) return null;
return {
name: this._extractString(contactData.name),
email: this._extractEmail(contactData.email),
phone: this._extractPhone(contactData.phone),
position: this._extractString(contactData.position || contactData.role)
};
}
/**
* Mapeia configurações do cliente
*/
_mapClientSettings(settingsData) {
if (!settingsData) return null;
return {
notifications_enabled: this._extractBoolean(settingsData.notifications_enabled, true),
email_notifications: this._extractBoolean(settingsData.email_notifications, true),
sms_notifications: this._extractBoolean(settingsData.sms_notifications, false),
portal_access: this._extractBoolean(settingsData.portal_access, true),
language: this._extractString(settingsData.language || 'pt-BR'),
timezone: this._extractString(settingsData.timezone || 'America/Sao_Paulo')
};
}
/**
* Mapeia paginação
*/
_mapPagination(paginationData) {
if (!paginationData) return null;
return {
current_page: this._extractNumber(paginationData.current_page || paginationData.page) || 1,
total_pages: this._extractNumber(paginationData.total_pages || paginationData.pages),
per_page: this._extractNumber(paginationData.per_page || paginationData.limit) || 20,
total_count: this._extractNumber(paginationData.total_count || paginationData.total),
has_more: !!paginationData.has_more || !!paginationData.next_page
};
}
/**
* Mapeia data/hora
*/
_mapDateTime(dateTime) {
if (!dateTime) return null;
try {
const date = new Date(dateTime);
if (isNaN(date.getTime())) return null;
return date.toISOString();
} catch {
return null;
}
}
/**
* Constrói endereço completo
*/
_buildFullAddress(addressData) {
const parts = [];
if (addressData.street) {
let streetPart = addressData.street;
if (addressData.number) streetPart += ', ' + addressData.number;
if (addressData.complement) streetPart += ', ' + addressData.complement;
parts.push(streetPart);
}
if (addressData.neighborhood) {
parts.push(addressData.neighborhood);
}
if (addressData.city) {
let cityPart = addressData.city;
if (addressData.state) cityPart += ' - ' + addressData.state;
parts.push(cityPart);
}
if (addressData.zipcode) {
parts.push('CEP: ' + addressData.zipcode);
}
return parts.length > 0 ? parts.join(', ') : null;
}
/**
* Constrói URL do cliente no sistema
*/
_buildClientURL(clientId) {
if (!clientId) return null;
const baseUrl = this.config.get('ui.base_url', 'https://app.tiflux.com');
return `${baseUrl}/clients/${clientId}`;
}
/**
* Constrói URL do portal do cliente
*/
_buildPortalURL(clientId) {
if (!clientId) return null;
const portalUrl = this.config.get('portal.base_url', 'https://portal.tiflux.com');
return `${portalUrl}/client/${clientId}`;
}
/**
* Extrai ID numérico
*/
_extractId(value) {
if (value === null || value === undefined) return null;
const id = parseInt(value);
return isNaN(id) ? null : id;
}
/**
* Extrai string
*/
_extractString(value) {
if (value === null || value === undefined) return null;
const str = String(value).trim();
return str === '' ? null : str;
}
/**
* Extrai número
*/
_extractNumber(value) {
if (value === null || value === undefined) return null;
const num = parseFloat(value);
return isNaN(num) ? null : num;
}
/**
* Extrai boolean com valor padrão
*/
_extractBoolean(value, defaultValue = false) {
if (value === null || value === undefined) return defaultValue;
if (typeof value === 'boolean') return value;
const str = String(value).toLowerCase();
return ['true', '1', 'yes', 'on'].includes(str);
}
/**
* Valida e normaliza email
*/
_extractEmail(email) {
if (!email) return null;
const emailStr = String(email).trim().toLowerCase();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(emailStr) ? emailStr : null;
}
/**
* Normaliza telefone
*/
_extractPhone(phone) {
if (!phone) return null;
let phoneStr = String(phone).trim();
// Remove caracteres especiais mas mantém + para código do país
phoneStr = phoneStr.replace(/[^\d\+]/g, '');
// Valida tamanho mínimo
if (phoneStr.length < 8) return null;
return phoneStr;
}
/**
* Remove campos null/undefined de um objeto
*/
_cleanObject(obj) {
const cleaned = {};
Object.keys(obj).forEach(key => {
const value = obj[key];
if (value !== null && value !== undefined) {
if (typeof value === 'object' && !Array.isArray(value)) {
const cleanedChild = this._cleanObject(value);
if (Object.keys(cleanedChild).length > 0) {
cleaned[key] = cleanedChild;
}
} else {
cleaned[key] = value;
}
}
});
return cleaned;
}
/**
* Estatísticas do mapper
*/
getStats() {
return {
supported_formats: {
api_to_internal: true,
search_results: true,
paginated_lists: true
},
transformations: {
document_normalization: true,
address_parsing: true,
contact_validation: true,
type_mapping: true,
date_iso_conversion: true
},
features: {
full_address_building: true,
url_generation: true,
company_data_mapping: true,
contact_person_mapping: true
}
};
}
}
module.exports = ClientMapper;