Hoje vamos pegar um das demandas mais recorrentes que recebemos de nossos clientes e vamos ensinar como resolvemos isso usando a nossa ferramenta TunnelHub: como criar uma integração sua base de funcionários do TOTVS RM para o SOC, usando a nossa plataforma de integração. O mais legal é que você mesmo pode fazer isso criando uma conta grátis e usar por quanto tempo for necessário.

Sem mais delongas pois esse será um post longo, vamos ao cenário hipotético do nosso estudo: o nosso cliente hipotético ACME usa a ferramenta TOTVS RM para sua operação de RH e acaba de contratar o SOC para realizar a gestão de saúde e segurança. Porém isso trouxe um novo problema para sua operação, já que o SOC precisa ter o cadastro completo de funcionários igual já é feito no TOTVS – e fazer isso manualmente não é viável. O cliente tem diversas admissões, movimentações e desligamento ao longo do mês e isso dobraria a carga de trabalho da equipe atual, que não possui mais tempo disponível para essa atividade. As opções são:

  • Aumentar a equipe de funcionários para manter o novo sistema atualizado
  • Criar uma integração que possa realizar essa tarefa automaticamente

Adivinhe qual opção eles irão optar? Criar uma integração com o TunnelHub!

Iniciando nosso projeto

Então vamos iniciar o nosso projeto. E, como qualquer projeto, esse terá que ser divido em fases. Iremos optar por algo bem simplificado:

  • Fase de preparação
  • Fase de execução
  • Fase de homologação
  • Go-live

Fase de preparação

Nessa fase iremos iniciar a parte que não é puramente técnica do projeto, que parece simples mas dá um grande trabalho: mapeamento dos campos necessários entre os dois sistemas.

Do ponto de vista técnico, iremos definir qual será a arquitetura para troca das informações entre os sistemas e liberação de acessos. Nesse ponto, a definição foi simples: iremos utilizar os serviços SOAP padrão do TOTVS RM, que estão publicados em um servidor publico na internet com autenticação por usuário e senha.

Agora o sobre o mapeamento foi notado que existem diferenças entre os códigos de alguns campos entre os sistemas. Nosso analista fez a análise dos valores e fez um de/para bem detalhado. Para deixar bem documentado, nós criamos uma planilha no Google Planilhas.

Fase de execução

Aqui é onde a mágica acontece! Como eu sei que a maioria das pessoas vai querer somente a solução final, disponibilizamos um repositório no GitHub com todo o código já finalizado. Mas vamos detalhar todos os passos!

Primeiro, é necessário ter uma conta no TunnelHub! Se você quiser de fato fazer todo o tutorial, pode entrar lá e criar sua conta grátis agora mesmo.

Com a sua conta criada, é hora de instalar nosso cliente CLI. O único pré-requisito necessário é o NodeJS v12+ e o NPM. Considerando que você já tenha o Node configurado, execute:

npm install -g @tunnelhub/cli

Após instalado, o próximo passo é fazer o login da sua conta com o comando th login. Antes, tenha em mãos as seguintes informações:
  • Tenant ID
  • Seu usuário
  • Sua senha.

Você pode buscar a informação do seu Tenant ID no menu superior direito:

Com os dados em mãos, hora de realizar o login:

Você pode verificar se seu login está válido sempre que precisar com o comando th login-check,

Criando um pacote

Antes de criar nossa integração, é necessário criar um pacote no TunnelHub. Pacotes são unidades lógicas para agrupamento de itens na plataforma TunnelHub. Ele pode ser criado pelo painel administrativo ou via CLI. Vamos fazer via CLI:

th create-package --env DEV

Tela da criação de pacote

Criando uma automação

Agora é hora de criar nossa automação e começarmos a codificar. Para isso usaremos o comando th create-automation --env DEV

Tela criando automação

Se você chegou até aqui, foi criado uma nova pasta com o nome da integração, com todos os items necessários e exemplos para você começar a sua codificação. Ao invés de detalhar cada pedaço, vamos sugerir que você verifique a nossa documentação. Lá tem todos os conceitos e fundamentos importantes para entender como funciona cada componente criado.

Agora, vamos detalhar a nossa implementação em alguns pontos principais, pegando de exemplo nosso repositório do GitHub.

Extração dos dados do TOTVS

Toda a extração é tratada no método loadSourceSystemData da classe src/integration.ts:

async loadSourceSystemData(payload?: any): Promise<FuncionarioSoc[]> {
  const dataUltimaModificacao = this.lerParametro(this.parameters, 'dataUltimaModificacao')?.value ?? DateTime.now().toISO({
    suppressMilliseconds: true,
    includeOffset: false,
  });

  const unidades = await this.totvsApi.buscaUnidades();

  const output = [] as FuncionarioSoc[];
  for (let i = 0; i < unidades.length; i++) {
    const unidade = unidades[i];

    const [setores, cargos, funcionariosAtivos] = await Promise.all([
      this.totvsApi.buscaSetores(unidade.CODCOLIGADA),
      this.totvsApi.buscaCargos(unidade.CODCOLIGADA),
      this.totvsApi.buscaFuncionariosAtivos(unidade.CODCOLIGADA, dataUltimaModificacao),
    ]);

    const detalhesFuncionarios = await Promise.map(funcionariosAtivos, (funcionario: FopFuncDataReadViewResponse.PFunc) => {
      const chapa = funcionario.CHAPA.toString().padStart(6, '0');
      return this.totvsApi.buscaDetalheFuncionario(funcionario.CODCOLIGADA, chapa);
    }, { concurrency: 10 });


    detalhesFuncionarios.forEach(funcionario => {
      const employee = {} as FuncionarioSoc;

      const setor = setores.find((value) => value.CODIGO === funcionario.FopFunc.PFunc.CODSECAO);
      const cargo = cargos.find((value) => value.CODIGO === funcionario.FopFunc.PFunc.CODFUNCAO);

      employee.codigoSocEmpresa = this.convertCodigoempresaSoc(unidade);
      employee.codigoRhUnidade = unidade?.CODCOLIGADA?.toString();
      employee.nomeUnidade = unidade?.NOMEFANTASIA?.toString();
      employee.codigoRhSetor = setor?.CODIGO?.toString();
      employee.nomeSetor = setor?.DESCRICAO?.toString();
      employee.codigoRhCargo = cargo?.CODIGO?.toString();
      employee.nomeCargo = cargo?.NOME?.toString();
      employee.matricula = funcionario.FopFunc.PFunc.MATRICULAESOCIAL;
      employee.nomeFuncionario = funcionario.FopFunc.PFunc.NOME;
      employee.dtNascimento = DateTime.fromFormat(funcionario.FopFunc.PFunc.DTNASCIMENTO, 'yyyy-LL-ddT00:00:00').toFormat('yyyy-LL-dd');
      employee.sexo = funcionario.FopFunc.PFunc.SEXO === 'M' ? 'MASCULINO' : 'FEMININO';
      employee.situacao = SocConversion.convertSituacao(funcionario.FopFunc.PFunc.CODSITUACAO);
      employee.dtAdmissao = funcionario.FopFunc.PFunc.DATAADMISSAO ? DateTime.fromFormat(funcionario.FopFunc.PFunc.DATAADMISSAO, 'yyyy-LL-ddT00:00:00').toFormat('yyyy-LL-dd') : undefined;
      employee.estadoCivil = SocConversion.convertEstadoCivil(funcionario.FopFunc.PFunc.ESTADOCIVIL);
      employee.tipoContratacao = SocConversion.convertTipoContratacao(funcionario.FopFunc.PFunc.CODTIPO);
      employee.cpf = funcionario.FopFunc?.PFunc?.CPF?.toString();
      employee.cnpjUnidade = unidade.CGC;
      employee.razaoSocialUnidade = unidade.NOME;
      employee.categoriaEsocial = funcionario?.FopFunc?.PFunc?.CODCATEGORIAESOCIAL?.toString();
      employee.pis = SocConversion.somenteNumeros(funcionario?.FopFunc?.PFunc?.PISPASEP?.toString());
      employee.rg = funcionario.FopFunc.PFunc?.CARTIDENTIDADE?.toString();
      employee.rgOrgaoEmissor = funcionario.FopFunc.PFunc.ORGEMISSORIDENT;
      employee.ufRG = funcionario.FopFunc.PFunc.UFCARTIDENT as EstadosBrasileirosSoc;
      employee.nrCtps = funcionario.FopFunc.PFunc?.CARTEIRATRAB?.toString();
      employee.enderecoFuncionario = funcionario.FopFunc.PFunc.RUA;
      employee.bairroFuncionario = funcionario.FopFunc.PFunc.BAIRRO;
      employee.cepFuncionario = funcionario?.FopFunc?.PFunc?.CEP?.toString();
      employee.cidadeFuncionario = funcionario.FopFunc.PFunc.CIDADE;
      employee.estadoFuncionario = funcionario.FopFunc.PFunc.ESTADO as EstadosBrasileirosSoc;
      employee.telefoneCelular = SocConversion.somenteNumeros(funcionario.FopFunc.PFunc?.TELEFONE1?.toString());
      employee.cor = SocConversion.convertCor(funcionario.FopFunc.PFunc.CORRACA);
      employee.cbo = cargo.CBO.toString();
      employee.enderecoUnidade = unidade.RUA;
      employee.bairroUnidade = unidade.BAIRRO;
      employee.cidadeUnidade = unidade.CIDADE;
      employee.estadoUnidade = unidade.ESTADO as EstadosBrasileirosSoc;
      employee.cepUnidade = unidade?.CEP?.toString();
      employee.nomedaMae = funcionario.FopFunc.PFunc.NOMEMAE;
      employee.serieCtps = funcionario.FopFunc.PFunc?.SERIECARTTRAB?.toString();

      output.push(employee);
    });
  }

  return output;
}

O código é complexo e precisaria ser visto em detalhes mas vamos destacar a linha geral de raciocínio para um entendimento macro:

  • Primeiramente serão buscadas as coligadas no TOTVS, que representam as empresas. Nesse método tem uma opção de filtro para selecionar apenas determinadas empresas. Isso é um requisito bem comum, pois muitas vezes não são todas as empresas que devem ir para o SOC. Deixamos fixado apenas a coligada 1.
  • Para cada coligada serão buscados os cargos, setores e funcionários ativos com data de modificação maior ou igual a data da última execução – ou data atual no caso da primeira execução.
  • Com os funcionários ativos, será feito uma nova chamada buscando os detalhes de cada funcionário encontrado. Isso se faz necessário, visto que o TOTVS não retorna todas as informações necessárias no método de busca dos ativos.
  • Com esses dados carregados em memória, começamos a preencher o objeto de funcionário conforme o mapeamento, considerando os DE/PARAs da planilha.

Após esse método ser finalizado, será criada uma fila com os funcionários carregados e formatados conforme mapeamento, a serem enviados um a um para o método sendData. Vamos dar uma olhada geral nesse método também:

async sendData(item: FuncionarioSoc): Promise<IntegrationMessageReturn> {
   const system = this.systems.find(value => value.internalName === 'SOC_FUNCIONARIOS');
   if (system.type !== 'SOAP') {
     throw new Error(`O sistema SOC_FUNCIONARIOS precisa ser do tipo SOAP`);
   }

   const wsSocEmployee = this.mapearValoresParaEstruturaWebservice(item);

   const employeeHireDate = DateTime.fromISO(item.dtAdmissao);
   const diffInDays = DateTime.now().diff(employeeHireDate, 'days');
   if (diffInDays.days >= -30 && diffInDays.days <= 30) {
     wsSocEmployee.Funcionario.funcionarioWsVo.chaveProcuraFuncionario = 'CPF_DATA_ADMISSAO_PERIODO';
   }

   const soapClient = await this.getSoapClient(system);
   soapClient.setSecurity(new soap.WSSecurity(this.socParameters.mainCompany, this.socParameters.password, {
     hasNonce: true,
     hasTimeStamp: true,
     hasTokenCreated: true,
     passwordType: 'PasswordDigest',
   }));

   const [socResponse] = await soapClient.importacaoFuncionarioAsync(wsSocEmployee);
   if (socResponse) {
     if (socResponse.FuncionarioRetorno.encontrouErro) {
       throw new Error(socResponse.FuncionarioRetorno.descricaoErro);
     }

     return {
       message: SocConversion.formataMensagemDeRetorno(socResponse.FuncionarioRetorno),
       data: socResponse.FuncionarioRetorno,
     };
   }
 }

De forma geral, esse método faz:

  • Converte os dados extraídos para a estrutura do Webservice do SOC
  • Adiciona uma regra para considerar casos de admissões recentes, para a chave de buscar ser CPF_DATA_ADMISSAO_PERIODO. Isso é necessário para evitar casos de duplicação de cadastro de pendentes.
  • Define a autenticação com WS Security no header, considerando as opções conforme manual do SOC
  • Envia os dados e avaliar a resposta

Existe diversos outros métodos auxiliares mas deixamos a tarefa de vocês explorarem o código e entender por si mesmos. Imagino que até possam surgir melhorias e, se for o caso, aceitamos pull requests no repositório!

Além do código, existe a parte dos testes unitários. Deixamos já diversos testes, que estão cobrindo quase todo o código. Eles devem ser suficientes para vocês avaliarem o comportamento do código. Eles estão na pasta __tests__.

Implantando nosso código no TunnelHub

Quando você estiver bem e confiante sobre sua integração, vamos implantá-la e executá-la na nuvem.
Como seu código será executado no ambiente AWS Lambda e estamos usando o Typescript, é necessário criar um pacote com todo o código e dependências transpilados para Javascript. Em nosso template, já está configurado usando Webpack. Basta executar:

npm run build && th deploy-automation --env DEV --message "Meu primeiro deploy"

Tela implantação

Agora que o deploy está feito, falta só criamos os sistemas e atribuirmos a nossa integração. Vamos lá, primeiramente vamos criar o sistema TOTVS:

Sistema TOTVS - Tela principal

Sistema TOTVS - Tela parâmetros

Agora vamos criar o sistema SOC:

Sistema SOC - Tela principal

Sistema SOC - Tela parâmetros

Sobre essa tela, vale uma explicação dos valores:

  • Usuário: Deve ser o Código Usuário de um usuário com as permissões necessárias para realizar as atualizações. Recomandamos usar um usuário do tipo webservice.
  • Senha: Deve ser o campo Chave de Acesso/Password da tela 337, configurações de integração. Nesse exemplo, estamos autenticando com a chave da empresa principal
  • Main company: Código da empresa principal, da tela 337, configurações de integração.
  • Responsible code: Código do responsável, da tela 337, configurações de integração.
  • Testing environment: indicador se é empresa de teste. Normalmente deve ser false

Além disso, deve-se solicitar ao responsável pelo SOC que ative o webservice “Funcionários (Modelo 2)” na mesma tela das credenciais.

Com os sistemas criados, devemos atribuí-los a nossa automação. Para isso, entraremos na automação em modo de edição e iremos na seção “Sistemas”:

Automação - Atribuindo sistemas

Com os sistemas atribuídos, agora precisamos definir um gatinho para executar nossa integração. Normalmente será um agendamento, em um detemrinado dia/horário. Para nosso caso, vamos definir como webhook apenas para fazer alguns testes:

Criando gatilho

Pronto, agora é só salvar e testar nossa integração! Isso pode ser feito no menu “Automações -> Definições”, no botão “Operações” da linha da nossa automacão:

Executando integração

Você pode acompanhar a execução no menu “Automações -> Monitoramento”:

Monitorando integração executando

Para ver mais detalhes, você pode clicar no botão “Ver detalhes” da coluna “Ações”:

Detalhe monitoramento

No meu caso, deu erro porque o endereço utilizado não existe, por conta de ser apenas um exemplo. Mas no caso de um endereço real, teríamos os logs dos dados extraídos e as mensagens de erro de cada item.

Fase de Homologação

A próxima fase seria realizar cargas usando dados, preferencialmente em um ambiente de testes, e pedir para o nosso cliente verificar se os dados estão chegando corretamente, considerando todo o trabalho de mapeamento feito na fase inicial.

Uma vez que o cliente esteja de acordo, podemos avançar para a próxima fase, pois sabemos que a integração está se comportando conforme esperado!

Go-live

Essa é a fase mais rápida do projeto: basicamente transportamos a integração de ambiente, atualizamos os dados dos sistemas para as credenciais do sistema produtivo e definimos qual o agendamento que irá ser executado durante a opção automática.

Conclusão

Enfim finalizamos nosso artigo! O que vocês acharam? Vocês podem usar a seção de comentários ou enviar issues ou PR no repositório do projeto.
E para recompensar que chegou até aqui, deixaremos o cupom SOCTOTVS20 que dá 20% de desconto no plano Premium do TunnelHub.

Até a próxima!