Integracao com Salesforce/HubSpot — CRM externo

Padrao tecnico de integracao bidirecional com CRM externo (Salesforce, HubSpot, RD Station). Tokens OAuth, sincronizacao, conflitos.

SIGACRM perdeu mercado pra CRMs SaaS especializados (Salesforce, HubSpot, RD Station). Padrao moderno: Protheus eh o "back-office" (cadastros, faturamento), CRM eh o "front" (vendas, marketing). Eles precisam sincronizar.

Cenarios tipicos

  1. CRM cria oportunidade → Protheus cria pedido
  2. Protheus altera cliente (SA1) → CRM atualiza conta
  3. CRM cadastra lead novo → Protheus cria/atualiza SA1
  4. Protheus fatura (gera NF) → CRM marca oportunidade como ganha

Arquitetura

┌──────────────────┐    ┌─────────────────┐    ┌──────────────────┐
│ Salesforce/HS    │ ←→ │ Middleware/     │ ←→ │ Protheus REST    │
│ (CRM)            │    │ Webhook handler │    │ API + PE         │
└──────────────────┘    └─────────────────┘    └──────────────────┘

Codigo: REST endpoint pra receber webhook do CRM

#include "tlpp-core.th"

namespace custom.integ.crm

@Post("/crm/webhook/lead")
function NovoLeadCrm()
    Local oBody := oRequest:getJsonBody()
    Local cNome := oBody:getJsonObject("name")
    Local cEmail := oBody:getJsonObject("email")
    Local cCnpj := oBody:getJsonObject("document")

    // Tenta achar cliente existente
    SA1->(DBSetOrder(3))  // por CNPJ
    If SA1->(DBSeek(xFilial("SA1") + cCnpj))
        // Atualiza
        RecLock("SA1", .F.)
        SA1->A1_EMAIL := cEmail
        SA1->(MsUnlock())
        oResponse:setStatus(200)
        oResponse:setBody('{"status":"updated","cod":"' + SA1->A1_COD + '"}')
    Else
        // Cria via MsExecAuto
        local aRet := U_IncCliViaApi(cNome, cEmail, cCnpj)
        oResponse:setStatus(201)
        oResponse:setBody('{"status":"created","cod":"' + aRet[1] + '"}')
    EndIf

    Return
endfunction

Codigo: PE M030ALT envia pro CRM (saida)

User Function M030ALT()
    // Envia atualizacao pro CRM em background
    StartJob("U_EnviaCrm", GetEnvServer(), .F., SA1->A1_COD, SA1->A1_LOJA)
Return

User Function EnviaCrm(cCod, cLoja)
    RpcSetType(3)
    RpcSetEnv("99", "01", "admin", "senha")

    Local oHttp := FwHttpClient():New()
    oHttp:SetHeader("Authorization", "Bearer " + GetTokenCRM())
    oHttp:SetHeader("Content-Type", "application/json")

    SA1->(DBSetOrder(1))
    SA1->(DBSeek(xFilial("SA1") + cCod + cLoja))

    Local oPayload := JsonObject():New()
    oPayload["external_id"] := cCod + cLoja
    oPayload["name"]        := AllTrim(SA1->A1_NOME)
    oPayload["document"]    := AllTrim(SA1->A1_CGC)
    oPayload["email"]       := AllTrim(SA1->A1_EMAIL)

    oHttp:Post("https://api.salesforce.com/v2/accounts", oPayload:ToJson())

    RpcClearEnv()
Return

OAuth/Token management

// Token tem expiracao — usar tabela Z pra cache
Static Function GetTokenCRM()
    Z9T->(DBSetOrder(1))
    If Z9T->(DBSeek("CRM" + "TOKEN"))
        If Z9T->Z9T_EXPIRA > Now()
            Return Z9T->Z9T_VALOR
        EndIf
    EndIf

    // Renovar
    Local oHttp := FwHttpClient():New()
    oHttp:Post("https://api.salesforce.com/oauth/token", ;
               "grant_type=client_credentials&client_id=...&client_secret=...")
    Local oResp := JsonObject():New()
    oResp:FromJson(oHttp:GetBody())

    RecLock("Z9T", !Z9T->(Found()))
    Z9T->Z9T_TIPO   := "CRM"
    Z9T->Z9T_CHAVE  := "TOKEN"
    Z9T->Z9T_VALOR  := oResp["access_token"]
    Z9T->Z9T_EXPIRA := Now() + oResp["expires_in"]/86400
    Z9T->(MsUnlock())

Return oResp["access_token"]

Pegadinhas operacionais

Cliente externo recomendado

Veja também