Metadata-Version: 2.5
Name: shade-plugins-hubspot
Version: 0.1.0
Summary: Buscar e atualizar contatos e negócios do HubSpot CRM (upsert por e-mail, negócio por estágio) pela API v3, com o token OAuth da conexão da plataforma.
Author: ShadeOne
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: shade-sdk<0.23,>=0.22.3
Description-Content-Type: text/markdown

# shade-plugins-hubspot

Busca contatos e negócios, cria ou atualiza um contato pelo e-mail e move um negócio de estágio no
[HubSpot CRM](https://developers.hubspot.com/docs/api/crm/understanding-the-crm) (API v3), de dentro
de um robô do [`shade-sdk`](https://pypi.org/project/shade-sdk/), com o token OAuth da conexão do
HubSpot na plataforma. Só a biblioteca padrão do Python: nenhuma dependência além do `shade-sdk`.

No `shade.toml` do robô, o host da API vai em `[hosts] permitidos` e a conexão em `[conexoes]`:

```toml
[hosts]
permitidos = ["api.hubapi.com"]

[conexoes]
nomes = ["hubspot"]

[plugins]
hubspot = "0.1.0"
```

```python
import shade_plugins_hubspot as hubspot

# Leitura: roda inclusive no ensaio.
negocios = hubspot.buscar_negocios({"dealstage": "contractsent"}, propriedades=["dealname"])

# Escrita: só dentro de item.efetivar() (no ensaio não roda).
with item.efetivar("registrar o cliente"):
    hubspot.upsert_contato(item["email"], {"firstname": item["nome"], "phone": item["fone"]})
    hubspot.mover_negocio(negocios[0]["id"], "closedwon")
```

## A conta

- **Conexão:** o robô nunca vê a senha nem o refresh token. O token de acesso chega sob demanda da
  conexão `hubspot` da plataforma e é renovado por ela. Fora da plataforma (no laptop), ponha um
  token de acesso em `SHADE_TOKEN_HUBSPOT`.
- **Escopos:** `crm.objects.contacts.read`, `crm.objects.contacts.write`,
  `crm.objects.deals.read` e `crm.objects.deals.write`. A conexão sem o escopo de uma operação é
  recusada pelo HubSpot, e o erro manda reconectar o HubSpot.
- **Propriedades** são os nomes internos do HubSpot (`firstname`, `dealstage`), e o estágio do
  negócio é o id interno do estágio do funil (`closedwon`), não o nome que aparece na tela.

## O que o plugin garante

- **Contato sem duplicar.** `upsert_contato` usa o upsert em lote do HubSpot com `idProperty=email`:
  o mesmo e-mail é sempre um contato só, e a segunda chamada atualiza as propriedades que cita (as
  outras ficam como estavam). Repetir a chamada é seguro, então a falha transitória é repetida
  dentro do `timeout=`.
- **Negócio sem `PATCH` à toa.** `mover_negocio` lê o estágio e, se o negócio já está no estágio
  pedido, não escreve nada (`movido=False`). Se o `PATCH` sai sem confirmação, o plugin confere o
  estágio: igual ao pedido é sucesso; diferente é `HubSpotIncerto`.
- **Só dentro de `item.efetivar()`.** Fora dele, a operação é um erro de contrato antes de qualquer
  byte; no ensaio, o bloco é pulado e nada é enviado.
- **Valores literais.** Os valores vão como texto, num corpo montado como objeto e serializado pelo
  `shade_sdk.http`, nunca por concatenação. O nome de propriedade, o id e o estágio são validados
  antes de qualquer pedido.
- **Host e prazo:** só `api.hubapi.com`, com o certificado verificado, sem seguir redirecionamento;
  toda chamada aceita `timeout=` (30 s por padrão), que vale para a chamada inteira.
- **Sem conteúdo em evento nem em erro:** cada chamada emite um evento com a operação, o host, a
  duração e o status. Nunca o e-mail, as propriedades, o token nem o corpo da resposta; o erro da
  API chega só pela categoria que o HubSpot dá (`VALIDATION_ERROR`, `MISSING_SCOPES`).

## Operações

| Operação | Efeito | O que faz |
|---|---|---|
| `buscar_contatos(filtros, propriedades=None, *, limite=100)` | não | Contatos que atendem a todos os `filtros`: `id` e `propriedades`. |
| `upsert_contato(email, propriedades)` | sim | Cria ou atualiza o contato do e-mail. Devolve `id`, `email` e `novo`. |
| `buscar_negocios(filtros, propriedades=None, *, limite=100)` | não | Negócios que atendem a todos os `filtros`: `id` e `propriedades`. |
| `mover_negocio(id, estagio)` | sim | Move o negócio para o estágio. Devolve `id`, `estagio` e `movido`. |

Os `filtros` são um dicionário: `{"dealstage": "contractsent"}` (igual) ou
`{"amount": ["GTE", 1000]}` (operador e valor: `EQ`, `NEQ`, `LT`, `LTE`, `GT`, `GTE`,
`CONTAINS_TOKEN`, `NOT_CONTAINS_TOKEN`; `["HAS_PROPERTY"]` e `["NOT_HAS_PROPERTY"]` sem valor). Todos
valem ao mesmo tempo, até 6 por busca.

## Limites

- A busca do HubSpot devolve no máximo 10 000 registros por consulta e enxerga o que acabou de ser
  criado com um pequeno atraso: não use `buscar_contatos` para conferir o contato que o
  `upsert_contato` acabou de gravar (o upsert já devolve o `id`).
- Criar negócio e propriedade customizada ficam fora do plugin.

## Testes

A suíte de contrato do `shade_sdk.testing.plugins` e um servidor falso da API (TLS de uma CA de
teste, que só aceita valor em texto e recusa propriedade e estágio inexistentes, como a de
verdade) acompanham o código-fonte em `tests/`.
