sgp-sdk 1.1.3 → 2.0.0-beta.1

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/COMPATIBILITY.md +151 -0
  3. package/MIGRATION.md +283 -0
  4. package/README.md +424 -225
  5. package/dist/address.d.ts +46 -0
  6. package/dist/annotation-selection.d.ts +32 -0
  7. package/dist/budget.d.ts +29 -0
  8. package/dist/classify.d.ts +15 -0
  9. package/dist/client.d.ts +662 -155
  10. package/dist/config.d.ts +115 -0
  11. package/dist/constants.d.ts +72 -0
  12. package/dist/effect.d.ts +22 -0
  13. package/dist/executor.d.ts +9 -0
  14. package/dist/exposure.d.ts +21 -0
  15. package/dist/guards.d.ts +2 -0
  16. package/dist/hooks.d.ts +16 -0
  17. package/dist/ids.d.ts +19 -0
  18. package/dist/index.d.ts +45 -47
  19. package/dist/index.js +2954 -1223
  20. package/dist/operation-sensitivity.d.ts +496 -0
  21. package/dist/operation.d.ts +139 -0
  22. package/dist/operations/catalogs-list-billing-profiles.d.ts +16 -0
  23. package/dist/operations/catalogs-list-nas.d.ts +25 -0
  24. package/dist/operations/catalogs-list-pops.d.ts +14 -0
  25. package/dist/operations/contracts-list-status-reasons.d.ts +15 -0
  26. package/dist/operations/contracts-list.d.ts +52 -0
  27. package/dist/operations/contracts-update-status.d.ts +41 -0
  28. package/dist/operations/contracts-update.d.ts +39 -0
  29. package/dist/operations/coverage-check.d.ts +42 -0
  30. package/dist/operations/crm-contracts-create-by-cpf-cnpj.d.ts +97 -0
  31. package/dist/operations/crm-contracts-create.d.ts +221 -0
  32. package/dist/operations/crm-contracts-list.d.ts +50 -0
  33. package/dist/operations/crm-customers-create.d.ts +155 -0
  34. package/dist/operations/crm-customers-update-status.d.ts +41 -0
  35. package/dist/operations/customers-list.d.ts +98 -0
  36. package/dist/{endpoints/ura/lookup-customer.d.ts → operations/customers-search.d.ts} +56 -46
  37. package/dist/operations/documents-add.d.ts +29 -0
  38. package/dist/operations/documents-list.d.ts +31 -0
  39. package/dist/operations/experimental-annotations-list.d.ts +67 -0
  40. package/dist/operations/invoices-list.d.ts +142 -0
  41. package/dist/operations/occurrences-create.d.ts +116 -0
  42. package/dist/operations/occurrences-list-methods.d.ts +14 -0
  43. package/dist/operations/occurrences-list-types.d.ts +20 -0
  44. package/dist/{endpoints/ura/list-plans.d.ts → operations/plans-list.d.ts} +33 -23
  45. package/dist/operations/pre-registrations-create.d.ts +253 -0
  46. package/dist/operations/pre-registrations-list-due-days.d.ts +15 -0
  47. package/dist/operations/pre-registrations-list-plans.d.ts +24 -0
  48. package/dist/operations/pre-registrations-list-sellers.d.ts +16 -0
  49. package/dist/operations/services-add-tv.d.ts +47 -0
  50. package/dist/operations/services-change-plan.d.ts +40 -0
  51. package/dist/operations/services-remove-tv.d.ts +29 -0
  52. package/dist/operations/work-orders-list-reasons.d.ts +20 -0
  53. package/dist/operations/work-orders-list.d.ts +81 -0
  54. package/dist/path.d.ts +13 -0
  55. package/dist/raw-request.d.ts +26 -0
  56. package/dist/request.d.ts +28 -0
  57. package/dist/result.d.ts +111 -43
  58. package/dist/retry.d.ts +34 -0
  59. package/dist/sensitivity.d.ts +89 -4
  60. package/dist/tolerant.d.ts +21 -0
  61. package/dist/validation.d.ts +3 -0
  62. package/package.json +6 -3
  63. package/dist/endpoint.d.ts +0 -66
  64. package/dist/endpoints/crm/address.d.ts +0 -20
  65. package/dist/endpoints/crm/create-contract.d.ts +0 -109
  66. package/dist/endpoints/crm/create-customer.d.ts +0 -65
  67. package/dist/endpoints/crm/update-customer-status.d.ts +0 -43
  68. package/dist/endpoints/os/list-occurrence-reasons.d.ts +0 -24
  69. package/dist/endpoints/os/list-occurrence-types.d.ts +0 -25
  70. package/dist/endpoints/precadastro/list-due-days.d.ts +0 -19
  71. package/dist/endpoints/precadastro/list-sellers.d.ts +0 -21
  72. package/dist/endpoints/suporte/add-customer-document.d.ts +0 -24
  73. package/dist/endpoints/suporte/list-customer-documents.d.ts +0 -25
  74. package/dist/endpoints/ura/check-coverage.d.ts +0 -46
  75. package/dist/endpoints/ura/list-clients.d.ts +0 -58
  76. package/dist/endpoints/ura/list-contracts.d.ts +0 -45
  77. package/dist/endpoints/ura/list-invoices.d.ts +0 -107
  78. package/dist/endpoints/ura/list-nas.d.ts +0 -29
  79. package/dist/endpoints/ura/list-occurrence-methods.d.ts +0 -19
  80. package/dist/endpoints/ura/list-payment-carriers.d.ts +0 -20
  81. package/dist/endpoints/ura/list-pops.d.ts +0 -19
  82. package/dist/endpoints/ura/list-service-orders.d.ts +0 -64
  83. package/dist/endpoints/ura/open-ticket.d.ts +0 -81
  84. package/dist/errors.d.ts +0 -59
  85. package/dist/schema/parse.d.ts +0 -19
  86. package/dist/schema/paths.d.ts +0 -38
  87. package/dist/transport.d.ts +0 -65
package/README.md CHANGED
@@ -1,262 +1,461 @@
1
1
  # sgp-sdk
2
2
 
3
- Unofficial TypeScript client for the HTTP API of SGP, the ISP ERP by TSMX. Not affiliated with
4
- TSMX. It is a _dumb_ SDK: typed inputs and outputs, normalized errors, no business rules. Your
5
- application decides what to do with the data.
3
+ Cliente TypeScript não oficial para a API HTTP do SGP, o ERP de provedores de internet da TSMX.
4
+ Sem vínculo com a TSMX.
6
5
 
7
- - Reads, plus writes on `crm`, `ura.openTicket` and `suporte.addCustomerDocument` (see
8
- [Writes](#writes)).
9
- - ESM package with types, Node >= 22 (also runs on Bun). The only runtime dependency is `zod`.
10
- - Methods never throw for network, HTTP or schema failures; they return a result object.
6
+ > **Versão 2.0.** Esta documentação descreve a 2.0 (em preparação, ainda não publicada). A série
7
+ > 1.x está congelada e depreciada: não recebe mais mudanças. Para migrar, leia
8
+ > [MIGRATION.md](MIGRATION.md).
11
9
 
12
- ## Install
10
+ ## O que é e o que não é
11
+
12
+ O sgp-sdk é um **gateway de integração**: encapsula o transporte e a semântica conhecida de cada
13
+ operação do SGP, preserva todos os dados que recebe, devolve falhas com códigos estáveis e diz
14
+ explicitamente o que sabe, e o que não sabe, sobre o efeito de uma escrita.
15
+
16
+ Ele **não é** uma camada de política. Quem decide autorização, orquestração de vários passos,
17
+ recuperação e o que chega a um usuário ou a um agente é a sua aplicação. O SDK também não
18
+ renomeia campos (`cpfcnpj`, `planobase_id` e os demais chegam e saem com os nomes do SGP) e não
19
+ inventa valores: ausente, `null` e `""` chegam como o SGP mandou.
20
+
21
+ - **Somente servidor.** O cliente carrega o token do SGP; navegadores não são suportados.
22
+ - **ESM**, Node >= 22 e Bun. A única dependência de runtime é `zod`. Os testes unitários rodam no
23
+ Bun; o Node é coberto pelo teste de fumaça do pacote construído.
24
+ - Nenhum método lança exceção por falha esperada (input inválido, rede, timeout, cancelamento,
25
+ recusa do SGP, resposta incompatível): tudo volta no resultado. Só `createSgpClient` lança, para
26
+ configuração inválida (`InvalidConfigurationError`), além de bugs internos.
27
+ - Tudo fora de `experimental.*` segue SemVer.
28
+
29
+ Instância de SGP: a versão do SGP em que cada operação foi comprovada é **desconhecida**. O que foi
30
+ provado, onde e quando está em [COMPATIBILITY.md](COMPATIBILITY.md).
31
+
32
+ ## Instalação
13
33
 
14
34
  ```sh
15
35
  bun add sgp-sdk
16
- # or
36
+ # ou
17
37
  npm install sgp-sdk
18
38
  ```
19
39
 
20
- Types resolve under `"moduleResolution": "bundler"` (Bun, Next.js, tsx, Vite, esbuild) and under
21
- `node16` / `nodenext`: the emitted `.d.ts` files carry explicit `.js` extensions.
40
+ Os tipos resolvem com `moduleResolution` `bundler` e `node16`/`nodenext`.
22
41
 
23
- ## Usage
42
+ ## Quickstart
24
43
 
25
44
  ```ts
26
- import { clientPersonalKeys, createSgpClient, omitSensitive } from "sgp-sdk";
45
+ import { CONTRACT_STATUS, createSgpClient } from "sgp-sdk";
27
46
 
28
47
  const sgp = createSgpClient({
29
- baseUrl: "https://sgp.example.com",
30
- token: "<token of your app>",
31
- app: "<app name>",
32
- // timeoutMs: 20_000, // default
33
- // fetch: fakeFetch, // default: the global fetch (inject a fake in tests)
48
+ baseUrl: "https://sgp.example.com", // origem da instância, sem caminho
49
+ credentials: { kind: "token", token: "<token>", app: "<nome do app>" }, // carregue como preferir
34
50
  });
35
51
 
36
- const plans = await sgp.ura.listPlans();
37
- if (plans.success) {
52
+ // Uma leitura: `ok` estreita o resultado.
53
+ const plans = await sgp.plans.list();
54
+ if (plans.ok) {
38
55
  console.log(plans.data.planos.length, plans.warnings ?? []);
39
56
  } else {
40
- console.error(plans.error.code, plans.error.status, plans.error.endpoint);
57
+ console.error(plans.error.code, plans.response?.status);
41
58
  }
42
59
 
43
- const clients = await sgp.ura.listClients({ cpfcnpj: "00000000000", limit: 1 });
44
- if (clients.success) {
45
- for (const client of clients.data.clientes) {
46
- const safe = omitSensitive(client, clientPersonalKeys); // no name, CPF/CNPJ, address, contacts
47
- console.log(safe.id);
48
- }
60
+ // Uma escrita: em caso de falha, `error.effect` diz o que fazer.
61
+ const change = await sgp.contracts.updateStatus({ contrato: 1001, status: CONTRACT_STATUS.active });
62
+ if (change.ok) {
63
+ console.log(change.data.resposta.contrato_status);
64
+ } else if (change.error.effect === "none") {
65
+ // Nada foi aplicado: corrigir a entrada e chamar de novo não duplica nada.
66
+ console.error(change.error.code, change.error.reason);
67
+ } else {
68
+ // "unknown": o SGP pode ter aplicado. Leia o estado (contracts.list) antes de enviar de novo.
49
69
  }
50
70
  ```
51
71
 
52
- The SDK never reads the environment: load the credentials your way and pass them in.
53
-
54
- ### Methods
55
-
56
- Field names in params and responses are exactly what SGP sends. Each method's JSDoc cites the HTTP
57
- method and path.
58
-
59
- | Method | SGP route |
60
- | ------------------------------------------------------- | ------------------------------------------------------- |
61
- | `ura.listClients(params?)` | `POST /api/ura/clientes/` |
62
- | `ura.lookupCustomer(params)` | `POST /api/ura/consultacliente/` |
63
- | `ura.listContracts(params?)` | `POST /api/ura/listacontrato/` |
64
- | `ura.listServiceOrders(params?)` | `POST /api/ura/ordemservico/list/` |
65
- | `ura.listInvoices(params?)` | `POST /api/ura/titulos/` |
66
- | `ura.listPops()` | `POST /api/ura/pops/` |
67
- | `ura.listNas()` | `POST /api/ura/nas/list/` |
68
- | `ura.listPaymentCarriers()` | `GET /api/ura/portador/` |
69
- | `ura.checkCoverage(params)` | `GET /api/ura/viabilidadeinstalacao` |
70
- | `ura.listPlans()` | `GET /api/ura/consultaplano/` |
71
- | `ura.listOccurrenceMethods()` | `GET /api/ura/ocorrencia/metodo/list/` |
72
- | `ura.openTicket(params)` | `POST /api/ura/chamado/` |
73
- | `os.listOccurrenceTypes()` | `GET /api/os/ocorrencia/tipo/list/` |
74
- | `os.listOccurrenceReasons()` | `GET /api/os/ocorrencia/motivo/list/` |
75
- | `precadastro.listDueDays()` | `POST /api/precadastro/vencimento/list` |
76
- | `precadastro.listSellers()` | `POST /api/precadastro/vendedor/list` |
77
- | `crm.createIndividualCustomer(params)` | `POST /api/crm/cliente/F` |
78
- | `crm.createCompanyCustomer(params)` | `POST /api/crm/cliente/J` |
79
- | `crm.createContract(clienteId, params)` | `POST /api/crm/cliente/{cliente_id}/contratos/` |
80
- | `crm.updateCustomerStatus(clienteId, params)` | `POST /api/crm/cliente/{cliente_id}/status/` |
81
- | `suporte.addCustomerDocument(clienteId, file, params?)` | `PUT /api/suporte/cliente/{cliente_id}/documento/add/` |
82
- | `suporte.listCustomerDocuments(clienteId)` | `GET /api/suporte/cliente/{cliente_id}/documento/list/` |
83
-
84
- Every route must be allowed on the consumer's token in SGP (System > Tools > Admin Panel >
85
- Tokens). A route outside the whitelist returns `ROUTE_NOT_ALLOWED`. Paging is the API's own:
86
- `offset` and `limit`; the SDK never pages for you.
87
-
88
- `ura.checkCoverage({ gateway, raio, coordenada })` checks installation viability: the answer is keyed
89
- by the gateway's name (data-defined, e.g. `SGPMaps`) and holds the CTOs within `raio` meters or the
90
- message `"Localização inviável"`. The coordinate (`"lat,lng"`) comes from the consumer; the SDK does
91
- not geocode. `lat`, `lng` and `distancia` are always numbers (Geogrid sends them as numeric strings
92
- and the SDK converts them). A malformed gateway answer, such as a CTO with a non-numeric `lat`, is
93
- `SCHEMA_MISMATCH`.
94
-
95
- ## Result and error contract
96
-
97
- Every method returns `SgpResult<T>`:
72
+ O SDK nunca lê o ambiente: as credenciais vêm de você. Exemplos completos e executados no CI, contra
73
+ um `fetch` falso, estão em `examples/` do repositório: `quickstart.ts`,
74
+ `write-with-effect.ts`,
75
+ `sensitive-logging.ts` e `request-raw.ts` (fora do pacote npm).
76
+ Para rodar um deles: `SGP_BASE_URL=... SGP_TOKEN=... SGP_APP=... bun examples/quickstart.ts`.
77
+
78
+ Cada rota precisa estar liberada no token do SGP, e a barra final conta (as rotas da
79
+ [referência](#referência-de-operações) estão exatamente como o SDK as chama). Rota fora da lista do
80
+ token volta como `FORBIDDEN` com `reason: "TOKEN_ROUTE_NOT_ALLOWED"`.
81
+
82
+ Nas rotas `GET`, `token` e `app` vão na query string; nas demais, no corpo. Uma camada de `fetch`,
83
+ proxy ou tracing que você injete enxerga isso.
84
+
85
+ ## O modelo de resultado
86
+
87
+ Toda operação devolve `SgpResult<T>`:
98
88
 
99
89
  ```ts
100
90
  type SgpResult<T> =
101
- | { success: true; data: T; warnings?: SchemaWarning[] }
102
- | { success: false; error: SgpError };
91
+ | {
92
+ ok: true;
93
+ data: T;
94
+ response: CapturedResponse;
95
+ attempts: number;
96
+ warnings?: SchemaWarning[];
97
+ diagnostics?: Diagnostic[];
98
+ }
99
+ | {
100
+ ok: false;
101
+ error: SgpError;
102
+ response: CapturedResponse | null;
103
+ attempts: number;
104
+ diagnostics?: Diagnostic[];
105
+ };
103
106
 
104
107
  interface SgpError {
105
- status: number; // HTTP status, 0 when no response arrived
106
- code: SgpErrorCode;
107
- message: string; // written by the SDK, in English
108
- detail?: string; // SGP's own error text (`detail`, `msg`, `errors` or `error`), copied verbatim
109
- endpoint: string; // path only, never the query string
108
+ code: SgpErrorCode; // estável: ramifique por ele, nunca por `message`
109
+ message: string; // escrita pelo SDK, em inglês
110
+ operation: string; // ex.: "customers.list"
111
+ reason?: string; // código específico da operação, só com evidência
112
+ effect?: "none" | "unknown"; // só em escritas
113
+ sgp?: { detail?: unknown; msg?: unknown; error?: unknown; errors?: unknown }; // campos de erro do SGP, como vieram
114
+ issues?: readonly string[]; // em INVALID_RESPONSE: caminhos que quebraram o contrato, nunca valores
110
115
  }
116
+ ```
111
117
 
112
- interface SchemaWarning {
113
- path: string; // e.g. "contratos[0].cobIsento"
114
- expected: string; // type names only, never values
115
- received: string;
116
- }
118
+ ### Códigos
119
+
120
+ | Código | Quando |
121
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
122
+ | `INVALID_INPUT` | Entrada local inválida (campos, chaves desconhecidas, parâmetros de caminho, opções da chamada). Nada é enviado: `attempts: 0`, `response: null` |
123
+ | `UNAUTHORIZED` | HTTP 401, ou 403 cujo `detail` diz que as credenciais estão incorretas |
124
+ | `FORBIDDEN` | Demais 403. Com `reason: "TOKEN_ROUTE_NOT_ALLOWED"` quando o `detail` diz que as credenciais "não foram fornecidas" (rota fora da lista do token, ou barra final errada) |
125
+ | `RATE_LIMITED` | HTTP 429 (nunca observado na instância) |
126
+ | `ROUTE_NOT_ALLOWED` | Restrição local do SDK: hoje só `request()` na rota de anotações (use `experimental.annotations.list`). Nada é enviado; nunca mapeia o 405 do HTTP |
127
+ | `NOT_FOUND` | HTTP 404 (rota ou recurso; o significado específico, só via `reason`) |
128
+ | `REJECTED` | Demais 4xx (400, 402, 405, 409, 422...), inclusive validação de campo do SGP, e recusas conhecidas que o SGP devolve com HTTP 200 |
129
+ | `UPSTREAM` | HTTP 5xx |
130
+ | `NETWORK` | Falha de transporte (DNS, conexão, TLS, corpo interrompido) |
131
+ | `TIMEOUT` | O prazo do SDK expirou (`timeoutMs`) |
132
+ | `ABORTED` | O seu `abortSignal` cancelou |
133
+ | `INVALID_RESPONSE` | 2xx fora do contrato do método, ou status que o método não aceita (3xx: redirecionamentos nunca são seguidos) |
134
+
135
+ O status HTTP recebido sempre decide o código; um corpo ilegível não apaga um status já recebido
136
+ (vira `response.body.kind === "unavailable"`).
137
+
138
+ ### `reason`
139
+
140
+ Código específico de uma operação, **só** onde há evidência por operação; nunca um mapa global de
141
+ status. Os existentes:
142
+
143
+ | `reason` | Operação | Situação |
144
+ | ----------------------------- | ------------------------------------------ | -------------------------------------- |
145
+ | `TOKEN_ROUTE_NOT_ALLOWED` | qualquer (com `FORBIDDEN`) | 403 "credenciais não foram fornecidas" |
146
+ | `DOCUMENT_ALREADY_REGISTERED` | `preRegistrations.create` | 402, CPF já cadastrado |
147
+ | `STATUS_NOT_CHANGED` | `contracts.updateStatus` | 400 "Status de Contrato não alterado." |
148
+ | `CUSTOMER_NOT_IN_CRM` | `crm.contracts.create`, `.createByCpfCnpj` | 400 "Não encontrado cliente CRM ..." |
149
+ | `CONTRACT_NOT_ACTIVE` | `services.addTv` | 400, contrato não ativo |
150
+
151
+ Sem `reason`, a falha continua útil: `code`, `effect`, `sgp` e `response.body` dizem tudo o que o
152
+ SGP disse.
153
+
154
+ ### `effect`: o que se sabe sobre uma escrita
155
+
156
+ Só escritas têm `error.effect`; leituras não têm. O código sozinho não determina o efeito (e "leitura"
157
+ é propriedade da operação conhecida, nunca do verbo HTTP).
158
+
159
+ - **`"none"`**: o SDK consegue afirmar que a mudança pedida **não** foi aplicada. Acontece em recusa
160
+ local (`INVALID_INPUT`), cancelamento antes do envio, recusa conhecida da operação (tabela de
161
+ `reason` acima) e 4xx do SGP com formato de erro de campo ou de negócio (`errors` como objeto,
162
+ `error` ou `detail` como texto). Corrigir a entrada e chamar de novo é seguro.
163
+ - **`"unknown"`**: o SGP pode ter aplicado. Vale para timeout, falha de rede, cancelamento depois do
164
+ envio, 5xx, 3xx, 2xx fora do contrato, 4xx com corpo ilegível ou de formato não reconhecido, a
165
+ recusa com HTTP 200 de `occurrences.create` (`status: 0`) e o "invólucro genérico" do SGP
166
+ (`errors` como **array**, "Ocorreu um erro: ..."), salvo quando a operação o declara como
167
+ recusa conhecida.
168
+
169
+ Uma observação de "nada foi aplicado" é registrada como observação com seu método de verificação
170
+ (releitura), não como garantia.
171
+
172
+ **Diante de `"unknown"`, não reenvie às cegas.** Releia o estado e só então decida:
173
+
174
+ | Escrita que ficou em `unknown` | Leitura que verifica |
175
+ | ------------------------------------------------------------ | ------------------------------------------------------------------------------ |
176
+ | `crm.customers.create` | `customers.search` ou `customers.list` pelo documento |
177
+ | `crm.contracts.create`, `crm.contracts.createByCpfCnpj` | `crm.contracts.list` (por documento) |
178
+ | `crm.customers.updateStatus` | ler o cliente antes de reenviar |
179
+ | `contracts.updateStatus`, `contracts.update` | `contracts.list({ contrato })` |
180
+ | `preRegistrations.create` | `customers.list({ cpfcnpj })` (a resposta de sucesso não traz ids) |
181
+ | `occurrences.create` | `workOrders.list({ contrato })`; com `desativa_os: 1` não há OS para consultar |
182
+ | `services.changePlan`, `services.addTv`, `services.removeTv` | `customers.list` (serviços do contrato) |
183
+ | `documents.add` | `documents.list` |
184
+
185
+ ### `sgp`, `response`, `warnings`, `attempts`
186
+
187
+ - **`error.sgp`** projeta os campos de erro do SGP (`detail`, `msg`, `error`, `errors`) com os
188
+ valores como vieram. O corpo inteiro continua em `response.body`.
189
+ - **`response`** é a resposta HTTP bruta: `{ status, headers, body }`, com nomes de header em
190
+ minúsculas. `body` é um valor discriminado: `{ kind: "json", value }` (o valor pode ser `null`),
191
+ `{ kind: "text", text }`, `{ kind: "empty" }`, `{ kind: "unavailable", reason }` (o corpo começou e
192
+ não foi lido até o fim) ou `{ kind: "withheld" }` (veja [leituras com escopo](#leituras-com-escopo)).
193
+ É o corpo já decodificado, não os bytes. `response` é `null` quando nenhuma resposta HTTP existiu.
194
+ - **`warnings`** aparece quando um campo **secundário** veio com tipo inesperado: o campo sai de
195
+ `data`, e `{ path, dropped, expected, received }` registra o caminho e os nomes dos tipos (nunca
196
+ valores). O valor original continua intacto em `response.body`. Os campos **estruturais** de cada
197
+ operação (ids e o que a navegação precisa) são validados com rigor: se falharem, o resultado é
198
+ `INVALID_RESPONSE`.
199
+ - **`attempts`** conta as requisições que o SDK iniciou: 0 quando recusou localmente, 1 na primeira.
200
+ - **Fidelidade.** Campos conhecidos são tipados; os desconhecidos chegam em `data` em todos os níveis
201
+ (raiz, aninhados, arrays), sem serem descartados. Um item que o SDK não consegue interpretar faz a
202
+ resposta falhar com `INVALID_RESPONSE`; nunca vira `{}` nem `[]`.
203
+ - **Paginação.** `customers.list`, `workOrders.list` e `invoices.list` devolvem **uma página**, com
204
+ a `paginacao` do SGP como veio (`total` nunca é calculado pelo SDK). Para a próxima página, passe
205
+ `offset`. Listas sem paginação mantêm o formato do SGP (array ou objeto), sem envelope.
206
+ - **Credenciais nos headers.** Os headers capturados nunca carregam o token nem o app: o valor de
207
+ qualquer parâmetro `token`/`app` é trocado por `<redacted>`, e o valor exato das credenciais (puro
208
+ ou codificado) também é trocado quando tem 8 caracteres ou mais. **Corpos não são alterados.**
209
+
210
+ ## Tempo, cancelamento e retry
211
+
212
+ ```ts
213
+ const controller = new AbortController();
214
+ const result = await sgp.customers.list(
215
+ { limit: 20 },
216
+ {
217
+ timeoutMs: 5_000, // prazo desta chamada (padrão do cliente: 20 000 ms)
218
+ abortSignal: controller.signal,
219
+ maxRetries: 2, // só leituras
220
+ },
221
+ );
117
222
  ```
118
223
 
119
- | Code | When |
120
- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
121
- | `UNAUTHORIZED` | 401/403 with `detail` (or `msg`) `"Credenciais de autenticação incorretas."` (wrong token or app) |
122
- | `ROUTE_NOT_ALLOWED` | 401/403 with `detail` (or `msg`) `"As credenciais de autenticação não foram fornecidas."`. The SDK always sends credentials, so this means the route is not on the token's whitelist |
123
- | `NOT_FOUND` | 404 |
124
- | `METHOD_NOT_ALLOWED` | 405 |
125
- | `VALIDATION` | any other 4xx: SGP refused the input |
126
- | `SCHEMA_MISMATCH` | 2xx whose body is not JSON, or whose key fields are missing or have the wrong type (`detail` then lists the failing paths) |
127
- | `UPSTREAM` | 5xx |
128
- | `TIMEOUT` | the request exceeded `timeoutMs` |
129
- | `NETWORK` | `fetch` failed before a response (DNS, connection, TLS) |
130
- | `UNKNOWN` | 401/403 with any other `detail` or `msg`, or a status the rows above do not cover |
131
-
132
- SGP answers errors in four formats: `{"detail": "..."}`, `{"msg": "..."}`, `{"errors": ...}` (an
133
- array of messages or an object of field messages, on CRM routes) and `{"error": "..."}` (e.g. a 404
134
- `Cadastro não encontrado` on CRM routes). `error.detail` holds, in this order: a string `detail`
135
- (even empty), a non-empty `msg`, the text of `errors`, a non-empty `error`.
136
-
137
- Guarantees:
138
-
139
- - The SDK never puts the token, the app, a URL with query string, a raw response body or the
140
- original message of a `fetch` exception in anything it returns (`data`, `warnings`, `error`).
141
- - `error.detail` is SGP's text copied verbatim: it may echo values from your request (for example
142
- a contract ID or a CPF in a `msg`). The SDK adds nothing to it, so treat it like the request
143
- itself before logging or showing it.
144
- - 401/403 are classified by that exact `detail` string, not by status (SGP uses either status for
145
- the same message). If SGP rewords those messages the code falls to `UNKNOWN`.
146
- - No retry, no cache, no logging, no automatic paging.
147
- - Redirects are never followed, so the token and payload are not re-sent to another host: a 3xx
148
- answer is an `UNKNOWN` error carrying its status (`outcome: "unknown"` on a write).
149
- - Unknown keys are dropped at every level (`strip`): everything that reaches you was declared and
150
- classified. Required key fields that fail validation are `SCHEMA_MISMATCH`; any other field with
151
- an unexpected type becomes `undefined` and is reported in `warnings`.
152
-
153
- ## Writes
154
-
155
- Write methods (`crm.*`, `ura.openTicket` and `suporte.addCustomerDocument`) return
156
- `SgpWriteResult<T>`: the same as `SgpResult<T>`, but `error` also carries an `outcome` that says
157
- whether SGP applied the write.
158
-
159
- - `"not_applied"`: SGP refused the request (4xx) or the SDK rejected it before sending it. SGP did
160
- not apply the write; fix the input and call again.
161
- - `"unknown"`: no answer (`TIMEOUT`, `NETWORK`), a 5xx, a 3xx (`UNKNOWN`: redirects are never
162
- followed), or a 2xx whose body does not confirm the write (`VALIDATION` with the real HTTP
163
- status). The write may have happened: check the state in SGP before repeating it.
164
- - A 4xx whose body has `errors` as an array (e.g. a 400 with `{"errors": ["Ocorreu um erro: ..."]}`)
165
- is SGP's generic exception wrapper: it can fail after applying part of the write, so it is
166
- `"unknown"`, even when `detail` or `msg` come with it. The `code` stays `VALIDATION` and the
167
- `message` reads "SGP reported an error while processing the write; it may have been applied."
168
- 401/403/404/405/422 stay `"not_applied"` whatever the body, and so does a 4xx whose `errors` is an
169
- object of field messages or that carries only `detail` or `msg`.
170
-
171
- After `outcome: "unknown"`, read the state back before repeating the write:
172
-
173
- | Write | Read that verifies it |
174
- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
175
- | `crm.createIndividualCustomer`, `crm.createCompanyCustomer` | `ura.listClients({ cpfcnpj })` |
176
- | `crm.createContract` | `ura.lookupCustomer` or `ura.listContracts` |
177
- | `ura.openTicket` | `ura.listServiceOrders({ contrato })`; a ticket with `desativa_os: 1` opens no service order, so there is nothing to look up |
178
- | `suporte.addCustomerDocument` | `suporte.listCustomerDocuments` |
179
- | `crm.updateCustomerStatus` | run it again; for status 6 on an existing CRM customer, a 404 `NOT_FOUND` means it was applied (it leaves the CRM) |
180
-
181
- A write only succeeds on a 2xx whose body carries the key fields (for example `cliente_id`). The one
182
- exception is `suporte.addCustomerDocument`: SGP answers an empty 2xx, which succeeds with
183
- `data: null`. Success there does not prove the file was stored; verify it with
184
- `suporte.listCustomerDocuments`. The SDK never retries a write.
185
-
186
- Sales flow: create the customer (`crm.createIndividualCustomer` or `crm.createCompanyCustomer`),
187
- create the contract on it (`crm.createContract(clienteId, params)`), then call
188
- `crm.updateCustomerStatus(clienteId, { status_id: crmStatus.contratoRealizadoNoSgp })` (status 6).
189
- Without that last step SGP's screen keeps the customer as a CRM customer and blocks contract status
190
- changes.
191
-
192
- `crm.createContract` sends the installation address nested (`endereco_instalacao`) and also as the
193
- loose fields (`logradouro`, `numero`, ...), and the billing address only nested
194
- (`endereco_cobranca`). The loose fields carry the installation address; a billing address that
195
- differs from the installation one has not been verified live, so check it on SGP before sending
196
- divergent addresses.
197
-
198
- Scheduling an installation goes through `ura.openTicket` with `data_hora_agendamento`
199
- (`"AAAA-MM-DD HH:MM"`); `os_instalacao` in `crm.createContract` opens the order without a date.
200
-
201
- The token whitelist accepts regular expressions and the trailing slash counts: release each path
202
- exactly as in the table above, so `/api/crm/cliente/F` has no final `/` and
203
- `/api/crm/cliente/{cliente_id}/status/` has one (the `{cliente_id}` routes need a pattern that
204
- matches any ID).
205
-
206
- ## Sensitive fields
207
-
208
- SGP responses carry credentials and personal data. The SDK transcribes them (the consumer decides
209
- what to show) and marks them: each endpoint exports `as const` tuples with the keys of one response
210
- item.
211
-
212
- - `secret`: Central password, PPPoE login and password, Wi-Fi passwords and SSIDs, MACs, links with
213
- an embedded token.
214
- - `personal`: CPF/CNPJ, phone, e-mail, address, birth date, customer name, free text typed by
215
- operators.
216
-
217
- Tuple names follow the item: `clientSecretKeys` / `clientPersonalKeys`,
218
- `customerContractSecretKeys` / `customerContractPersonalKeys`, and so on (see the package exports).
219
- `omitSensitive(item, keys)` returns a shallow copy without those keys and removes them from the
220
- type, so a leak becomes a compile error:
224
+ - **Um único orçamento de tempo por chamada lógica**: tentativas, esperas e leitura do corpo. Nunca
225
+ reinicia por tentativa. `timeoutMs` por chamada substitui o padrão do cliente (é padrão, não teto).
226
+ Deve ser um número positivo, finito, até `MAX_TIMEOUT_MS`.
227
+ - **`TIMEOUT`** é o prazo do SDK; **`ABORTED`** é o seu `abortSignal`. Se o sinal já estiver
228
+ cancelado quando você chama, nada é enviado (`effect: "none"` numa escrita); cancelado depois do
229
+ envio, `effect: "unknown"`.
230
+ - **Retry é opt-in** (`maxRetries`, padrão 0 = nenhuma nova tentativa), vale **só para leituras**
231
+ e só para falhas transitórias: `RATE_LIMITED`, `NETWORK` e `UPSTREAM` com HTTP 502, 503 ou 504.
232
+ Há espera exponencial com jitter (base de 500 ms, teto de 10 s) e `Retry-After` (segundos ou data
233
+ HTTP) é respeitado como piso, nunca encurtado; se a espera não cabe no orçamento, a chamada
234
+ termina com a última falha. `TIMEOUT`, `ABORTED`, `INVALID_*`, `UNAUTHORIZED`, `FORBIDDEN`,
235
+ `NOT_FOUND` e `REJECTED` nunca são repetidos.
236
+ - **Escritas nunca são repetidas pelo SDK**, e `request()` também não, qualquer que seja
237
+ `maxRetries`. Conferir o `attempts` mostra quantas requisições partiram.
238
+
239
+ ## Hooks
240
+
241
+ ```ts
242
+ const sgp = createSgpClient({
243
+ baseUrl,
244
+ credentials,
245
+ hooks: {
246
+ onRequest: (m) => log.debug({ op: m.operation, attempt: m.attempt }),
247
+ onResponse: (m) => log.info({ op: m.operation, status: m.status, ms: m.durationMs }),
248
+ onError: (m) => log.warn({ op: m.operation, code: m.code }),
249
+ },
250
+ });
251
+ ```
252
+
253
+ Opcionais, **síncronos** e só para observabilidade. Recebem metadados: `callId` (liga as tentativas de
254
+ uma chamada), `attempt` (1, 2, ...), `operation`, `method`, `pathTemplate` (omitido quando
255
+ desconhecido, como em `request()`) e, conforme o hook, `status`, `code` e `durationMs`. Nunca
256
+ recebem credenciais, URLs nem corpos. `onResponse` roda por tentativa quando os headers chegam;
257
+ `onError`, por tentativa que falhou.
258
+
259
+ Um hook que lança (ou devolve uma Promise, que quebra o contrato síncrono) **nunca altera o
260
+ resultado**: uma escrita confirmada continua confirmada. O fato aparece em `diagnostics`
261
+ (`{ hook, occurrence }`), sem o valor lançado.
262
+
263
+ ## Dados sensíveis
264
+
265
+ Fidelidade significa que os dados do SGP chegam inteiros, e o SGP mistura credenciais (logins e senhas
266
+ de Central e PPPoE, Wi-Fi, MAC, IP) e dados pessoais de clientes nas respostas. **A sua aplicação
267
+ projeta o que sai do processo.**
268
+
269
+ - **`getSensitivity("customers.search")`** devolve, por operação, as chaves secretas e pessoais da
270
+ entrada (`input`) e da resposta (`output`). Serve para saber o que remover. Uma chave que não está
271
+ listada **não é segura**: o SGP acrescenta campos sem aviso.
272
+ - **`pick(origem, allowlist)`** copia só os caminhos nomeados (`"clientes[].contratos[].id"`;
273
+ `"[].id"` para array na raiz). Chave não nomeada nunca sai. Use para tudo o que vai a logs,
274
+ ferramentas, MCP ou agentes.
275
+ - **`toSafeLog(result)`** devolve a versão para log de um resultado: `ok`, `attempts`, `diagnostics`,
276
+ `warnings` (caminhos e nomes de tipo), status HTTP e, do erro, `code`, `message`, `operation`,
277
+ `reason`, `effect`, `issues` e os **nomes** dos campos do SGP que vieram. Sem `data`, sem `response.body`, sem headers, sem o texto de `error.sgp`
278
+ (que costuma repetir o que você enviou).
279
+
280
+ ```ts
281
+ logger.info(toSafeLog(result)); // nunca `logger.info(result)`
282
+ const forTheAgent = pick(result.data, ["contratos[].contratoId", "contratos[].clienteId"]);
283
+ ```
284
+
285
+ **Nunca registre o resultado cru.** `console.log(result)`, `structuredClone(result)`, um logger que
286
+ percorre o objeto ou uma cópia feita antes de `toSafeLog` enxergam tudo. E os corpos em `response.body`
287
+ **não são redigidos**: podem repetir credenciais ou dados pessoais que o SGP devolveu, e o texto de
288
+ erro do SGP pode ecoar o que você enviou. Os headers são redigidos; os corpos, não.
289
+
290
+ Veja `examples/sensitive-logging.ts` no repositório.
291
+
292
+ ## Leituras com escopo
293
+
294
+ Princípio: leitor tolerante na interpretação, **fronteira estrita** no isolamento. A tolerância à
295
+ evolução do contrato do SGP nunca alarga o que sai de uma operação.
296
+
297
+ `experimental.annotations.list` (anotações de um cliente e/ou contrato) é o caso: o SGP ignora os
298
+ filtros e responde com anotações de **outros** clientes. O SDK seleciona localmente, sobre a única
299
+ resposta recebida:
300
+
301
+ - `response.body` é `{ kind: "withheld" }` em qualquer status, `error.sgp` guarda só textos, e nenhum
302
+ hook recebe o corpo. Cada anotação selecionada traz `raw`, uma cópia profunda do registro como o SGP
303
+ mandou.
304
+ - Uma anotação cuja identidade (cliente ou contrato) não pode ser decidida, ou que embute identidade
305
+ de outro cliente em qualquer profundidade, faz a chamada inteira falhar com `INVALID_RESPONSE`
306
+ (`error.issues` aponta os caminhos, sem valores). Falha fechada, nunca pulo silencioso.
307
+ - `request()` recusa essa rota (`ROUTE_NOT_ALLOWED`), para a fronteira não ser contornada.
308
+ - Não é uma fronteira de autorização: autorize os ids que você passa e rode isso só em processo de
309
+ servidor confiável. Uma camada de `fetch` ou tracing que você injete enxerga o corpo inteiro **antes**
310
+ da seleção e não deve capturá-lo.
311
+ - É **experimental** e pode mudar de forma incompatível em uma versão menor. A completude não é
312
+ garantida: o SDK faz uma requisição, nunca pagina e nunca trunca; a rota não documenta paginação.
313
+
314
+ ## `request()`: chamada bruta
315
+
316
+ Para uma rota que nenhum método cobre ainda, com a mesma autenticação, prazo, cancelamento e hooks.
317
+ **Ferramenta de desenvolvedor: nunca a exponha como ferramenta de agente nem monte `path` ou `body`
318
+ a partir de entrada não confiável.**
221
319
 
222
320
  ```ts
223
- const safe = omitSensitive(client, clientPersonalKeys);
224
- safe.nome; // type error: `nome` was omitted
321
+ const raw = await sgp.request({ method: "POST", path: "/api/ura/clientes/", body: { limit: 1 } });
225
322
  ```
226
323
 
227
- The SDK only marks and offers the helper; deciding when to apply it is the consumer's job. Strip
228
- before logging, caching or forwarding an item to a model.
229
-
230
- ## Known limitations
231
-
232
- - **Placeholder arrays.** These fields are declared as arrays of empty objects
233
- (`z.array(z.object({}))`) because no sample response had content for them. Every element arrives
234
- as `{}`, with no warning, and what SGP sends inside is dropped:
235
- - `listClients`: `clientes[].contratos` and `clientes[].titulos` (use `lookupCustomer` and the
236
- contract methods for those details);
237
- - `lookupCustomer`: `contratos[].telefones_cargos`, `contratos[].outros` and
238
- `contratos[].anotacoes`;
239
- - `listServiceOrders`: `ordens_servicos[].tecnicos_auxiliares` and
240
- `ordens_servicos[].classificacoes`;
241
- - `listPlans`: `planos[].combo`;
242
- - `os.listOccurrenceTypes` and `os.listOccurrenceReasons`: `classificacoes` and `setores` of
243
- each item;
244
- - `listInvoices`: `acordados` and `cobrados` of `titulos[].acordoPagamento`.
245
- - **Guessed types.** SGP only sent `null` for four `lookupCustomer` fields, so their types are
246
- inferred: `contratos[].contrato_unificado_principal` (number),
247
- `contratos[].contrato_unificado_filhos` (array of numbers), `contratos[].endereco_pontoreferencia`
248
- (string) and `contratos[].tags[].categoria_id` (number). A wrong guess does not fail the call: the
249
- field comes out as `undefined` and is reported in `warnings`.
250
- - **A bad element drops the whole array.** When one element of an array field has the wrong type,
251
- the whole field becomes `undefined`, and the warning names only the failing element. For example,
252
- a non-string entry in `clientes[].contatos.telefones` drops the whole `telefones` array and the
253
- warning path is `clientes[0].contatos.telefones[1]`.
254
- - `listContracts` does not transcribe the address fields returned by `exibir_endereco`.
255
-
256
- ## Pending
257
-
258
- What is left:
259
-
260
- - **E-signature of the adhesion term.** Not available in the SGP API.
261
- - **Boleto delivery methods and invoice (nota fiscal) fields** of the contract. Not available in the
262
- SGP API, so `crm.createContract` leaves them empty.
324
+ - Só caminho relativo sob `/api/` da instância configurada, já na forma final (sem query, `//`,
325
+ `.`/`..`, `%`). Qualquer outro: `INVALID_INPUT`, `effect: "none"`, `attempts: 0`, nada enviado.
326
+ Parâmetros de um `GET` vão em `body` (só primitivos). `encoding`: `json` (padrão) ou `form`.
327
+ - Classifica só por HTTP, com o mesmo mapa de status dos métodos. **`ok: true` significa "o SGP
328
+ aceitou em nível HTTP" (2xx), não sucesso de negócio**: o SGP responde algumas recusas com 200,
329
+ então leia `data` (o corpo como veio, tipado `unknown`).
330
+ - Nunca segue redirecionamentos (3xx é `INVALID_RESPONSE`) e nunca repete.
331
+ - Depois do envio, toda falha tem `effect: "unknown"`, inclusive 4xx; o verbo nunca é tomado como
332
+ prova de que a chamada só lê.
333
+ - Não declara chaves sensíveis: trate `data` e `response` como sensíveis.
334
+
335
+ Veja `examples/request-raw.ts` no repositório.
336
+
337
+ ## IDs com marca e os dois catálogos de planos
338
+
339
+ Alguns ids têm tipo "com marca" (apenas em tempo de compilação; em runtime são números):
340
+
341
+ | Tipo | De onde vem | Para onde vai |
342
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
343
+ | `CustomerId` | `customers.list` (`clientes[].id`), `customers.search` (`clienteId`), `crm.customers.create` (`cliente_id`) | `cliente_id` de `crm.*`, `documents.*`, `customers.list`, `invoices.list` |
344
+ | `PlanId` | `plans.list` (`planos[].id`) | `plano` de `contracts.list`, `plano_id` de `services.changePlan` e de `crm.contracts.create`, `planobase_id` de `services.addTv` |
345
+ | `PreRegistrationPlanId` | `preRegistrations.listPlans` (`id`) | `plano_id` de `preRegistrations.create` |
346
+
347
+ Um cliente CRM tem o mesmo `CustomerId` do cliente. **O id de um contrato não é um `CustomerId`**, e
348
+ o compilador recusa um no lugar do outro e um número solto onde um id é esperado. Para marcar um
349
+ número em que você confia: `as CustomerId`.
350
+
351
+ **Há dois catálogos de planos, com espaços de ids diferentes**: o mesmo plano comercial pode ter outro
352
+ id no catálogo do pré-cadastro, e um id aceito por uma rota pode ser recusado pela outra. Os tipos
353
+ `PlanId` e `PreRegistrationPlanId` são mantidos separados de propósito, nos dois sentidos. A
354
+ conversão entre catálogos não é feita pelo SDK.
355
+
356
+ ## Constantes
357
+
358
+ Códigos fixos do SGP exportados como constantes; as operações aceitam qualquer número e o SGP
359
+ decide.
360
+
361
+ | Constante | Valores | Evidência |
362
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
363
+ | `CONTRACT_STATUS` | `active` 1, `inactive` 2, `canceled` 3, `suspended` 4, `technicalInfeasibility` 5, `new` 6, `activeReducedSpeed` 7 | 1 e 4 confirmados (200 e releitura); 2 enviado e recusado a partir de Ativo; 3, 5 e 7 do painel; 6 documentado pelo fornecedor, nunca observado |
364
+ | `BILLING_METHOD` | `boleto` 1, `automaticDebit` 2, `booklet` 3, `recurringCharge` 4, `pix` 6 | códigos globais fixos; o 5 não é listado (documentado para uma rota, nunca confirmado) |
365
+ | `ACQUISITION_MODE` | `owned` 0, `loan` 1 | o valor 2, que a documentação do fornecedor lista para o CRM, foi recusado ao vivo |
366
+ | `SERVICE_TYPE` | `internet` 1 | só o 1 foi enviado ao SGP |
367
+ | `CRM_STATUS` | `underAnalysis` 1 ... `technicalFeasibilityAnalysis` 10 | só o 6 (`contractMadeInSgp`) foi enviado ao vivo |
368
+
369
+ O SDK não guarda tabela de transições de status: quem decide o que é permitido é o SGP.
370
+
371
+ ## Referência de operações
372
+
373
+ Cada método aceita `options?: CallOptions` (`abortSignal`, `timeoutMs`, `maxRetries`) como último
374
+ argumento. Campos de entrada e de resposta usam os nomes do SGP. Uma entrada com chave desconhecida é
375
+ recusada localmente (`INVALID_INPUT`); para campos que ainda não têm tipo, use `request()`. O JSDoc de
376
+ cada método, no tipo `SgpClient` (nos tipos publicados, `dist/client.d.ts`), é o texto de referência; aqui
377
+ ficam só rota, codificação, natureza, recusas conhecidas e evidência.
378
+
379
+ Legenda. **Natureza:** _leitura_ é repetível com `maxRetries`; _escrita_ nunca é repetida.
380
+ **Codificação:** `query` (GET, parâmetros na URL), `json`, `form` (multipart sem arquivos),
381
+ `form-files` (multipart com arquivo). **Evidência**, resumida (detalhes por cenário em
382
+ [COMPATIBILITY.md](COMPATIBILITY.md)): _observado_ (resposta real capturada), _parcial_ (sucesso
383
+ observado, mas recusas só documentadas ou reconstruídas, ou o inverso), _documentado_ (só a
384
+ documentação do fornecedor), _reconstruído_ (corpo reconstruído a partir de relatos ou testes, sem
385
+ captura bruta).
386
+
387
+ ### `customers`
388
+
389
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
390
+ | ------------------------- | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
391
+ | `customers.list(input?)` | `POST /api/ura/clientes/` · json | leitura | Uma página; `paginacao` do SGP como veio; filtros `cliente_id`, `cpfcnpj`, `cliente_nome`; contratos e serviços aninhados chegam inteiros | observado |
392
+ | `customers.search(input)` | `POST /api/ura/consultacliente/` · json | leitura | Devolve **candidatos** e nunca escolhe um; ao menos um critério (`cpfcnpj`, `contrato`, `nome`, `login`, `telefone`); texto vazio é recusado; sem resultado é sucesso com `contratos: []`; cada candidato traz credenciais do serviço | observado (corpo mascarado, 1.x) |
393
+
394
+ ### `crm.customers` e `crm.contracts`
395
+
396
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
397
+ | -------------------------------------- | ------------------------------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
398
+ | `crm.customers.create(input)` | `POST /api/crm/cliente/{F\|J}` · json | escrita | `personType` escolhe a rota; `endereco` aninhado, como o SGP o lê. **Aceita CPF já cadastrado e cria OUTRO cliente com o mesmo documento**: confira antes (`customers.search`). 400 de validação de campo: `REJECTED`, `effect: "none"` | observado (`/F`); `/J` documentado |
399
+ | `crm.customers.updateStatus(input)` | `POST /api/crm/cliente/{cliente_id}/status/` · json | escrita | Códigos em `CRM_STATUS`. Cliente que já saiu do CRM (ou status 6 repetido): 404 `{error:"Cadastro não encontrado"}`, `NOT_FOUND`, `effect: "none"` | observado (status 6; o 404 vem de captura da 1.x) |
400
+ | `crm.contracts.create(input)` | `POST /api/crm/cliente/{cliente_id}/contratos/` · json | escrita | Só para cliente ainda no funil CRM. Recusa `CUSTOMER_NOT_IN_CRM`, `effect: "none"`. `plano_id` é `PlanId`; `modoaquisicao` é `ACQUISITION_MODE` | parcial |
401
+ | `crm.contracts.createByCpfCnpj(input)` | `POST /api/crm/cliente/contratos/?cpfcnpj=` · json | escrita | Documento na **query** do POST. Mesma recusa `CUSTOMER_NOT_IN_CRM`. Com documento duplicado, qual cliente recebe o contrato não foi verificado. **Nenhum sucesso foi observado nesta rota** | parcial (sucesso só documentado) |
402
+ | `crm.contracts.list(input)` | `GET /api/crm/cliente/contratos/?cpfcnpj=` · query | leitura | Documento que não é cliente CRM: 400, `REJECTED`, sem `reason` | observado |
403
+
404
+ ### `preRegistrations`
405
+
406
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
407
+ | -------------------------------- | ----------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
408
+ | `preRegistrations.create(input)` | `POST /api/precadastro/{F\|J}` · form | escrita | Endereço em campos soltos no topo da entrada (`logradouro`, `numero`, ...), com os nomes do SGP. Com `precadastro_ativar: 1` vira cliente definitivo; contrato só se plano e cobrança forem suficientes. Sucesso traz só `message`, **sem ids**. CPF existente: 402, `REJECTED`, `DOCUMENT_ALREADY_REGISTERED`, `effect: "none"` | parcial (recusa observada; sucesso `{message}` do fornecedor; `/J` documentado) |
409
+ | `preRegistrations.listPlans()` | `POST /api/precadastro/plano/list` · form (sem barra final) | leitura | `PreRegistrationPlanId`: catálogo próprio, não é o de `plans.list` | observado |
410
+ | `preRegistrations.listDueDays()` | `POST /api/precadastro/vencimento/list` · json | leitura | `id` é o `vencimento_id` de `create`; `dia` é o dia do mês | observado (corpo mascarado, 1.x) |
411
+ | `preRegistrations.listSellers()` | `POST /api/precadastro/vendedor/list` · json | leitura | `id` é o `vendedor_id`; nome, CPF e e-mail são dados pessoais | observado (corpo mascarado, 1.x) |
412
+
413
+ ### `contracts` e `services`
414
+
415
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
416
+ | ------------------------------- | ------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
417
+ | `contracts.list(input?)` | `POST /api/ura/listacontrato/` · json | leitura | Sem paginação; array simples; sem filtro pode vir a base inteira do provedor; `plano` é `PlanId` | observado (corpo mascarado, 1.x) |
418
+ | `contracts.updateStatus(input)` | `POST /api/ura/contrato/status/edit/` · json | escrita | Ativar inicia a cobrança. O SGP decide as transições. Recusa `STATUS_NOT_CHANGED` (`effect: "none"`). `motivo` vem de `contracts.listStatusReasons` | parcial (200 observado; a recusa é reconstruída) |
419
+ | `contracts.update(input)` | `POST /api/ura/contrato/edit/` · json | escrita | Só `forma_cobranca` (`BILLING_METHOD`); devolve `resposta.alterados` como o SGP mandou. Sem recusa conhecida | observado |
420
+ | `contracts.listStatusReasons()` | `POST /api/ura/contrato/status/motivos/` · json | leitura | Cada provedor tem a sua lista: leia antes de passar `motivo` | reconstruído |
421
+ | `services.changePlan(input)` | `POST /api/suporte/service/update/{servico_id}/` · form | escrita | `action` fixa `change_plano`; `servico_tipo` obrigatório (só `1` exercitado); evento de cobrança. Sucesso traz só `msg` | observado |
422
+ | `services.addTv(input)` | `POST /api/servico/tv` · form | escrita | HTTP 201 com `servico_id`; só contratos ativos: recusa `CONTRACT_NOT_ACTIVE` (`effect: "none"`); `planobase_id` é `PlanId` | parcial (201 observado; a recusa é reconstruída) |
423
+ | `services.removeTv(input)` | `DELETE /api/servico/tv/{servico_id}` · form | escrita | O contrato vai num corpo multipart no DELETE, como o SGP espera. Sem recusa conhecida | observado |
424
+
425
+ ### `plans`, `invoices`, `workOrders`, `occurrences`
426
+
427
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
428
+ | --------------------------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
429
+ | `plans.list()` | `GET /api/ura/consultaplano/` · query | leitura | Mantém o formato do SGP (`planos` e totais); `planos[].id` é `PlanId` | observado |
430
+ | `invoices.list(input?)` | `POST /api/ura/titulos/` · json | leitura | Uma página; `paginacao.total` ausente é `INVALID_RESPONSE`; máximo de 250 por página é do fornecedor. Traz nome, documento, links de pagamento e PIX | parcial (página vazia observada; página populada reconstruída) |
431
+ | `workOrders.list(input?)` | `POST /api/ura/ordemservico/list/` · json | leitura | Uma página; `ocorrencia` é texto e continua texto | observado |
432
+ | `workOrders.listReasons()` | `GET /api/os/ocorrencia/motivo/list/` · query | leitura | `id` e `codigo` são distintos; `occurrences.create` usa o `codigo` em `motivoos` | observado (corpo mascarado, 1.x) |
433
+ | `occurrences.create(input)` | `POST /api/ura/chamado/` · json | escrita | "Chamado" do SGP. Devolve `id` da **ocorrência** e `protocolo`, nunca o id da OS (leia com `workOrders.list`). Recusa com HTTP 200 e `status: 0`: `REJECTED`, `effect: "unknown"` | parcial (sucesso: corpo mascarado, 1.x; a recusa é reconstruída) |
434
+ | `occurrences.listTypes()` | `GET /api/os/ocorrencia/tipo/list/` · query | leitura | `id` e `codigo`; `occurrences.create` usa o `codigo` em `ocorrenciatipo` | observado (corpo mascarado, 1.x) |
435
+ | `occurrences.listMethods()` | `GET /api/ura/ocorrencia/metodo/list/` · query | leitura | | observado (corpo mascarado, 1.x) |
436
+
437
+ ### `documents`, `coverage`, `catalogs`, `experimental`
438
+
439
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
440
+ | -------------------------------------- | ------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
441
+ | `documents.add(input)` | `PUT /api/suporte/cliente/{cliente_id}/documento/add/` · form-files | escrita | Sucesso é 2xx com corpo **vazio** (`data: null`, nada identifica o documento); qualquer outro corpo 2xx é `INVALID_RESPONSE` com `effect: "unknown"` | reconstruído |
442
+ | `documents.list(input)` | `GET /api/suporte/cliente/{cliente_id}/documento/list/` · query | leitura | `cliente_id` é um `CustomerId` positivo; array vazio = sem documentos | observado (corpo mascarado, 1.x) |
443
+ | `coverage.check(input)` | `GET /api/ura/viabilidadeinstalacao` · query (sem barra final) | leitura | `gateway`, `raio` (metros) e `coordenada` (`"lat,lng"`) obrigatórios; o SDK não geocodifica; nada é convertido (o Geogrid manda números como texto) | observado (corpo mascarado, 1.x; SGPMaps e Geogrid) |
444
+ | `catalogs.listPops()` | `POST /api/ura/pops/` · json | leitura | | observado (corpo mascarado, 1.x) |
445
+ | `catalogs.listNas()` | `POST /api/ura/nas/list/` · json | leitura | `endereco_ip` é segredo | observado (corpo mascarado, 1.x) |
446
+ | `catalogs.listBillingProfiles()` | `GET /api/ura/portador/` · query | leitura | "Portadores" do SGP; os ids alimentam o filtro `portador` de `invoices.list` | observado (corpo mascarado, 1.x) |
447
+ | `experimental.annotations.list(input)` | `GET /api/ura/cliente/anotacao/list/` · query | leitura | **Experimental.** Seleção local, corpo retido. Veja [leituras com escopo](#leituras-com-escopo) | reconstruído (corpo sintético; tipos de campo observados) |
448
+
449
+ Todas as leituras acima aceitam `maxRetries`. `request(input, options?)` está descrita
450
+ [acima](#request-chamada-bruta).
451
+
452
+ ## Migração do 1.x
453
+
454
+ A 2.0 é uma quebra de contrato (resultado, códigos, nomes e comportamento), sem aliases. O mapa de
455
+ cada método do 1.x e as mudanças de comportamento estão em [MIGRATION.md](MIGRATION.md); o histórico
456
+ de mudanças, em [CHANGELOG.md](CHANGELOG.md).
457
+
458
+ ## Desenvolvimento
459
+
460
+ Veja `CONTRIBUTING.md` no repositório. Os testes usam sempre um `fetch` injetado: nenhum teste
461
+ toca o SGP, e a CI não usa credenciais.