sgp-sdk 1.2.0 → 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 (88) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/COMPATIBILITY.md +151 -0
  3. package/MIGRATION.md +283 -0
  4. package/README.md +422 -265
  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 +661 -165
  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 -49
  19. package/dist/index.js +2947 -1292
  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-annotations.d.ts +0 -32
  76. package/dist/endpoints/ura/list-clients.d.ts +0 -58
  77. package/dist/endpoints/ura/list-contracts.d.ts +0 -45
  78. package/dist/endpoints/ura/list-invoices.d.ts +0 -107
  79. package/dist/endpoints/ura/list-nas.d.ts +0 -29
  80. package/dist/endpoints/ura/list-occurrence-methods.d.ts +0 -19
  81. package/dist/endpoints/ura/list-payment-carriers.d.ts +0 -20
  82. package/dist/endpoints/ura/list-pops.d.ts +0 -19
  83. package/dist/endpoints/ura/list-service-orders.d.ts +0 -64
  84. package/dist/endpoints/ura/open-ticket.d.ts +0 -81
  85. package/dist/errors.d.ts +0 -59
  86. package/dist/schema/parse.d.ts +0 -19
  87. package/dist/schema/paths.d.ts +0 -38
  88. package/dist/transport.d.ts +0 -65
package/README.md CHANGED
@@ -1,304 +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.listAnnotations(params)` **experimental** | `GET /api/ura/cliente/anotacao/list/` |
65
- | `ura.listServiceOrders(params?)` | `POST /api/ura/ordemservico/list/` |
66
- | `ura.listInvoices(params?)` | `POST /api/ura/titulos/` |
67
- | `ura.listPops()` | `POST /api/ura/pops/` |
68
- | `ura.listNas()` | `POST /api/ura/nas/list/` |
69
- | `ura.listPaymentCarriers()` | `GET /api/ura/portador/` |
70
- | `ura.checkCoverage(params)` | `GET /api/ura/viabilidadeinstalacao` |
71
- | `ura.listPlans()` | `GET /api/ura/consultaplano/` |
72
- | `ura.listOccurrenceMethods()` | `GET /api/ura/ocorrencia/metodo/list/` |
73
- | `ura.openTicket(params)` | `POST /api/ura/chamado/` |
74
- | `os.listOccurrenceTypes()` | `GET /api/os/ocorrencia/tipo/list/` |
75
- | `os.listOccurrenceReasons()` | `GET /api/os/ocorrencia/motivo/list/` |
76
- | `precadastro.listDueDays()` | `POST /api/precadastro/vencimento/list` |
77
- | `precadastro.listSellers()` | `POST /api/precadastro/vendedor/list` |
78
- | `crm.createIndividualCustomer(params)` | `POST /api/crm/cliente/F` |
79
- | `crm.createCompanyCustomer(params)` | `POST /api/crm/cliente/J` |
80
- | `crm.createContract(clienteId, params)` | `POST /api/crm/cliente/{cliente_id}/contratos/` |
81
- | `crm.updateCustomerStatus(clienteId, params)` | `POST /api/crm/cliente/{cliente_id}/status/` |
82
- | `suporte.addCustomerDocument(clienteId, file, params?)` | `PUT /api/suporte/cliente/{cliente_id}/documento/add/` |
83
- | `suporte.listCustomerDocuments(clienteId)` | `GET /api/suporte/cliente/{cliente_id}/documento/list/` |
84
-
85
- Every route must be allowed on the consumer's token in SGP (System > Tools > Admin Panel >
86
- Tokens). A route outside the whitelist returns `ROUTE_NOT_ALLOWED`. Paging is the API's own:
87
- `offset` and `limit`; the SDK never pages for you.
88
-
89
- `ura.checkCoverage({ gateway, raio, coordenada })` checks installation viability: the answer is keyed
90
- by the gateway's name (data-defined, e.g. `SGPMaps`) and holds the CTOs within `raio` meters or the
91
- message `"Localização inviável"`. The coordinate (`"lat,lng"`) comes from the consumer; the SDK does
92
- not geocode. `lat`, `lng` and `distancia` are always numbers (Geogrid sends them as numeric strings
93
- and the SDK converts them). A malformed gateway answer, such as a CTO with a non-numeric `lat`, is
94
- `SCHEMA_MISMATCH`.
95
-
96
- ### Experimental annotation listing
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`.
97
77
 
98
- ```ts
99
- const notes = await sgp.ura.listAnnotations({ cliente_id: 10, contrato_id: 100 });
100
- ```
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"`.
101
81
 
102
- `ura.listAnnotations` requires at least one of `cliente_id` / `contrato_id`, each a positive safe-integer
103
- number. Missing filters, nulls, numeric strings and invalid numbers return `VALIDATION` with status 0
104
- without fetching. Customer-only includes notes with a null contract; contract-only selects that
105
- contract; both filters mean **AND**, never OR or fallback. No match is a successful empty array.
106
-
107
- SGP currently ignores these upstream filters (vendor defect reported, not claimed fixed). The SDK
108
- still sends canonical filters in one authenticated GET and selects raw identities **before** parsing
109
- selected notes. A definite mismatch excludes a row even if unrelated content is malformed. If a
110
- needed identity is missing or malformed and no definite mismatch excludes it, the call fails with
111
- `SCHEMA_MISMATCH`, not partial data. Source identity strings are not coerced. Selected core fields
112
- are strict; optional-field drift produces ordinary, selected-only schema warnings. Source order and
113
- duplicates are preserved; unknown fields are stripped.
114
-
115
- Returns matching notes present in one SGP response; source completeness is not guaranteed while this method is experimental.
116
- No documented pagination contract proves completeness; there is no automatic paging, retry, polling,
117
- cache, completeness flag or operational warning. The method, snake_case params and array-shaped
118
- `SgpResult<ListAnnotationsResponse>` remain usable after the vendor fix; local selection and
119
- validation remain as defense. Removing the experimental label requires evidence of corrected behavior.
120
-
121
- The nine declared fields are required numeric `id` / `cliente`, required number-or-null
122
- `clientecontrato`, required string `anotacao`, optional/nullable numeric `usuario` / `tipo` /
123
- `alerta_modo`, and optional/nullable string `data_cadastro` / `data_alteracao`. Text and dates are
124
- preserved verbatim, without HTML rendering, trimming or date parsing. `annotationPersonalKeys`
125
- classifies note text and staff identity (`anotacao`, `usuario`); `annotationSecretKeys` is empty.
126
- Use `omitSensitive(note, annotationPersonalKeys)` before logging or forwarding: free text may contain
127
- user-entered sensitive content even though no credential field is declared.
128
-
129
- **Trusted server only:** the multi-customer upstream body enters SDK memory, but the SDK never
130
- returns, logs or caches the bulk array. Injected fetch, tracing and other instrumentation see the
131
- body **before** SDK selection and MUST NOT capture it. Local selection is not an authorization
132
- boundary: the application must authorize requested IDs and keep credentials and execution trusted.
133
- SDK-generated diagnostics contain paths/types, not source values. Existing upstream error details
134
- are unchanged and may echo request values; treat them as sensitive too. This special filtered read
135
- is deliberately excluded from the generic filterless probe registry.
136
-
137
- ## Result and error contract
138
-
139
- Every method returns `SgpResult<T>`:
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>`:
140
88
 
141
89
  ```ts
142
90
  type SgpResult<T> =
143
- | { success: true; data: T; warnings?: SchemaWarning[] }
144
- | { 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
+ };
145
106
 
146
107
  interface SgpError {
147
- status: number; // HTTP status, 0 when no response arrived
148
- code: SgpErrorCode;
149
- message: string; // written by the SDK, in English
150
- detail?: string; // SGP's own error text (`detail`, `msg`, `errors` or `error`), copied verbatim
151
- 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
152
115
  }
116
+ ```
153
117
 
154
- interface SchemaWarning {
155
- path: string; // e.g. "contratos[0].cobIsento"
156
- expected: string; // type names only, never values
157
- received: string;
158
- }
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
+ );
159
222
  ```
160
223
 
161
- | Code | When |
162
- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
163
- | `UNAUTHORIZED` | 401/403 with `detail` (or `msg`) `"Credenciais de autenticação incorretas."` (wrong token or app) |
164
- | `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 |
165
- | `NOT_FOUND` | 404 |
166
- | `METHOD_NOT_ALLOWED` | 405 |
167
- | `VALIDATION` | any other 4xx: SGP refused the input |
168
- | `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) |
169
- | `UPSTREAM` | 5xx |
170
- | `TIMEOUT` | the request exceeded `timeoutMs` |
171
- | `NETWORK` | `fetch` failed before a response (DNS, connection, TLS) |
172
- | `UNKNOWN` | 401/403 with any other `detail` or `msg`, or a status the rows above do not cover |
173
-
174
- SGP answers errors in four formats: `{"detail": "..."}`, `{"msg": "..."}`, `{"errors": ...}` (an
175
- array of messages or an object of field messages, on CRM routes) and `{"error": "..."}` (e.g. a 404
176
- `Cadastro não encontrado` on CRM routes). `error.detail` holds, in this order: a string `detail`
177
- (even empty), a non-empty `msg`, the text of `errors`, a non-empty `error`.
178
-
179
- Guarantees:
180
-
181
- - The SDK never puts the token, the app, a URL with query string, a raw response body or the
182
- original message of a `fetch` exception in anything it returns (`data`, `warnings`, `error`).
183
- - `error.detail` is SGP's text copied verbatim: it may echo values from your request (for example
184
- a contract ID or a CPF in a `msg`). The SDK adds nothing to it, so treat it like the request
185
- itself before logging or showing it.
186
- - 401/403 are classified by that exact `detail` string, not by status (SGP uses either status for
187
- the same message). If SGP rewords those messages the code falls to `UNKNOWN`.
188
- - No retry, no cache, no logging, no automatic paging.
189
- - Redirects are never followed, so the token and payload are not re-sent to another host: a 3xx
190
- answer is an `UNKNOWN` error carrying its status (`outcome: "unknown"` on a write).
191
- - Unknown keys are dropped at every level (`strip`): everything that reaches you was declared and
192
- classified. Required key fields that fail validation are `SCHEMA_MISMATCH`; any other field with
193
- an unexpected type becomes `undefined` and is reported in `warnings`.
194
-
195
- ## Writes
196
-
197
- Write methods (`crm.*`, `ura.openTicket` and `suporte.addCustomerDocument`) return
198
- `SgpWriteResult<T>`: the same as `SgpResult<T>`, but `error` also carries an `outcome` that says
199
- whether SGP applied the write.
200
-
201
- - `"not_applied"`: SGP refused the request (4xx) or the SDK rejected it before sending it. SGP did
202
- not apply the write; fix the input and call again.
203
- - `"unknown"`: no answer (`TIMEOUT`, `NETWORK`), a 5xx, a 3xx (`UNKNOWN`: redirects are never
204
- followed), or a 2xx whose body does not confirm the write (`VALIDATION` with the real HTTP
205
- status). The write may have happened: check the state in SGP before repeating it.
206
- - A 4xx whose body has `errors` as an array (e.g. a 400 with `{"errors": ["Ocorreu um erro: ..."]}`)
207
- is SGP's generic exception wrapper: it can fail after applying part of the write, so it is
208
- `"unknown"`, even when `detail` or `msg` come with it. The `code` stays `VALIDATION` and the
209
- `message` reads "SGP reported an error while processing the write; it may have been applied."
210
- 401/403/404/405/422 stay `"not_applied"` whatever the body, and so does a 4xx whose `errors` is an
211
- object of field messages or that carries only `detail` or `msg`.
212
-
213
- After `outcome: "unknown"`, read the state back before repeating the write:
214
-
215
- | Write | Read that verifies it |
216
- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
217
- | `crm.createIndividualCustomer`, `crm.createCompanyCustomer` | `ura.listClients({ cpfcnpj })` |
218
- | `crm.createContract` | `ura.lookupCustomer` or `ura.listContracts` |
219
- | `ura.openTicket` | `ura.listServiceOrders({ contrato })`; a ticket with `desativa_os: 1` opens no service order, so there is nothing to look up |
220
- | `suporte.addCustomerDocument` | `suporte.listCustomerDocuments` |
221
- | `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) |
222
-
223
- A write only succeeds on a 2xx whose body carries the key fields (for example `cliente_id`). The one
224
- exception is `suporte.addCustomerDocument`: SGP answers an empty 2xx, which succeeds with
225
- `data: null`. Success there does not prove the file was stored; verify it with
226
- `suporte.listCustomerDocuments`. The SDK never retries a write.
227
-
228
- Sales flow: create the customer (`crm.createIndividualCustomer` or `crm.createCompanyCustomer`),
229
- create the contract on it (`crm.createContract(clienteId, params)`), then call
230
- `crm.updateCustomerStatus(clienteId, { status_id: crmStatus.contratoRealizadoNoSgp })` (status 6).
231
- Without that last step SGP's screen keeps the customer as a CRM customer and blocks contract status
232
- changes.
233
-
234
- `crm.createContract` sends the installation address nested (`endereco_instalacao`) and also as the
235
- loose fields (`logradouro`, `numero`, ...), and the billing address only nested
236
- (`endereco_cobranca`). The loose fields carry the installation address; a billing address that
237
- differs from the installation one has not been verified live, so check it on SGP before sending
238
- divergent addresses.
239
-
240
- Scheduling an installation goes through `ura.openTicket` with `data_hora_agendamento`
241
- (`"AAAA-MM-DD HH:MM"`); `os_instalacao` in `crm.createContract` opens the order without a date.
242
-
243
- The token whitelist accepts regular expressions and the trailing slash counts: release each path
244
- exactly as in the table above, so `/api/crm/cliente/F` has no final `/` and
245
- `/api/crm/cliente/{cliente_id}/status/` has one (the `{cliente_id}` routes need a pattern that
246
- matches any ID).
247
-
248
- ## Sensitive fields
249
-
250
- SGP responses carry credentials and personal data. The SDK transcribes them (the consumer decides
251
- what to show) and marks them: each endpoint exports `as const` tuples with the keys of one response
252
- item.
253
-
254
- - `secret`: Central password, PPPoE login and password, Wi-Fi passwords and SSIDs, MACs, links with
255
- an embedded token.
256
- - `personal`: CPF/CNPJ, phone, e-mail, address, birth date, customer name, free text typed by
257
- operators.
258
-
259
- Tuple names follow the item: `clientSecretKeys` / `clientPersonalKeys`,
260
- `customerContractSecretKeys` / `customerContractPersonalKeys`, and so on (see the package exports).
261
- `omitSensitive(item, keys)` returns a shallow copy without those keys and removes them from the
262
- 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.**
263
319
 
264
320
  ```ts
265
- const safe = omitSensitive(client, clientPersonalKeys);
266
- safe.nome; // type error: `nome` was omitted
321
+ const raw = await sgp.request({ method: "POST", path: "/api/ura/clientes/", body: { limit: 1 } });
267
322
  ```
268
323
 
269
- The SDK only marks and offers the helper; deciding when to apply it is the consumer's job. Strip
270
- before logging, caching or forwarding an item to a model.
271
-
272
- ## Known limitations
273
-
274
- - **Placeholder arrays.** These fields are declared as arrays of empty objects
275
- (`z.array(z.object({}))`) because no sample response had content for them. Every element arrives
276
- as `{}`, with no warning, and what SGP sends inside is dropped:
277
- - `listClients`: `clientes[].contratos` and `clientes[].titulos` (use `lookupCustomer` and the
278
- contract methods for those details);
279
- - `lookupCustomer`: `contratos[].telefones_cargos`, `contratos[].outros` and
280
- `contratos[].anotacoes`;
281
- - `listServiceOrders`: `ordens_servicos[].tecnicos_auxiliares` and
282
- `ordens_servicos[].classificacoes`;
283
- - `listPlans`: `planos[].combo`;
284
- - `os.listOccurrenceTypes` and `os.listOccurrenceReasons`: `classificacoes` and `setores` of
285
- each item;
286
- - `listInvoices`: `acordados` and `cobrados` of `titulos[].acordoPagamento`.
287
- - **Guessed types.** SGP only sent `null` for four `lookupCustomer` fields, so their types are
288
- inferred: `contratos[].contrato_unificado_principal` (number),
289
- `contratos[].contrato_unificado_filhos` (array of numbers), `contratos[].endereco_pontoreferencia`
290
- (string) and `contratos[].tags[].categoria_id` (number). A wrong guess does not fail the call: the
291
- field comes out as `undefined` and is reported in `warnings`.
292
- - **A bad element drops the whole array.** When one element of an array field has the wrong type,
293
- the whole field becomes `undefined`, and the warning names only the failing element. For example,
294
- a non-string entry in `clientes[].contatos.telefones` drops the whole `telefones` array and the
295
- warning path is `clientes[0].contatos.telefones[1]`.
296
- - `listContracts` does not transcribe the address fields returned by `exibir_endereco`.
297
-
298
- ## Pending
299
-
300
- What is left:
301
-
302
- - **E-signature of the adhesion term.** Not available in the SGP API.
303
- - **Boleto delivery methods and invoice (nota fiscal) fields** of the contract. Not available in the
304
- 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.