sgp-sdk 1.2.0 → 2.0.0-beta.2

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 (100) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/COMPATIBILITY.md +231 -0
  3. package/MIGRATION.md +344 -0
  4. package/README.md +464 -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 +938 -165
  10. package/dist/config.d.ts +115 -0
  11. package/dist/constants.d.ts +85 -0
  12. package/dist/document.d.ts +11 -0
  13. package/dist/effect.d.ts +22 -0
  14. package/dist/executor.d.ts +9 -0
  15. package/dist/exposure.d.ts +21 -0
  16. package/dist/guards.d.ts +2 -0
  17. package/dist/hooks.d.ts +16 -0
  18. package/dist/ids.d.ts +19 -0
  19. package/dist/index.d.ts +55 -49
  20. package/dist/index.js +3750 -1293
  21. package/dist/normalize.d.ts +9 -0
  22. package/dist/operation-sensitivity.d.ts +729 -0
  23. package/dist/operation.d.ts +145 -0
  24. package/dist/operations/catalogs-list-billing-profiles.d.ts +16 -0
  25. package/dist/operations/catalogs-list-nas.d.ts +25 -0
  26. package/dist/operations/catalogs-list-pops.d.ts +14 -0
  27. package/dist/operations/contracts-check-connection.d.ts +56 -0
  28. package/dist/operations/contracts-list-services.d.ts +103 -0
  29. package/dist/operations/contracts-list-status-reasons.d.ts +15 -0
  30. package/dist/operations/contracts-list.d.ts +52 -0
  31. package/dist/operations/contracts-update-status.d.ts +41 -0
  32. package/dist/operations/contracts-update.d.ts +39 -0
  33. package/dist/operations/coverage-check-address.d.ts +46 -0
  34. package/dist/operations/coverage-check.d.ts +43 -0
  35. package/dist/operations/coverage-list-gateways.d.ts +16 -0
  36. package/dist/operations/crm-contracts-create-by-cpf-cnpj.d.ts +97 -0
  37. package/dist/operations/crm-contracts-create.d.ts +221 -0
  38. package/dist/operations/crm-contracts-list.d.ts +50 -0
  39. package/dist/operations/crm-customers-create.d.ts +165 -0
  40. package/dist/operations/crm-customers-update-status.d.ts +41 -0
  41. package/dist/operations/customers-list.d.ts +104 -0
  42. package/dist/{endpoints/ura/lookup-customer.d.ts → operations/customers-search.d.ts} +56 -46
  43. package/dist/operations/documents-add.d.ts +29 -0
  44. package/dist/operations/documents-list.d.ts +31 -0
  45. package/dist/operations/experimental-annotations-list.d.ts +67 -0
  46. package/dist/operations/invoices-list.d.ts +148 -0
  47. package/dist/operations/maintenance-list.d.ts +35 -0
  48. package/dist/operations/occurrences-create.d.ts +125 -0
  49. package/dist/operations/occurrences-list-methods.d.ts +14 -0
  50. package/dist/operations/occurrences-list-types.d.ts +20 -0
  51. package/dist/operations/occurrences-list.d.ts +127 -0
  52. package/dist/{endpoints/ura/list-plans.d.ts → operations/plans-list.d.ts} +33 -23
  53. package/dist/operations/pre-registrations-create.d.ts +276 -0
  54. package/dist/operations/pre-registrations-list-due-days.d.ts +15 -0
  55. package/dist/operations/pre-registrations-list-plans.d.ts +24 -0
  56. package/dist/operations/pre-registrations-list-sellers.d.ts +16 -0
  57. package/dist/operations/services-add-generic.d.ts +52 -0
  58. package/dist/operations/services-add-tv.d.ts +47 -0
  59. package/dist/operations/services-change-plan.d.ts +40 -0
  60. package/dist/operations/services-remove-generic.d.ts +28 -0
  61. package/dist/operations/services-remove-tv.d.ts +29 -0
  62. package/dist/operations/work-orders-get.d.ts +99 -0
  63. package/dist/operations/work-orders-list-comments.d.ts +34 -0
  64. package/dist/operations/work-orders-list-reasons.d.ts +20 -0
  65. package/dist/operations/work-orders-list.d.ts +81 -0
  66. package/dist/path.d.ts +13 -0
  67. package/dist/raw-request.d.ts +26 -0
  68. package/dist/request.d.ts +28 -0
  69. package/dist/result.d.ts +113 -41
  70. package/dist/retry.d.ts +34 -0
  71. package/dist/sensitivity.d.ts +89 -4
  72. package/dist/tolerant.d.ts +21 -0
  73. package/dist/validation.d.ts +3 -0
  74. package/package.json +6 -3
  75. package/dist/endpoint.d.ts +0 -66
  76. package/dist/endpoints/crm/address.d.ts +0 -20
  77. package/dist/endpoints/crm/create-contract.d.ts +0 -109
  78. package/dist/endpoints/crm/create-customer.d.ts +0 -65
  79. package/dist/endpoints/crm/update-customer-status.d.ts +0 -43
  80. package/dist/endpoints/os/list-occurrence-reasons.d.ts +0 -24
  81. package/dist/endpoints/os/list-occurrence-types.d.ts +0 -25
  82. package/dist/endpoints/precadastro/list-due-days.d.ts +0 -19
  83. package/dist/endpoints/precadastro/list-sellers.d.ts +0 -21
  84. package/dist/endpoints/suporte/add-customer-document.d.ts +0 -24
  85. package/dist/endpoints/suporte/list-customer-documents.d.ts +0 -25
  86. package/dist/endpoints/ura/check-coverage.d.ts +0 -46
  87. package/dist/endpoints/ura/list-annotations.d.ts +0 -32
  88. package/dist/endpoints/ura/list-clients.d.ts +0 -58
  89. package/dist/endpoints/ura/list-contracts.d.ts +0 -45
  90. package/dist/endpoints/ura/list-invoices.d.ts +0 -107
  91. package/dist/endpoints/ura/list-nas.d.ts +0 -29
  92. package/dist/endpoints/ura/list-occurrence-methods.d.ts +0 -19
  93. package/dist/endpoints/ura/list-payment-carriers.d.ts +0 -20
  94. package/dist/endpoints/ura/list-pops.d.ts +0 -19
  95. package/dist/endpoints/ura/list-service-orders.d.ts +0 -64
  96. package/dist/endpoints/ura/open-ticket.d.ts +0 -81
  97. package/dist/errors.d.ts +0 -59
  98. package/dist/schema/parse.d.ts +0 -19
  99. package/dist/schema/paths.d.ts +0 -38
  100. package/dist/transport.d.ts +0 -65
package/README.md CHANGED
@@ -1,304 +1,503 @@
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 (beta).** Esta documentação descreve a 2.0, publicada em beta na tag `next` do npm
7
+ > (`npm install sgp-sdk@next`); a `latest` continua na 1.x até a 2.0.0 estável. A série 1.x está
8
+ > congelada: não recebe mais mudanças. Para migrar, leia [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
+ | `DOCUMENT_ALREADY_REGISTERED` | `crm.customers.create` | 400 `errors.cpfcnpj`, CPF já no CRM |
148
+ | `DOCUMENT_INVALID` | `crm.customers.create` | 400 `errors.cpfcnpj` "CPF inválido." |
149
+ | `DOCUMENT_INVALID` | `preRegistrations.create` | 400 `message` "CPF inválido" |
150
+ | `LOGIN_ALREADY_REGISTERED` | `preRegistrations.create` | 400 `message` "Login ... já existe" |
151
+ | `INVALID_SCHEDULE` | `occurrences.create` | 200 `status: 0`, agendamento inválido |
152
+ | `STATUS_NOT_CHANGED` | `contracts.updateStatus` | 400 "Status de Contrato não alterado." |
153
+ | `CUSTOMER_NOT_IN_CRM` | `crm.contracts.create`, `.createByCpfCnpj` | 400 "Não encontrado cliente CRM ..." |
154
+ | `CONTRACT_NOT_ACTIVE` | `services.addTv` | 400, contrato não ativo |
155
+ | `PLAN_NOT_GENERIC` | `services.addGeneric` | 400, plano não é do tipo genérico |
156
+
157
+ Sem `reason`, a falha continua útil: `code`, `effect`, `sgp` e `response.body` dizem tudo o que o
158
+ SGP disse.
159
+
160
+ ### `effect`: o que se sabe sobre uma escrita
161
+
162
+ Só escritas têm `error.effect`; leituras não têm. O código sozinho não determina o efeito (e "leitura"
163
+ é propriedade da operação conhecida, nunca do verbo HTTP).
164
+
165
+ - **`"none"`**: o SDK consegue afirmar que a mudança pedida **não** foi aplicada. Acontece em recusa
166
+ local (`INVALID_INPUT`), cancelamento antes do envio, recusa conhecida da operação (tabela de
167
+ `reason` acima) e 4xx do SGP com formato de erro de campo ou de negócio (`errors` como objeto,
168
+ `error` ou `detail` como texto). Corrigir a entrada e chamar de novo é seguro.
169
+ - **`"unknown"`**: o SGP pode ter aplicado. Vale para timeout, falha de rede, cancelamento depois do
170
+ envio, 5xx, 3xx, 2xx fora do contrato, 4xx com corpo ilegível ou de formato não reconhecido, a
171
+ recusa com HTTP 200 de `occurrences.create` (`status: 0`, salvo a do formato do agendamento, de
172
+ `reason` `INVALID_SCHEDULE`, que é `"none"`) e o "invólucro genérico" do SGP
173
+ (`errors` como **array**, "Ocorreu um erro: ..."), salvo quando a operação o declara como
174
+ recusa conhecida.
175
+
176
+ Uma observação de "nada foi aplicado" é registrada como observação com seu método de verificação
177
+ (releitura), não como garantia.
178
+
179
+ **Diante de `"unknown"`, não reenvie às cegas.** Releia o estado e só então decida:
180
+
181
+ | Escrita que ficou em `unknown` | Leitura que verifica |
182
+ | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
183
+ | `crm.customers.create` | `customers.search` ou `customers.list` pelo documento |
184
+ | `crm.contracts.create`, `crm.contracts.createByCpfCnpj` | `crm.contracts.list` (por documento) |
185
+ | `crm.customers.updateStatus` | ler o cliente antes de reenviar |
186
+ | `contracts.updateStatus`, `contracts.update` | `contracts.list({ contrato })` |
187
+ | `preRegistrations.create` | `customers.list({ cpfcnpj })` (só quando a resposta de sucesso não traz `new_cliente_id`) |
188
+ | `contracts.checkConnection` | `occurrences.list({ contrato })` (uma ocorrência pode ter sido aberta em manutenção) |
189
+ | `occurrences.create` | `occurrences.list({ contrato })` (traz a OS em `ordens_servicos[]`) ou `workOrders.list({ contrato })`; com `desativa_os: 1` não há OS para consultar |
190
+ | `services.changePlan`, `services.addTv`, `services.removeTv`, `services.addGeneric`, `services.removeGeneric` | `customers.list` (serviços do contrato) |
191
+ | `documents.add` | `documents.list` |
192
+
193
+ ### `sgp`, `response`, `warnings`, `attempts`
194
+
195
+ - **`error.sgp`** projeta os campos de erro do SGP (`detail`, `msg`, `error`, `errors`) com os
196
+ valores como vieram. O corpo inteiro continua em `response.body`.
197
+ - **`response`** é a resposta HTTP bruta: `{ status, headers, body }`, com nomes de header em
198
+ minúsculas. `body` é um valor discriminado: `{ kind: "json", value }` (o valor pode ser `null`),
199
+ `{ kind: "text", text }`, `{ kind: "empty" }`, `{ kind: "unavailable", reason }` (o corpo começou e
200
+ não foi lido até o fim) ou `{ kind: "withheld" }` (veja [leituras com escopo](#leituras-com-escopo)).
201
+ É o corpo já decodificado, não os bytes. `response` é `null` quando nenhuma resposta HTTP existiu.
202
+ - **`warnings`** aparece quando um campo **secundário** veio com tipo inesperado: o campo sai de
203
+ `data`, e `{ path, dropped, expected, received }` registra o caminho e os nomes dos tipos (nunca
204
+ valores). O valor original continua intacto em `response.body`. Os campos **estruturais** de cada
205
+ operação (ids e o que a navegação precisa) são validados com rigor: se falharem, o resultado é
206
+ `INVALID_RESPONSE`.
207
+ - **`attempts`** conta as requisições que o SDK iniciou: 0 quando recusou localmente, 1 na primeira.
208
+ - **Fidelidade.** Campos conhecidos são tipados; os desconhecidos chegam em `data` em todos os níveis
209
+ (raiz, aninhados, arrays), sem serem descartados. Um item que o SDK não consegue interpretar faz a
210
+ resposta falhar com `INVALID_RESPONSE`; nunca vira `{}` nem `[]`.
211
+ - **Paginação.** `customers.list`, `workOrders.list`, `occurrences.list` e `invoices.list` devolvem **uma página**, com
212
+ a `paginacao` do SGP como veio (`total` nunca é calculado pelo SDK). Para a próxima página, passe
213
+ `offset`. Listas sem paginação mantêm o formato do SGP (array ou objeto), sem envelope.
214
+ - **Credenciais nos headers.** Os headers capturados nunca carregam o token nem o app: o valor de
215
+ qualquer parâmetro `token`/`app` é trocado por `<redacted>`, e o valor exato das credenciais (puro
216
+ ou codificado) também é trocado quando tem 8 caracteres ou mais. **Corpos não são alterados.**
217
+
218
+ ## Tempo, cancelamento e retry
219
+
220
+ ```ts
221
+ const controller = new AbortController();
222
+ const result = await sgp.customers.list(
223
+ { limit: 20 },
224
+ {
225
+ timeoutMs: 5_000, // prazo desta chamada (padrão do cliente: 20 000 ms)
226
+ abortSignal: controller.signal,
227
+ maxRetries: 2, // só leituras
228
+ },
229
+ );
159
230
  ```
160
231
 
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:
232
+ - **Um único orçamento de tempo por chamada lógica**: tentativas, esperas e leitura do corpo. Nunca
233
+ reinicia por tentativa. `timeoutMs` por chamada substitui o padrão do cliente (é padrão, não teto).
234
+ Deve ser um número positivo, finito, até `MAX_TIMEOUT_MS`.
235
+ - **`TIMEOUT`** é o prazo do SDK; **`ABORTED`** é o seu `abortSignal`. Se o sinal já estiver
236
+ cancelado quando você chama, nada é enviado (`effect: "none"` numa escrita); cancelado depois do
237
+ envio, `effect: "unknown"`.
238
+ - **Retry é opt-in** (`maxRetries`, padrão 0 = nenhuma nova tentativa), vale **só para leituras**
239
+ e só para falhas transitórias: `RATE_LIMITED`, `NETWORK` e `UPSTREAM` com HTTP 502, 503 ou 504.
240
+ Há espera exponencial com jitter (base de 500 ms, teto de 10 s) e `Retry-After` (segundos ou data
241
+ HTTP) é respeitado como piso, nunca encurtado; se a espera não cabe no orçamento, a chamada
242
+ termina com a última falha. `TIMEOUT`, `ABORTED`, `INVALID_*`, `UNAUTHORIZED`, `FORBIDDEN`,
243
+ `NOT_FOUND` e `REJECTED` nunca são repetidos.
244
+ - **Escritas nunca são repetidas pelo SDK**, e `request()` também não, qualquer que seja
245
+ `maxRetries`. Conferir o `attempts` mostra quantas requisições partiram.
246
+
247
+ ## Hooks
248
+
249
+ ```ts
250
+ const sgp = createSgpClient({
251
+ baseUrl,
252
+ credentials,
253
+ hooks: {
254
+ onRequest: (m) => log.debug({ op: m.operation, attempt: m.attempt }),
255
+ onResponse: (m) => log.info({ op: m.operation, status: m.status, ms: m.durationMs }),
256
+ onError: (m) => log.warn({ op: m.operation, code: m.code }),
257
+ },
258
+ });
259
+ ```
260
+
261
+ Opcionais, **síncronos** e só para observabilidade. Recebem metadados: `callId` (liga as tentativas de
262
+ uma chamada), `attempt` (1, 2, ...), `operation`, `method`, `pathTemplate` (omitido quando
263
+ desconhecido, como em `request()`) e, conforme o hook, `status`, `code` e `durationMs`. Nunca
264
+ recebem credenciais, URLs nem corpos. `onResponse` roda por tentativa quando os headers chegam;
265
+ `onError`, por tentativa que falhou.
266
+
267
+ Um hook que lança (ou devolve uma Promise, que quebra o contrato síncrono) **nunca altera o
268
+ resultado**: uma escrita confirmada continua confirmada. O fato aparece em `diagnostics`
269
+ (`{ hook, occurrence }`), sem o valor lançado.
270
+
271
+ ## Dados sensíveis
272
+
273
+ Fidelidade significa que os dados do SGP chegam inteiros, e o SGP mistura credenciais (logins e senhas
274
+ de Central e PPPoE, Wi-Fi, MAC, IP) e dados pessoais de clientes nas respostas. **A sua aplicação
275
+ projeta o que sai do processo.**
276
+
277
+ - **`getSensitivity("customers.search")`** devolve, por operação, as chaves secretas e pessoais da
278
+ entrada (`input`) e da resposta (`output`). Serve para saber o que remover. Uma chave que não está
279
+ listada **não é segura**: o SGP acrescenta campos sem aviso.
280
+ - **`pick(origem, allowlist)`** copia só os caminhos nomeados (`"clientes[].contratos[].id"`;
281
+ `"[].id"` para array na raiz). Chave não nomeada nunca sai. Use para tudo o que vai a logs,
282
+ ferramentas, MCP ou agentes.
283
+ - **`toSafeLog(result)`** devolve a versão para log de um resultado: `ok`, `attempts`, `diagnostics`,
284
+ `warnings` (caminhos e nomes de tipo), status HTTP e, do erro, `code`, `message`, `operation`,
285
+ `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`
286
+ (que costuma repetir o que você enviou).
287
+
288
+ ```ts
289
+ logger.info(toSafeLog(result)); // nunca `logger.info(result)`
290
+ const forTheAgent = pick(result.data, ["contratos[].contratoId", "contratos[].clienteId"]);
291
+ ```
292
+
293
+ **Nunca registre o resultado cru.** `console.log(result)`, `structuredClone(result)`, um logger que
294
+ percorre o objeto ou uma cópia feita antes de `toSafeLog` enxergam tudo. E os corpos em `response.body`
295
+ **não são redigidos**: podem repetir credenciais ou dados pessoais que o SGP devolveu, e o texto de
296
+ erro do SGP pode ecoar o que você enviou. Os headers são redigidos; os corpos, não.
297
+
298
+ Veja `examples/sensitive-logging.ts` no repositório.
299
+
300
+ ## Leituras com escopo
301
+
302
+ Princípio: leitor tolerante na interpretação, **fronteira estrita** no isolamento. A tolerância à
303
+ evolução do contrato do SGP nunca alarga o que sai de uma operação.
304
+
305
+ `experimental.annotations.list` (anotações de um cliente e/ou contrato) é o caso: o SGP ignora os
306
+ filtros e responde com anotações de **outros** clientes. O SDK seleciona localmente, sobre a única
307
+ resposta recebida:
308
+
309
+ - `response.body` é `{ kind: "withheld" }` em qualquer status, `error.sgp` guarda só textos, e nenhum
310
+ hook recebe o corpo. Cada anotação selecionada traz `raw`, uma cópia profunda do registro como o SGP
311
+ mandou.
312
+ - Uma anotação cuja identidade (cliente ou contrato) não pode ser decidida, ou que embute identidade
313
+ de outro cliente em qualquer profundidade, faz a chamada inteira falhar com `INVALID_RESPONSE`
314
+ (`error.issues` aponta os caminhos, sem valores). Falha fechada, nunca pulo silencioso.
315
+ - `request()` recusa essa rota (`ROUTE_NOT_ALLOWED`), para a fronteira não ser contornada.
316
+ - Não é uma fronteira de autorização: autorize os ids que você passa e rode isso só em processo de
317
+ servidor confiável. Uma camada de `fetch` ou tracing que você injete enxerga o corpo inteiro **antes**
318
+ da seleção e não deve capturá-lo.
319
+ - É **experimental** e pode mudar de forma incompatível em uma versão menor. A completude não é
320
+ garantida: o SDK faz uma requisição, nunca pagina e nunca trunca; a rota não documenta paginação.
321
+
322
+ ## `request()`: chamada bruta
323
+
324
+ Para uma rota que nenhum método cobre ainda, com a mesma autenticação, prazo, cancelamento e hooks.
325
+ **Ferramenta de desenvolvedor: nunca a exponha como ferramenta de agente nem monte `path` ou `body`
326
+ a partir de entrada não confiável.**
263
327
 
264
328
  ```ts
265
- const safe = omitSensitive(client, clientPersonalKeys);
266
- safe.nome; // type error: `nome` was omitted
329
+ const raw = await sgp.request({ method: "POST", path: "/api/ura/clientes/", body: { limit: 1 } });
267
330
  ```
268
331
 
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.
332
+ - Só caminho relativo sob `/api/` da instância configurada, já na forma final (sem query, `//`,
333
+ `.`/`..`, `%`). Qualquer outro: `INVALID_INPUT`, `effect: "none"`, `attempts: 0`, nada enviado.
334
+ Parâmetros de um `GET` vão em `body` (só primitivos). `encoding`: `json` (padrão) ou `form`.
335
+ - Classifica só por HTTP, com o mesmo mapa de status dos métodos. **`ok: true` significa "o SGP
336
+ aceitou em nível HTTP" (2xx), não sucesso de negócio**: o SGP responde algumas recusas com 200,
337
+ então leia `data` (o corpo como veio, tipado `unknown`).
338
+ - Nunca segue redirecionamentos (3xx é `INVALID_RESPONSE`) e nunca repete.
339
+ - Depois do envio, toda falha tem `effect: "unknown"`, inclusive 4xx; o verbo nunca é tomado como
340
+ prova de que a chamada só lê.
341
+ - Não declara chaves sensíveis: trate `data` e `response` como sensíveis.
342
+
343
+ Veja `examples/request-raw.ts` no repositório.
344
+
345
+ ## IDs com marca e os dois catálogos de planos
346
+
347
+ Alguns ids têm tipo "com marca" (apenas em tempo de compilação; em runtime são números):
348
+
349
+ | Tipo | De onde vem | Para onde vai |
350
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
351
+ | `CustomerId` | `customers.list` (`clientes[].id`), `customers.search` (`clienteId`), `crm.customers.create` (`cliente_id`) | `cliente_id` de `crm.*`, `documents.*`, `customers.list`, `invoices.list` |
352
+ | `PlanId` | `plans.list` (`planos[].id`) | `plano` de `contracts.list`, `plano_id` de `services.changePlan` e de `crm.contracts.create`, `planobase_id` de `services.addTv` e de `services.addGeneric` |
353
+ | `PreRegistrationPlanId` | `preRegistrations.listPlans` (`id`) | `plano_id` de `preRegistrations.create` |
354
+
355
+ Um cliente CRM tem o mesmo `CustomerId` do cliente. **O id de um contrato não é um `CustomerId`**, e
356
+ o compilador recusa um no lugar do outro e um número solto onde um id é esperado. Para marcar um
357
+ número em que você confia: `as CustomerId`.
358
+
359
+ **Há dois catálogos de planos, com espaços de ids diferentes**: o mesmo plano comercial pode ter outro
360
+ id no catálogo do pré-cadastro, e um id aceito por uma rota pode ser recusado pela outra. Os tipos
361
+ `PlanId` e `PreRegistrationPlanId` são mantidos separados de propósito, nos dois sentidos. A
362
+ conversão entre catálogos não é feita pelo SDK.
363
+
364
+ ## Constantes
365
+
366
+ Códigos fixos do SGP exportados como constantes; as operações aceitam qualquer número e o SGP
367
+ decide.
368
+
369
+ | Constante | Valores | Evidência |
370
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
371
+ | `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 |
372
+ | `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) |
373
+ | `ACQUISITION_MODE` | `owned` 0, `loan` 1 | o valor 2, que a documentação do fornecedor lista para o CRM, foi recusado ao vivo |
374
+ | `CONNECTION_STATUS` | `notChecked` 0, `online` 1, `offline` 2, `maintenance` 9 | 1 e 2 observados; 0 e 9 só na documentação do fornecedor |
375
+ | `SERVICE_TYPE` | `internet` 1 | só o 1 foi enviado ao SGP |
376
+ | `CRM_STATUS` | `underAnalysis` 1 ... `technicalFeasibilityAnalysis` 10 | só o 6 (`contractMadeInSgp`) foi enviado ao vivo |
377
+
378
+ O SDK não guarda tabela de transições de status: quem decide o que é permitido é o SGP.
379
+
380
+ ## Referência de operações
381
+
382
+ Cada método aceita `options?: CallOptions` (`abortSignal`, `timeoutMs`, `maxRetries`) como último
383
+ argumento. Campos de entrada e de resposta usam os nomes do SGP. Uma entrada com chave desconhecida é
384
+ recusada localmente (`INVALID_INPUT`); para campos que ainda não têm tipo, use `request()`. O JSDoc de
385
+ cada método, no tipo `SgpClient` (nos tipos publicados, `dist/client.d.ts`), é o texto de referência; aqui
386
+ ficam só rota, codificação, natureza, recusas conhecidas e evidência.
387
+
388
+ Legenda. **Natureza:** _leitura_ é repetível com `maxRetries`; _escrita_ nunca é repetida.
389
+ **Codificação:** `query` (GET, parâmetros na URL), `json`, `form` (multipart sem arquivos),
390
+ `form-files` (multipart com arquivo). **Evidência**, resumida (detalhes por cenário em
391
+ [COMPATIBILITY.md](COMPATIBILITY.md)): _observado_ (resposta real capturada), _parcial_ (sucesso
392
+ observado, mas recusas só documentadas ou reconstruídas, ou o inverso), _documentado_ (só a
393
+ documentação do fornecedor), _confirmado pelo fornecedor, não testado_ (o suporte afirmou, nunca exercitado), _reconstruído_ (corpo reconstruído a partir de relatos ou testes, sem
394
+ captura bruta).
395
+
396
+ ### `customers`
397
+
398
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
399
+ | ------------------------- | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
400
+ | `customers.list(input?)` | `POST /api/ura/clientes/` · json | leitura | Uma página; `paginacao` do SGP como veio; filtros `cliente_id`, `cpfcnpj`, `cliente_nome` e `login` (login PPPoE exato; segredo); contratos e serviços aninhados chegam inteiros | observado |
401
+ | `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) |
402
+
403
+ ### `crm.customers` e `crm.contracts`
404
+
405
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
406
+ | -------------------------------------- | ------------------------------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
407
+ | `crm.customers.create(input)` | `POST /api/crm/cliente/{F\|J}` · json | escrita | `personType` escolhe a rota; `endereco` aninhado, como o SGP o lê. O SDK envia `cpfcnpj` na máscara canônica (CPF `999.999.999-99` em `F`, CNPJ `AA.AAA.AAA/AAAA-99` em `J`, alfanumérico em maiúsculas, com ou sem pontuação na entrada) e recusa localmente (`INVALID_INPUT`, `error.issues` `["cpfcnpj: ..."]`, nada enviado) outro formato ou o tipo errado para a rota; o SDK confere só o formato, mas o SGP confere os dígitos verificadores do CPF (400 `DOCUMENT_INVALID`, `effect: "none"`; os do CNPJ nunca foram testados). O SGP recusa documento já cadastrado, em qualquer status (400 `DOCUMENT_ALREADY_REGISTERED`); **exceção observada: um cliente cujo documento foi gravado sem formatação (ex.: um pré-cadastro só com dígitos, antes do SDK formatar) não foi detectado e cria OUTRO cliente com o mesmo documento**: confira antes (`customers.list({ cpfcnpj })`). 400 de validação de campo: `REJECTED`, `effect: "none"` | observado (`/F`); `/J` confirmado pelo fornecedor, não testado |
408
+ | `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) |
409
+ | `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 |
410
+ | `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) |
411
+ | `crm.contracts.list(input)` | `GET /api/crm/cliente/contratos/?cpfcnpj=` · query | leitura | Documento que não é cliente CRM: 400, `REJECTED`, sem `reason` | observado |
412
+
413
+ ### `preRegistrations`
414
+
415
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
416
+ | -------------------------------- | ----------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
417
+ | `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. O SDK envia `cpfcnpj` na máscara canônica (CPF em `F`, CNPJ em `J`; o SGP grava o que recebe e um CPF só com dígitos escapou da checagem de duplicidade do CRM) e recusa localmente outro formato ou o tipo errado para a rota (`INVALID_INPUT`, `error.issues` `["cpfcnpj: ..."]`); o SGP confere os dígitos verificadores do CPF e recusa o errado (400 `{ message: "CPF inválido", error: "Requisição inválida." }`, `REJECTED`, `DOCUMENT_INVALID`, `effect: "none"`; os do CNPJ nunca foram testados). O SGP recusa um `login` PPPoE já em uso (400 `{ message: "Login <login> já existe", error: "Requisição inválida." }`, `REJECTED`, `LOGIN_ALREADY_REGISTERED`, `effect: "none"`, observado uma vez; `error.sgp` traz só o `error`, e o login que o `message` repete fica em `response.body`, nunca em outro campo do erro): confira antes com `customers.list({ login })`. Com `precadastro_ativar: 1` vira cliente definitivo; contrato só se plano e cobrança forem suficientes. Sucesso traz `message` e, quando o SGP os manda (a captura mais recente mandou; os registros antigos listam só `message`, e como o decoder da 1.x descartava chaves desconhecidas não se sabe se os ids já vinham), `new_cliente_id` (`CustomerId`), `new_contrato_id` e `precadastro_id`, opcionais: sem eles o SDK não inventa nada, releia por `customers.list({ cpfcnpj })`. CPF existente: 402, `REJECTED`, `DOCUMENT_ALREADY_REGISTERED`, `effect: "none"` | parcial (recusa observada; sucesso: `{message}` do fornecedor e, na captura bruta mais recente, também os ids; `/J` confirmado pelo fornecedor, não testado) |
418
+ | `preRegistrations.listPlans()` | `POST /api/precadastro/plano/list` · form (sem barra final) | leitura | `PreRegistrationPlanId`: catálogo próprio, não é o de `plans.list` | observado |
419
+ | `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) |
420
+ | `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) |
421
+
422
+ ### `contracts` e `services`
423
+
424
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
425
+ | ---------------------------------- | ----------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
426
+ | `contracts.listServices(input?)` | `POST /api/suporte/contrato/list/` · form (com barra final) | leitura | Suporte técnico: um array com UMA LINHA POR SERVIÇO (internet e TV) do contrato, com cliente, contrato, endereço e plano repetidos; `servico_online` diz se o serviço está online e a linha online traz a sessão RADIUS (`rad_*`: IP, MAC, NAS, tempo de conexão, contadores). **Exige ao menos um filtro** (`contrato_id` ou `cliente_id`): sem filtro o SGP lista os serviços de todos os clientes, com **senhas de serviço em texto claro**, então o SDK recusa a chamada sem filtro (`INVALID_INPUT`, `attempts: 0`, nada enviado). Ids que o SGP manda como texto e os contadores RADIUS são números, `contrato_status` vem aparado. Login, senha, MACs, IPs (inclusive prefixos IPv6 e NAS) e ids de sessão são segredos; nome, e-mail, contato e endereço são pessoais (`getSensitivity("contracts.listServices")`) | observado (bruta, contrato sintético offline; a linha online é reconstruída da forma mascarada de um contrato real, só tipos) |
427
+ | `contracts.checkConnection(input)` | `POST /api/ura/verificaacesso/` · form (com barra final) | **escrita** | Veredito do SGP sobre a conexão (`status` 1 online, 2 offline, 9 manutenção, 0 nada checado: `CONNECTION_STATUS`). `contrato` (exercitado) ou `telefone` (documentado); `status_all: 1` inclui contratos que o filtro padrão deixa de fora (um contrato suspenso devolveu `[]` sem ele). **É DECLARADA ESCRITA**: o fornecedor documenta que, em manutenção ativa, o SGP abre uma ocorrência por padrão, mesmo sem `protocolo_ura` (não testado). Nunca é repetida, e uma falha traz `effect` (em geral `"unknown"`: releia `occurrences.list({ contrato })` antes de reenviar). Chame `maintenance.list()` antes. `protocolo_ura` não é oferecido (use `occurrences.create`). `data` é SEMPRE um array (um item por serviço de internet): o objeto único que o SGP devolve num contrato ativo sem `status_all` é embrulhado; `[]` não é "offline", é sem veredito. `login` é segredo; `razaoSocial` e `cpfCnpj` são pessoais | observado (bruta, contrato sintético suspenso; o objeto único é reconstruído da forma mascarada de um contrato real) |
428
+ | `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) |
429
+ | `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) |
430
+ | `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 |
431
+ | `contracts.listStatusReasons()` | `POST /api/ura/contrato/status/motivos/` · json | leitura | Cada provedor tem a sua lista: leia antes de passar `motivo` | observado |
432
+ | `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 |
433
+ | `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) |
434
+ | `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 |
435
+ | `services.addGeneric(input)` | `POST /api/servico/generico` · form | escrita | Rota que a TSMX recomenda para serviço extra do tipo genérico. `planobase_id` é `PlanId` de um plano criado como genérico no painel. Plano de outro tipo: recusa `PLAN_NOT_GENERIC` (`effect: "none"`). Sucesso nunca observado (o provedor não tem plano genérico): `data` é o objeto JSON do SGP, sem campo assumido | parcial (a recusa foi vista ao vivo, corpo reconstruído; sucesso documentado) |
436
+ | `services.removeGeneric(input)` | `DELETE /api/servico/generico/{servico_id}` · form | escrita | O contrato vai num corpo multipart, como em `removeTv`. O fornecedor documenta que a rota não remove o único serviço do contrato; nenhum corpo dessa recusa foi capturado, então não há `reason`. Sucesso nunca observado: `data` é o objeto JSON do SGP | documentado, nunca observado |
437
+
438
+ ### `plans`, `invoices`, `workOrders`, `occurrences`
439
+
440
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
441
+ | -------------------------------- | ---------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
442
+ | `plans.list()` | `GET /api/ura/consultaplano/` · query | leitura | Mantém o formato do SGP (`planos` e totais); `planos[].id` é `PlanId` | observado |
443
+ | `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. Valores (`valor`, `valorDesconto`, `valorCorrigido`, `valorPago`, `valorPagoParcial`) são `number` em reais ou `null` (o SGP manda `""` quando não há valor); texto formatado como `"1.234,56"` não é adivinhado e sai de `data` com `warning`. Traz nome, documento, links de pagamento e PIX | parcial (página vazia observada; página populada reconstruída) |
444
+ | `workOrders.list(input?)` | `POST /api/ura/ordemservico/list/` · json | leitura | Uma página; `ocorrencia` é texto e continua texto | observado |
445
+ | `workOrders.get(input)` | `POST /api/os/list/id/{id}` · form (sem barra final) | leitura | Um objeto plano com 72 campos (OS, cliente, contrato, endereço, plano e serviço); a coleção do fornecedor mostra um array de um elemento e o SGP mandou o objeto. **Traz senhas em texto claro** (`servico_password`, `wan_password`, `wifi_password`, `wifi_password_5`) e login, MAC e IP do serviço: segredos; nome, e-mail, contatos, endereço e texto livre são pessoais. Normalizado por campo: `servico_id`, `contrato_status_id` e `os_setor_id` são números (o SGP manda texto) e `contrato_status` vem aparado; o resto como veio | observado (bruta, ao vivo; as senhas de Wi-Fi e WAN vieram vazias) |
446
+ | `workOrders.listComments(input)` | `GET /api/os/{id}/comentario/list/` · query | leitura | Array do SGP sem envelope; vazio = sem comentários. Texto do comentário e autor são pessoais | parcial (bruta: só `[]`; o formato dos itens é só documentado) |
447
+ | `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) |
448
+ | `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 `occurrences.list`). `data_hora_agendamento` tem de ser exatamente `AAAA-MM-DD HH:MM`: o SGP recusa segundos (HTTP 200, `status: 0`, `msg` "Data e hora de agendamento inválida, formato esperado: AAAA-MM-DD HH:MM"; `REJECTED`, `INVALID_SCHEDULE`, `effect: "none"`, observado uma vez, com releitura: nada foi criado) e o SDK não confere o formato antes (quem decide é o SGP). Qualquer outra recusa com HTTP 200 e `status: 0`: `REJECTED`, `effect: "unknown"`. Observado: ocorrência sem `motivoos` é aceita e o SGP cria a OS mesmo assim; `status_encerrado: 1` encerra ocorrência e OS; encerrar uma OS de instalação (`motivoos` 10) não altera o status do contrato | parcial (sucesso: corpo mascarado, 1.x; recusa do agendamento: bruta; outras recusas: reconstruídas) |
449
+ | `occurrences.list(input?)` | `POST /api/ura/ocorrencia/list/` · form | leitura | **Exige ao menos um filtro (de preferência `contrato`): sem filtro o SGP lista as ocorrências de todos os clientes (até 1000), com nomes e texto livre, então o SDK recusa a chamada sem filtro (`INVALID_INPUT`, `attempts: 0`, nada enviado); `offset` e `limit` sozinhos não contam.** Uma página; devolve a OS de cada ocorrência em `ordens_servicos[]` (é o caminho do chamado até a OS: `ordens_servicos[].id`). Só `contrato` e `limit` foram enviados; os demais filtros são os da coleção do fornecedor | observado (bruta, ao vivo: contrato com e sem ocorrência) |
450
+ | `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) |
451
+ | `occurrences.listMethods()` | `GET /api/ura/ocorrencia/metodo/list/` · query | leitura | | observado (corpo mascarado, 1.x) |
452
+
453
+ ### `documents`, `coverage`, `catalogs`, `experimental`
454
+
455
+ | Método | Rota e codificação | Natureza | Notas | Evidência |
456
+ | -------------------------------------- | ------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
457
+ | `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 |
458
+ | `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) |
459
+ | `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; `lat`, `lng`, `distancia` e `id` de cada CTO são números (o Geogrid manda texto, convertido) | observado (corpo mascarado, 1.x; SGPMaps e Geogrid) |
460
+ | `coverage.checkAddress(input)` | `POST /api/ura/viabilidade/` · form | leitura | Viabilidade por **endereço**, sem coordenada: `cep`, `cidade`, `logradouro`, `bairro`, `numero_inicial`, `numero_final`; ao menos um (o fornecedor manda usar um dos filtros). Devolve o objeto do SGP como veio: `{ viabilidade: true }` ou `{}`. **Sem a chave `viabilidade` = desconhecido, nunca "sem cobertura"**; a resposta de "sem cobertura" nunca foi observada. O endereço é dado pessoal | observado (bruta, ao vivo: `cep` sozinho e `cidade` sozinha; os demais filtros só documentados) |
461
+ | `coverage.listGateways()` | `GET /api/ura/listgatewaymapa` · query (sem barra final) | leitura | Gateways de mapa do provedor; o `id` de cada um é o `gateway` de `coverage.check`; array vazio = nenhum gateway | observado (corpo, ao vivo; Geogrid e SGPMaps) |
462
+ | `maintenance.list()` | `GET /api/ura/manutencao/list/` · query | leitura | Avisos de manutenção (SGP "manutenção"), **inclusive os inativos**, sem filtro nem paginação: filtre por `ativa` e pelo período (`data_inicial`, `data_final`). Array do SGP, sem envelope; as listas de elementos afetados vieram vazias, então o conteúdo delas é desconhecido | observado (bruta, ao vivo: três avisos, todos inativos) |
463
+ | `catalogs.listPops()` | `POST /api/ura/pops/` · json | leitura | | observado (corpo mascarado, 1.x) |
464
+ | `catalogs.listNas()` | `POST /api/ura/nas/list/` · json | leitura | `endereco_ip` é segredo | observado (corpo mascarado, 1.x) |
465
+ | `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) |
466
+ | `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) |
467
+
468
+ Todas as leituras acima aceitam `maxRetries`. `request(input, options?)` está descrita
469
+ [acima](#request-chamada-bruta).
470
+
471
+ ## Limitações confirmadas pelo fornecedor (TSMX, 06/11)
472
+
473
+ O que o suporte da TSMX (Erick Silva, 05/11; Natanael Azevedo, 06/11) afirmou que a API do SGP **não**
474
+ faz. São palavras do fornecedor, que o SDK não testa nem contorna; as evidências por cenário estão em
475
+ [COMPATIBILITY.md](COMPATIBILITY.md).
476
+
477
+ - **Segundo contrato em cliente definitivo:** sem API ("No momento não seria possível adicionar um
478
+ segundo contrato em cadastros definitivos via API").
479
+ - **Telefonia:** sem API para adicionar o serviço ("No momento não temos end-points para adicionar
480
+ serviços de telefonia").
481
+ - **Serviços extras:** pela API só TV (`services.addTv`) e genérico (`services.addGeneric`); internet
482
+ adicional (como locação de roteador) e multimídia, só pelo painel.
483
+ - **"Alterar Titular" e migração de contrato:** sem rota de API (constatação nossa nas rotas
484
+ sondadas; a TSMX não respondeu essa pergunta de forma explícita).
485
+ - **Documentação oficial:** só a coleção do Postman; não há Swagger/OpenAPI.
486
+ - **CNPJ alfanumérico e tipos `E`/`EJ`:** os filtros `cpfcnpj` de leitura aceitam um CNPJ
487
+ alfanumérico (constatação nossa, observada ao vivo, só leitura: resposta normal, sem rejeição de formato; nenhum
488
+ achado foi observado). A criação de PJ com ele não foi testada, e para
489
+ `/E` e `/EJ` o fornecedor não tem exemplo que funcione. Detalhes em
490
+ [COMPATIBILITY.md](COMPATIBILITY.md).
491
+ - **Planos:** os IDs de plano do pré-cadastro e do cliente definitivo são diferentes de propósito e
492
+ nenhuma rota devolve a correspondência; o consumidor casa pela descrição.
493
+
494
+ ## Migração do 1.x
495
+
496
+ A 2.0 é uma quebra de contrato (resultado, códigos, nomes e comportamento), sem aliases. O mapa de
497
+ cada método do 1.x e as mudanças de comportamento estão em [MIGRATION.md](MIGRATION.md); o histórico
498
+ de mudanças, em [CHANGELOG.md](CHANGELOG.md).
499
+
500
+ ## Desenvolvimento
501
+
502
+ Veja `CONTRIBUTING.md` no repositório. Os testes usam sempre um `fetch` injetado: nenhum teste
503
+ toca o SGP, e a CI não usa credenciais.