bfocus 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {bfocus-0.2.0 → bfocus-0.2.1}/PKG-INFO +119 -2
- {bfocus-0.2.0 → bfocus-0.2.1}/README.md +118 -1
- {bfocus-0.2.0 → bfocus-0.2.1}/bfocus/_resources.py +43 -4
- {bfocus-0.2.0 → bfocus-0.2.1}/bfocus/_transport.py +7 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/bfocus/_version.py +1 -1
- {bfocus-0.2.0 → bfocus-0.2.1}/bfocus/errors.py +9 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/bfocus/types.py +11 -1
- {bfocus-0.2.0 → bfocus-0.2.1}/pyproject.toml +1 -1
- {bfocus-0.2.0 → bfocus-0.2.1}/tests/fixtures/cases.json +120 -6
- {bfocus-0.2.0 → bfocus-0.2.1}/tests/test_conformance.py +1 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/tests/test_unit.py +26 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/.gitignore +0 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/LICENSE +0 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/bfocus/__init__.py +0 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/bfocus/_client.py +0 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/bfocus/py.typed +0 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/bfocus/widget.py +0 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/examples/quickstart.py +0 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/tests/_support.py +0 -0
- {bfocus-0.2.0 → bfocus-0.2.1}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bfocus
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: SDK oficial em Python da API pública do bFocus: clientes, produtos, release notes, base de conhecimento e agentes de IA.
|
|
5
5
|
Project-URL: Homepage, https://bfocus.com.br
|
|
6
6
|
Project-URL: Repository, https://github.com/bernisoftware/bfocus-python
|
|
@@ -118,7 +118,7 @@ Construir o cliente não faz nenhuma chamada de rede.
|
|
|
118
118
|
| `bf.customers.interactions` | `list`, `list_all`, `create` |
|
|
119
119
|
| `bf.customers.identifiers` | `add`, `remove` |
|
|
120
120
|
| `bf.people` | `upsert`, `list`, `delete`, `batch` |
|
|
121
|
-
| `bf.people.identifiers` | `add`, `remove` |
|
|
121
|
+
| `bf.people.identifiers` | `list`, `add`, `remove` |
|
|
122
122
|
| `bf.products` | `list`, `get`, `upsert`, `archive` |
|
|
123
123
|
| `bf.release_notes` | `list`, `list_all`, `get`, `upsert`, `publish` |
|
|
124
124
|
| `bf.kb` | `search` |
|
|
@@ -201,6 +201,101 @@ bf.people.upsert("erp-1042", "app-77", access=True) # devolve o acesso
|
|
|
201
201
|
de chamados e conversas. Um `upsert` com `access=True` devolve o acesso.
|
|
202
202
|
- Como nos outros upserts, só o que você passa muda; `name` é obrigatório ao criar.
|
|
203
203
|
|
|
204
|
+
### Campos personalizados da pessoa
|
|
205
|
+
|
|
206
|
+
`custom_fields` leva o que só existe no seu sistema (matrícula, centro de custo, filial). É a
|
|
207
|
+
**exceção** ao "só o que vier muda": a lista enviada **substitui a lista inteira** — campo que
|
|
208
|
+
ficar de fora é **removido**. Mande sempre a lista que o seu sistema tem hoje; omitir o argumento não mexe
|
|
209
|
+
em nada, como em qualquer outro campo.
|
|
210
|
+
|
|
211
|
+
A `visibility` é decidida no bFocus e **preservada entre sincronizações** — por isso ela não vai
|
|
212
|
+
no envio, só volta na resposta: o seu ERP não rebaixa nem promove a exposição de um dado sem
|
|
213
|
+
querer.
|
|
214
|
+
|
|
215
|
+
Vale no upsert de pessoa, no lote de pessoas e na listagem de pessoas do cliente.
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
p = bf.people.upsert(
|
|
219
|
+
"erp-1042",
|
|
220
|
+
"app-77",
|
|
221
|
+
custom_fields=[ # a lista INTEIRA do seu sistema
|
|
222
|
+
{"key": "matricula", "label": "Matrícula", "value": "4471"},
|
|
223
|
+
{"key": "filial", "label": "Filial", "value": "Centro"},
|
|
224
|
+
],
|
|
225
|
+
)
|
|
226
|
+
for campo in p["custom_fields"]:
|
|
227
|
+
print(campo["key"], campo["value"], campo["visibility"]) # visibility vem do bFocus
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Apagar o e-mail ou o telefone da pessoa
|
|
231
|
+
|
|
232
|
+
Um contato gravado errado ficava preso para sempre: enquanto a ficha errada segurasse o
|
|
233
|
+
telefone, nenhum reenvio o soltava. `clear` apaga.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
bf.people.upsert("erp-1042", "app-77", clear=["phone"]) # some o telefone
|
|
237
|
+
bf.people.upsert("erp-1042", "app-77", clear=["email", "phone"]) # some os dois
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Três regras que parecem contraintuitivas e são de propósito:
|
|
241
|
+
|
|
242
|
+
- **Apagar é explícito.** `phone=None`, `clear=[]` e não passar o argumento continuam
|
|
243
|
+
significando **"não mexe"** — a SDK não traduz `None` em `clear`. Fazer o `None` apagar
|
|
244
|
+
teria apagado, em silêncio e na primeira carga seguinte, o dado de todo sistema que manda
|
|
245
|
+
`None` para "não tenho esse valor".
|
|
246
|
+
- **Campo fora da lista é recusado, não ignorado**: hoje só `"email"` e `"phone"`; qualquer
|
|
247
|
+
outro devolve 422 `PERSON_CLEAR_FIELD_INVALID` (`ValidationError`).
|
|
248
|
+
- **Só se limpa a própria ficha.** Se você alcançou a pessoa por um identificador **extra**, a
|
|
249
|
+
API recusa com 409 `PERSON_CLEAR_NOT_OWN_RECORD` (`ConflictError`): apagar o contato de uma
|
|
250
|
+
ficha alcançada por apelido seria apagar dado de outro sistema. Para saber se o id que você
|
|
251
|
+
tem em mãos é o principal ou um extra, use `bf.people.identifiers.list(...)`.
|
|
252
|
+
|
|
253
|
+
Vale no `people.upsert` e no `people.batch` (`{"clear": ["phone"]}` no item).
|
|
254
|
+
|
|
255
|
+
### Contato já usado: um 409 que você consegue resolver
|
|
256
|
+
|
|
257
|
+
`PERSON_EMAIL_TAKEN` e `PERSON_PHONE_TAKEN` (409) não são "tente de novo": o e-mail (ou o
|
|
258
|
+
telefone) já é de outra pessoa da conta. O erro diz **de quem**, em `err.data` (a API repete o mesmo
|
|
259
|
+
detalhe em `err.validation`, por compatibilidade):
|
|
260
|
+
|
|
261
|
+
| campo | o que é |
|
|
262
|
+
| --- | --- |
|
|
263
|
+
| `field` | `email` ou `phone` — qual contato está tomado |
|
|
264
|
+
| `owner_external_id` | o identificador da pessoa que já usa esse contato |
|
|
265
|
+
| `owner_name` | o nome dela |
|
|
266
|
+
| `owner_customer_external_id` | o cliente a que ela pertence |
|
|
267
|
+
|
|
268
|
+
**É o `owner_customer_external_id` que decide a ação**, e os dois casos pedem coisas opostas:
|
|
269
|
+
|
|
270
|
+
- **mesmo cliente que você enviou** → é quase sempre a MESMA pessoa em dois sistemas. Uma pessoa
|
|
271
|
+
tem **N identificadores**: registre o seu como **extra** dela. A partir daí o seu id encontra
|
|
272
|
+
essa pessoa.
|
|
273
|
+
- **outro cliente** → ninguém decide sozinho a quem a pessoa pertence. Não force: registre o caso
|
|
274
|
+
e leve para quem conhece o cadastro. Unificar dois clientes é decisão de gente, não de um
|
|
275
|
+
casamento por e-mail.
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
from bfocus import ConflictError
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
bf.people.upsert("erp-1042", "app-77", name="Paula Reis", email="paula@padaria.example")
|
|
282
|
+
except ConflictError as err:
|
|
283
|
+
if err.code not in ("PERSON_EMAIL_TAKEN", "PERSON_PHONE_TAKEN"):
|
|
284
|
+
raise
|
|
285
|
+
dono = err.data
|
|
286
|
+
if dono.get("owner_customer_external_id") == "erp-1042":
|
|
287
|
+
# A mesma pessoa, com dois ids: o seu vira mais um identificador dela.
|
|
288
|
+
bf.people.identifiers.add(dono["owner_external_id"], "app-77", label="ERP")
|
|
289
|
+
else:
|
|
290
|
+
# Dono em OUTRO cliente: não decida sozinho — registre e leve para o cadastro.
|
|
291
|
+
avisar_cadastro(err.code, dono)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`PERSON_CONTACT_OTHER_CUSTOMER` (409) é o mesmo assunto pelo outro lado, e é **recusa
|
|
295
|
+
definitiva**: a API não move mais uma pessoa de um cliente para outro só porque o e-mail (ou o
|
|
296
|
+
telefone) casou. Repetir a chamada não resolve — trate como caso para o cadastro, nunca como
|
|
297
|
+
falha temporária.
|
|
298
|
+
|
|
204
299
|
## Lotes
|
|
205
300
|
|
|
206
301
|
`customers.batch` e `people.batch` criam/atualizam **até 500 itens por chamada** (`bfocus.BATCH_MAX`).
|
|
@@ -264,6 +359,23 @@ bf.people.identifiers.remove("app-77", "crm-p5")
|
|
|
264
359
|
|
|
265
360
|
Num lote, um item enviado com um id extra volta com o principal em `merged_into`.
|
|
266
361
|
|
|
362
|
+
### Ler os identificadores da pessoa (para reconciliar)
|
|
363
|
+
|
|
364
|
+
`bf.people.list(...)` mostra só o identificador **principal** de cada pessoa. Quando dois
|
|
365
|
+
cadastros seus eram a mesma pessoa, um dos ids virou **extra** — e some da listagem sem ter
|
|
366
|
+
sumido do cadastro. É isso que faz a sua conferência fechar "633 de 636" sem explicar os 3.
|
|
367
|
+
|
|
368
|
+
`people.identifiers.list` é a fonte de verdade dessa conferência, e é **leitura**: antes dela
|
|
369
|
+
era preciso ESCREVER (tentar um `add`) para descobrir o que tinha acontecido. Aceita no
|
|
370
|
+
caminho o id principal **ou qualquer um dos extras**.
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
ids = bf.people.identifiers.list("crm-p5") # o id extra que "sumiu" da listagem
|
|
374
|
+
print(ids["external_id"]) # "app-77" — o principal do cadastro
|
|
375
|
+
for i in ids["identifiers"]:
|
|
376
|
+
print(i["external_id"], i["label"], i["source"])
|
|
377
|
+
```
|
|
378
|
+
|
|
267
379
|
## Sincronizar clientes e usuários do seu sistema
|
|
268
380
|
|
|
269
381
|
**Ids com o prefixo do sistema, sem `:`** — a assinatura do widget recusa `:`. Use `-` como
|
|
@@ -476,6 +588,11 @@ Qualquer resposta fora de 2xx levanta `BfocusError` (ou uma subclasse):
|
|
|
476
588
|
| `ServerError` | 5xx |
|
|
477
589
|
| `NetworkError` | conexão/timeout — `status == 0`, `code == "NETWORK_ERROR"` |
|
|
478
590
|
|
|
591
|
+
Além de `code`, `status`, `message`, `request_id`, `validation`, `retry_after` e
|
|
592
|
+
`required_scope`, o erro tem **`data`**: o `data` do corpo, com o detalhe estruturado que alguns
|
|
593
|
+
erros trazem (`{}` quando não há). É por ele que um 409 de contato tomado diz de **quem** é o
|
|
594
|
+
contato — veja [Pessoas](#pessoas).
|
|
595
|
+
|
|
479
596
|
**Decida pelo `code`** — ele é estável (`CUSTOMER_NOT_FOUND`, `INTEGRATION_SCOPE_MISSING`,
|
|
480
597
|
`VALIDATION_ERROR`…). O `message` é texto para humanos e pode mudar. Ao falar com o suporte,
|
|
481
598
|
informe o `request_id`: ele vem do corpo da resposta, senão do header `X-Request-Id`, senão é o id
|
|
@@ -91,7 +91,7 @@ Construir o cliente não faz nenhuma chamada de rede.
|
|
|
91
91
|
| `bf.customers.interactions` | `list`, `list_all`, `create` |
|
|
92
92
|
| `bf.customers.identifiers` | `add`, `remove` |
|
|
93
93
|
| `bf.people` | `upsert`, `list`, `delete`, `batch` |
|
|
94
|
-
| `bf.people.identifiers` | `add`, `remove` |
|
|
94
|
+
| `bf.people.identifiers` | `list`, `add`, `remove` |
|
|
95
95
|
| `bf.products` | `list`, `get`, `upsert`, `archive` |
|
|
96
96
|
| `bf.release_notes` | `list`, `list_all`, `get`, `upsert`, `publish` |
|
|
97
97
|
| `bf.kb` | `search` |
|
|
@@ -174,6 +174,101 @@ bf.people.upsert("erp-1042", "app-77", access=True) # devolve o acesso
|
|
|
174
174
|
de chamados e conversas. Um `upsert` com `access=True` devolve o acesso.
|
|
175
175
|
- Como nos outros upserts, só o que você passa muda; `name` é obrigatório ao criar.
|
|
176
176
|
|
|
177
|
+
### Campos personalizados da pessoa
|
|
178
|
+
|
|
179
|
+
`custom_fields` leva o que só existe no seu sistema (matrícula, centro de custo, filial). É a
|
|
180
|
+
**exceção** ao "só o que vier muda": a lista enviada **substitui a lista inteira** — campo que
|
|
181
|
+
ficar de fora é **removido**. Mande sempre a lista que o seu sistema tem hoje; omitir o argumento não mexe
|
|
182
|
+
em nada, como em qualquer outro campo.
|
|
183
|
+
|
|
184
|
+
A `visibility` é decidida no bFocus e **preservada entre sincronizações** — por isso ela não vai
|
|
185
|
+
no envio, só volta na resposta: o seu ERP não rebaixa nem promove a exposição de um dado sem
|
|
186
|
+
querer.
|
|
187
|
+
|
|
188
|
+
Vale no upsert de pessoa, no lote de pessoas e na listagem de pessoas do cliente.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
p = bf.people.upsert(
|
|
192
|
+
"erp-1042",
|
|
193
|
+
"app-77",
|
|
194
|
+
custom_fields=[ # a lista INTEIRA do seu sistema
|
|
195
|
+
{"key": "matricula", "label": "Matrícula", "value": "4471"},
|
|
196
|
+
{"key": "filial", "label": "Filial", "value": "Centro"},
|
|
197
|
+
],
|
|
198
|
+
)
|
|
199
|
+
for campo in p["custom_fields"]:
|
|
200
|
+
print(campo["key"], campo["value"], campo["visibility"]) # visibility vem do bFocus
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Apagar o e-mail ou o telefone da pessoa
|
|
204
|
+
|
|
205
|
+
Um contato gravado errado ficava preso para sempre: enquanto a ficha errada segurasse o
|
|
206
|
+
telefone, nenhum reenvio o soltava. `clear` apaga.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
bf.people.upsert("erp-1042", "app-77", clear=["phone"]) # some o telefone
|
|
210
|
+
bf.people.upsert("erp-1042", "app-77", clear=["email", "phone"]) # some os dois
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Três regras que parecem contraintuitivas e são de propósito:
|
|
214
|
+
|
|
215
|
+
- **Apagar é explícito.** `phone=None`, `clear=[]` e não passar o argumento continuam
|
|
216
|
+
significando **"não mexe"** — a SDK não traduz `None` em `clear`. Fazer o `None` apagar
|
|
217
|
+
teria apagado, em silêncio e na primeira carga seguinte, o dado de todo sistema que manda
|
|
218
|
+
`None` para "não tenho esse valor".
|
|
219
|
+
- **Campo fora da lista é recusado, não ignorado**: hoje só `"email"` e `"phone"`; qualquer
|
|
220
|
+
outro devolve 422 `PERSON_CLEAR_FIELD_INVALID` (`ValidationError`).
|
|
221
|
+
- **Só se limpa a própria ficha.** Se você alcançou a pessoa por um identificador **extra**, a
|
|
222
|
+
API recusa com 409 `PERSON_CLEAR_NOT_OWN_RECORD` (`ConflictError`): apagar o contato de uma
|
|
223
|
+
ficha alcançada por apelido seria apagar dado de outro sistema. Para saber se o id que você
|
|
224
|
+
tem em mãos é o principal ou um extra, use `bf.people.identifiers.list(...)`.
|
|
225
|
+
|
|
226
|
+
Vale no `people.upsert` e no `people.batch` (`{"clear": ["phone"]}` no item).
|
|
227
|
+
|
|
228
|
+
### Contato já usado: um 409 que você consegue resolver
|
|
229
|
+
|
|
230
|
+
`PERSON_EMAIL_TAKEN` e `PERSON_PHONE_TAKEN` (409) não são "tente de novo": o e-mail (ou o
|
|
231
|
+
telefone) já é de outra pessoa da conta. O erro diz **de quem**, em `err.data` (a API repete o mesmo
|
|
232
|
+
detalhe em `err.validation`, por compatibilidade):
|
|
233
|
+
|
|
234
|
+
| campo | o que é |
|
|
235
|
+
| --- | --- |
|
|
236
|
+
| `field` | `email` ou `phone` — qual contato está tomado |
|
|
237
|
+
| `owner_external_id` | o identificador da pessoa que já usa esse contato |
|
|
238
|
+
| `owner_name` | o nome dela |
|
|
239
|
+
| `owner_customer_external_id` | o cliente a que ela pertence |
|
|
240
|
+
|
|
241
|
+
**É o `owner_customer_external_id` que decide a ação**, e os dois casos pedem coisas opostas:
|
|
242
|
+
|
|
243
|
+
- **mesmo cliente que você enviou** → é quase sempre a MESMA pessoa em dois sistemas. Uma pessoa
|
|
244
|
+
tem **N identificadores**: registre o seu como **extra** dela. A partir daí o seu id encontra
|
|
245
|
+
essa pessoa.
|
|
246
|
+
- **outro cliente** → ninguém decide sozinho a quem a pessoa pertence. Não force: registre o caso
|
|
247
|
+
e leve para quem conhece o cadastro. Unificar dois clientes é decisão de gente, não de um
|
|
248
|
+
casamento por e-mail.
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from bfocus import ConflictError
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
bf.people.upsert("erp-1042", "app-77", name="Paula Reis", email="paula@padaria.example")
|
|
255
|
+
except ConflictError as err:
|
|
256
|
+
if err.code not in ("PERSON_EMAIL_TAKEN", "PERSON_PHONE_TAKEN"):
|
|
257
|
+
raise
|
|
258
|
+
dono = err.data
|
|
259
|
+
if dono.get("owner_customer_external_id") == "erp-1042":
|
|
260
|
+
# A mesma pessoa, com dois ids: o seu vira mais um identificador dela.
|
|
261
|
+
bf.people.identifiers.add(dono["owner_external_id"], "app-77", label="ERP")
|
|
262
|
+
else:
|
|
263
|
+
# Dono em OUTRO cliente: não decida sozinho — registre e leve para o cadastro.
|
|
264
|
+
avisar_cadastro(err.code, dono)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`PERSON_CONTACT_OTHER_CUSTOMER` (409) é o mesmo assunto pelo outro lado, e é **recusa
|
|
268
|
+
definitiva**: a API não move mais uma pessoa de um cliente para outro só porque o e-mail (ou o
|
|
269
|
+
telefone) casou. Repetir a chamada não resolve — trate como caso para o cadastro, nunca como
|
|
270
|
+
falha temporária.
|
|
271
|
+
|
|
177
272
|
## Lotes
|
|
178
273
|
|
|
179
274
|
`customers.batch` e `people.batch` criam/atualizam **até 500 itens por chamada** (`bfocus.BATCH_MAX`).
|
|
@@ -237,6 +332,23 @@ bf.people.identifiers.remove("app-77", "crm-p5")
|
|
|
237
332
|
|
|
238
333
|
Num lote, um item enviado com um id extra volta com o principal em `merged_into`.
|
|
239
334
|
|
|
335
|
+
### Ler os identificadores da pessoa (para reconciliar)
|
|
336
|
+
|
|
337
|
+
`bf.people.list(...)` mostra só o identificador **principal** de cada pessoa. Quando dois
|
|
338
|
+
cadastros seus eram a mesma pessoa, um dos ids virou **extra** — e some da listagem sem ter
|
|
339
|
+
sumido do cadastro. É isso que faz a sua conferência fechar "633 de 636" sem explicar os 3.
|
|
340
|
+
|
|
341
|
+
`people.identifiers.list` é a fonte de verdade dessa conferência, e é **leitura**: antes dela
|
|
342
|
+
era preciso ESCREVER (tentar um `add`) para descobrir o que tinha acontecido. Aceita no
|
|
343
|
+
caminho o id principal **ou qualquer um dos extras**.
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
ids = bf.people.identifiers.list("crm-p5") # o id extra que "sumiu" da listagem
|
|
347
|
+
print(ids["external_id"]) # "app-77" — o principal do cadastro
|
|
348
|
+
for i in ids["identifiers"]:
|
|
349
|
+
print(i["external_id"], i["label"], i["source"])
|
|
350
|
+
```
|
|
351
|
+
|
|
240
352
|
## Sincronizar clientes e usuários do seu sistema
|
|
241
353
|
|
|
242
354
|
**Ids com o prefixo do sistema, sem `:`** — a assinatura do widget recusa `:`. Use `-` como
|
|
@@ -449,6 +561,11 @@ Qualquer resposta fora de 2xx levanta `BfocusError` (ou uma subclasse):
|
|
|
449
561
|
| `ServerError` | 5xx |
|
|
450
562
|
| `NetworkError` | conexão/timeout — `status == 0`, `code == "NETWORK_ERROR"` |
|
|
451
563
|
|
|
564
|
+
Além de `code`, `status`, `message`, `request_id`, `validation`, `retry_after` e
|
|
565
|
+
`required_scope`, o erro tem **`data`**: o `data` do corpo, com o detalhe estruturado que alguns
|
|
566
|
+
erros trazem (`{}` quando não há). É por ele que um 409 de contato tomado diz de **quem** é o
|
|
567
|
+
contato — veja [Pessoas](#pessoas).
|
|
568
|
+
|
|
452
569
|
**Decida pelo `code`** — ele é estável (`CUSTOMER_NOT_FOUND`, `INTEGRATION_SCOPE_MISSING`,
|
|
453
570
|
`VALIDATION_ERROR`…). O `message` é texto para humanos e pode mudar. Ao falar com o suporte,
|
|
454
571
|
informe o `request_id`: ele vem do corpo da resposta, senão do header `X-Request-Id`, senão é o id
|
|
@@ -150,12 +150,12 @@ def _identifier_label(label: Any) -> Optional[Dict[str, Any]]:
|
|
|
150
150
|
return None if label is UNSET else {"label": label}
|
|
151
151
|
|
|
152
152
|
|
|
153
|
-
def _strings(value: Any) -> Any:
|
|
153
|
+
def _strings(value: Any, field: str = "extra_emails/extra_phones") -> Any:
|
|
154
154
|
"""Sequência de strings → lista (preserva UNSET/None). String solta é erro (viraria letras)."""
|
|
155
155
|
if value is UNSET or value is None:
|
|
156
156
|
return value
|
|
157
157
|
if isinstance(value, (str, bytes)):
|
|
158
|
-
raise TypeError("
|
|
158
|
+
raise TypeError(f"{field} precisa ser uma lista de strings.")
|
|
159
159
|
return list(value)
|
|
160
160
|
|
|
161
161
|
|
|
@@ -554,6 +554,26 @@ class Customers(_Resource):
|
|
|
554
554
|
class PeopleIdentifiers(_Resource):
|
|
555
555
|
"""Identificadores extras de uma pessoa — ``client.people.identifiers``."""
|
|
556
556
|
|
|
557
|
+
def list(
|
|
558
|
+
self, person_external_id: str, *, timeout: Optional[float] = None
|
|
559
|
+
) -> PersonIdentifiers:
|
|
560
|
+
"""Todos os identificadores da pessoa: o principal + os extras.
|
|
561
|
+
|
|
562
|
+
``GET /people/{person_external_id}/identifiers``. Escopo ``customers:read``. Aceita
|
|
563
|
+
no caminho o identificador **principal ou qualquer um dos extras** — ``external_id``
|
|
564
|
+
no retorno é sempre o principal.
|
|
565
|
+
|
|
566
|
+
É a fonte de verdade para **reconciliar**: :meth:`People.list` mostra só o
|
|
567
|
+
identificador principal de cada pessoa, então um id que virou extra (porque dois
|
|
568
|
+
cadastros seus eram a mesma pessoa) some de lá sem ter sumido do cadastro. Sem esta
|
|
569
|
+
leitura era preciso ESCREVER (um ``add``) para descobrir o que tinha acontecido.
|
|
570
|
+
|
|
571
|
+
Pessoa inexistente: ``NotFoundError`` com ``code == "PERSON_NOT_FOUND"``.
|
|
572
|
+
"""
|
|
573
|
+
pid = path_segment(person_external_id, "person_external_id")
|
|
574
|
+
data, _ = self._t.request("GET", f"/people/{pid}/identifiers", timeout=timeout)
|
|
575
|
+
return cast(PersonIdentifiers, data)
|
|
576
|
+
|
|
557
577
|
def add(
|
|
558
578
|
self,
|
|
559
579
|
person_external_id: str,
|
|
@@ -624,6 +644,8 @@ class People(_Resource):
|
|
|
624
644
|
is_primary: MaybeUnset[Optional[bool]] = UNSET,
|
|
625
645
|
extra_emails: MaybeUnset[Optional[Sequence[str]]] = UNSET,
|
|
626
646
|
extra_phones: MaybeUnset[Optional[Sequence[str]]] = UNSET,
|
|
647
|
+
custom_fields: MaybeUnset[Optional[Sequence[CustomFieldInput]]] = UNSET,
|
|
648
|
+
clear: MaybeUnset[Optional[Sequence[str]]] = UNSET,
|
|
627
649
|
idempotency_key: Optional[str] = None,
|
|
628
650
|
timeout: Optional[float] = None,
|
|
629
651
|
) -> PersonUpsertResult:
|
|
@@ -640,6 +662,19 @@ class People(_Resource):
|
|
|
640
662
|
acesso retirado por :meth:`delete`.
|
|
641
663
|
extra_emails: E-mails adicionais (somam aos que já existem).
|
|
642
664
|
extra_phones: Telefones adicionais (somam aos que já existem).
|
|
665
|
+
custom_fields: Campos personalizados da pessoa, ``{"key", "label", "value"}``.
|
|
666
|
+
Ao contrário de ``extra_emails``/``extra_phones``, a lista **substitui** a
|
|
667
|
+
lista inteira: mande o que o seu sistema tem HOJE, porque campo que ficou de
|
|
668
|
+
fora é REMOVIDO. Omitir o argumento não mexe em nada. A ``visibility`` é
|
|
669
|
+
decidida no bFocus e preservada entre sincronizações.
|
|
670
|
+
clear: Campos a **apagar** nesta pessoa — hoje ``["email"]``, ``["phone"]`` ou os
|
|
671
|
+
dois. Apagar é EXPLÍCITO: ``email=None`` (e a lista vazia, e omitir o
|
|
672
|
+
argumento) continua significando "não mexe", nunca "apague". Campo fora da
|
|
673
|
+
lista aceita é RECUSADO pela API (422 ``PERSON_CLEAR_FIELD_INVALID``), não
|
|
674
|
+
ignorado. E só se limpa a PRÓPRIA ficha: se você alcançou a pessoa por um
|
|
675
|
+
identificador EXTRA, a API recusa (409 ``PERSON_CLEAR_NOT_OWN_RECORD``) —
|
|
676
|
+
apagar contato de ficha alcançada por apelido seria apagar dado de outro
|
|
677
|
+
sistema.
|
|
643
678
|
"""
|
|
644
679
|
cext = path_segment(customer_external_id, "customer_external_id")
|
|
645
680
|
pid = path_segment(person_external_id, "person_external_id")
|
|
@@ -653,6 +688,8 @@ class People(_Resource):
|
|
|
653
688
|
"is_primary": is_primary,
|
|
654
689
|
"extra_emails": _strings(extra_emails),
|
|
655
690
|
"extra_phones": _strings(extra_phones),
|
|
691
|
+
"custom_fields": _dicts(custom_fields),
|
|
692
|
+
"clear": _strings(clear, "clear"),
|
|
656
693
|
}
|
|
657
694
|
)
|
|
658
695
|
data, _ = self._t.request(
|
|
@@ -712,9 +749,11 @@ class People(_Resource):
|
|
|
712
749
|
customer = _required_id(op, index, item, "customer_external_id")
|
|
713
750
|
_required_id(op, index, item, "external_id")
|
|
714
751
|
person = compact({k: v for k, v in item.items() if k != "customer_external_id"})
|
|
715
|
-
for field in ("extra_emails", "extra_phones"):
|
|
752
|
+
for field in ("extra_emails", "extra_phones", "clear"):
|
|
716
753
|
if field in person:
|
|
717
|
-
person[field] = _strings(person[field])
|
|
754
|
+
person[field] = _strings(person[field], field)
|
|
755
|
+
if "custom_fields" in person:
|
|
756
|
+
person["custom_fields"] = _dicts(person["custom_fields"])
|
|
718
757
|
body_items.append({"customer_external_id": customer, "person": person})
|
|
719
758
|
if not body_items:
|
|
720
759
|
return _empty_batch()
|
|
@@ -142,6 +142,7 @@ def build_error(
|
|
|
142
142
|
code: Optional[str] = None
|
|
143
143
|
human: Optional[str] = None
|
|
144
144
|
validation: Dict[str, Any] = {}
|
|
145
|
+
data: Dict[str, Any] = {}
|
|
145
146
|
request_id: Optional[str] = None
|
|
146
147
|
|
|
147
148
|
if isinstance(payload, dict):
|
|
@@ -154,6 +155,11 @@ def build_error(
|
|
|
154
155
|
human = msg
|
|
155
156
|
if isinstance(payload.get("validation"), dict):
|
|
156
157
|
validation = payload["validation"]
|
|
158
|
+
# `data` é o detalhe estruturado do erro (de quem é o contato já usado, o dono de um
|
|
159
|
+
# identificador…). A API também o repete em `validation`, mas quem lê o erro precisa
|
|
160
|
+
# alcançá-lo sem depender dessa duplicação.
|
|
161
|
+
if isinstance(payload.get("data"), dict):
|
|
162
|
+
data = payload["data"]
|
|
157
163
|
rid = payload.get("request_id")
|
|
158
164
|
if isinstance(rid, str) and rid:
|
|
159
165
|
request_id = rid
|
|
@@ -184,6 +190,7 @@ def build_error(
|
|
|
184
190
|
status,
|
|
185
191
|
request_id=request_id,
|
|
186
192
|
validation=validation,
|
|
193
|
+
data=data,
|
|
187
194
|
retry_after=retry_after,
|
|
188
195
|
required_scope=required_scope,
|
|
189
196
|
body=payload if payload is not None else (text or None),
|
|
@@ -38,6 +38,13 @@ class BfocusError(Exception):
|
|
|
38
38
|
``X-Request-Id`` que a SDK enviou (a API ecoa o do cliente) — sempre preenchido;
|
|
39
39
|
informe-o ao suporte.
|
|
40
40
|
validation: Mapa campo → motivo (erros de validação); ``{}`` quando não há.
|
|
41
|
+
data: O ``data`` do corpo do erro — o detalhe estruturado que alguns erros trazem;
|
|
42
|
+
``{}`` quando não há. É onde vem, por exemplo, de quem é o contato já usado num
|
|
43
|
+
409 ``PERSON_EMAIL_TAKEN``/``PERSON_PHONE_TAKEN`` (``field``,
|
|
44
|
+
``owner_external_id``, ``owner_name``, ``owner_customer_external_id``) e o
|
|
45
|
+
``owner`` de um ``IDENTIFIER_IN_USE``. A API repete esse detalhe em
|
|
46
|
+
:attr:`validation`, por compatibilidade com as SDKs que ainda não expunham
|
|
47
|
+
``data``.
|
|
41
48
|
retry_after: Segundos sugeridos pelo header ``Retry-After`` (só em 429).
|
|
42
49
|
required_scope: Escopo que faltou na chave (header ``X-Required-Scope``, só em 403).
|
|
43
50
|
body: Corpo da resposta já decodificado (dict), ou o texto cru quando não é JSON.
|
|
@@ -54,6 +61,7 @@ class BfocusError(Exception):
|
|
|
54
61
|
retry_after: Optional[float] = None,
|
|
55
62
|
required_scope: Optional[str] = None,
|
|
56
63
|
body: Any = None,
|
|
64
|
+
data: Optional[Dict[str, Any]] = None,
|
|
57
65
|
) -> None:
|
|
58
66
|
super().__init__(message)
|
|
59
67
|
self.code = code
|
|
@@ -61,6 +69,7 @@ class BfocusError(Exception):
|
|
|
61
69
|
self.status = status
|
|
62
70
|
self.request_id = request_id
|
|
63
71
|
self.validation: Dict[str, Any] = dict(validation or {})
|
|
72
|
+
self.data: Dict[str, Any] = dict(data or {})
|
|
64
73
|
self.retry_after = retry_after
|
|
65
74
|
self.required_scope = required_scope
|
|
66
75
|
self.body = body
|
|
@@ -124,7 +124,12 @@ class _CustomFieldInputRequired(TypedDict):
|
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
class CustomFieldInput(_CustomFieldInputRequired, total=False):
|
|
127
|
-
"""Campo personalizado de cliente
|
|
127
|
+
"""Campo personalizado de cliente **ou de pessoa**. Enviar a lista SUBSTITUI a lista inteira.
|
|
128
|
+
|
|
129
|
+
Em pessoa, ``visibility`` **não** é aceito aqui: quem vê o campo é decisão do bFocus e é
|
|
130
|
+
preservada entre sincronizações (o seu sistema não rebaixa nem promove a exposição de um
|
|
131
|
+
dado sem querer).
|
|
132
|
+
"""
|
|
128
133
|
|
|
129
134
|
label: str
|
|
130
135
|
type: Literal[
|
|
@@ -188,6 +193,10 @@ class PersonBatchItem(_PersonBatchItemRequired, total=False):
|
|
|
188
193
|
is_primary: Optional[bool]
|
|
189
194
|
extra_emails: Optional[List[str]]
|
|
190
195
|
extra_phones: Optional[List[str]]
|
|
196
|
+
custom_fields: Optional[List[CustomFieldInput]]
|
|
197
|
+
#: Campos a APAGAR nesta pessoa (hoje ``"email"`` e/ou ``"phone"``). Apagar é explícito:
|
|
198
|
+
#: ``None``, lista vazia ou chave ausente continuam significando "não mexe".
|
|
199
|
+
clear: Optional[List[str]]
|
|
191
200
|
|
|
192
201
|
|
|
193
202
|
class AgentTurn(TypedDict):
|
|
@@ -375,6 +384,7 @@ class Person(TypedDict):
|
|
|
375
384
|
access: bool
|
|
376
385
|
is_primary: bool
|
|
377
386
|
customer_external_id: str
|
|
387
|
+
custom_fields: List[CustomField]
|
|
378
388
|
|
|
379
389
|
|
|
380
390
|
class PersonUpsertResult(Person):
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bfocus"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "SDK oficial em Python da API pública do bFocus: clientes, produtos, release notes, base de conhecimento e agentes de IA."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -1021,6 +1021,14 @@
|
|
|
1021
1021
|
"access": true,
|
|
1022
1022
|
"is_primary": true,
|
|
1023
1023
|
"customer_external_id": "ERP 1042",
|
|
1024
|
+
"custom_fields": [
|
|
1025
|
+
{
|
|
1026
|
+
"key": "matricula",
|
|
1027
|
+
"label": "Matrícula",
|
|
1028
|
+
"value": "4471",
|
|
1029
|
+
"visibility": "interno"
|
|
1030
|
+
}
|
|
1031
|
+
],
|
|
1024
1032
|
"status": "created"
|
|
1025
1033
|
},
|
|
1026
1034
|
"message": "Executado com sucesso"
|
|
@@ -1038,6 +1046,14 @@
|
|
|
1038
1046
|
"access": true,
|
|
1039
1047
|
"is_primary": true,
|
|
1040
1048
|
"customer_external_id": "ERP 1042",
|
|
1049
|
+
"custom_fields": [
|
|
1050
|
+
{
|
|
1051
|
+
"key": "matricula",
|
|
1052
|
+
"label": "Matrícula",
|
|
1053
|
+
"value": "4471",
|
|
1054
|
+
"visibility": "interno"
|
|
1055
|
+
}
|
|
1056
|
+
],
|
|
1041
1057
|
"status": "created"
|
|
1042
1058
|
}
|
|
1043
1059
|
}
|
|
@@ -1085,6 +1101,14 @@
|
|
|
1085
1101
|
"access": false,
|
|
1086
1102
|
"is_primary": true,
|
|
1087
1103
|
"customer_external_id": "ERP 1042",
|
|
1104
|
+
"custom_fields": [
|
|
1105
|
+
{
|
|
1106
|
+
"key": "matricula",
|
|
1107
|
+
"label": "Matrícula",
|
|
1108
|
+
"value": "4471",
|
|
1109
|
+
"visibility": "interno"
|
|
1110
|
+
}
|
|
1111
|
+
],
|
|
1088
1112
|
"status": "updated"
|
|
1089
1113
|
},
|
|
1090
1114
|
"message": "Executado com sucesso"
|
|
@@ -1102,6 +1126,14 @@
|
|
|
1102
1126
|
"access": false,
|
|
1103
1127
|
"is_primary": true,
|
|
1104
1128
|
"customer_external_id": "ERP 1042",
|
|
1129
|
+
"custom_fields": [
|
|
1130
|
+
{
|
|
1131
|
+
"key": "matricula",
|
|
1132
|
+
"label": "Matrícula",
|
|
1133
|
+
"value": "4471",
|
|
1134
|
+
"visibility": "interno"
|
|
1135
|
+
}
|
|
1136
|
+
],
|
|
1105
1137
|
"status": "updated"
|
|
1106
1138
|
}
|
|
1107
1139
|
}
|
|
@@ -1181,7 +1213,15 @@
|
|
|
1181
1213
|
"role": "Financeiro",
|
|
1182
1214
|
"access": true,
|
|
1183
1215
|
"is_primary": true,
|
|
1184
|
-
"customer_external_id": "ERP 1042"
|
|
1216
|
+
"customer_external_id": "ERP 1042",
|
|
1217
|
+
"custom_fields": [
|
|
1218
|
+
{
|
|
1219
|
+
"key": "matricula",
|
|
1220
|
+
"label": "Matrícula",
|
|
1221
|
+
"value": "4471",
|
|
1222
|
+
"visibility": "interno"
|
|
1223
|
+
}
|
|
1224
|
+
]
|
|
1185
1225
|
},
|
|
1186
1226
|
{
|
|
1187
1227
|
"external_id": null,
|
|
@@ -1191,7 +1231,8 @@
|
|
|
1191
1231
|
"role": "Compras",
|
|
1192
1232
|
"access": false,
|
|
1193
1233
|
"is_primary": false,
|
|
1194
|
-
"customer_external_id": "ERP 1042"
|
|
1234
|
+
"customer_external_id": "ERP 1042",
|
|
1235
|
+
"custom_fields": []
|
|
1195
1236
|
}
|
|
1196
1237
|
],
|
|
1197
1238
|
"message": "Executado com sucesso"
|
|
@@ -1209,7 +1250,15 @@
|
|
|
1209
1250
|
"role": "Financeiro",
|
|
1210
1251
|
"access": true,
|
|
1211
1252
|
"is_primary": true,
|
|
1212
|
-
"customer_external_id": "ERP 1042"
|
|
1253
|
+
"customer_external_id": "ERP 1042",
|
|
1254
|
+
"custom_fields": [
|
|
1255
|
+
{
|
|
1256
|
+
"key": "matricula",
|
|
1257
|
+
"label": "Matrícula",
|
|
1258
|
+
"value": "4471",
|
|
1259
|
+
"visibility": "interno"
|
|
1260
|
+
}
|
|
1261
|
+
]
|
|
1213
1262
|
},
|
|
1214
1263
|
{
|
|
1215
1264
|
"external_id": null,
|
|
@@ -1219,7 +1268,8 @@
|
|
|
1219
1268
|
"role": "Compras",
|
|
1220
1269
|
"access": false,
|
|
1221
1270
|
"is_primary": false,
|
|
1222
|
-
"customer_external_id": "ERP 1042"
|
|
1271
|
+
"customer_external_id": "ERP 1042",
|
|
1272
|
+
"custom_fields": []
|
|
1223
1273
|
}
|
|
1224
1274
|
]
|
|
1225
1275
|
}
|
|
@@ -1253,7 +1303,15 @@
|
|
|
1253
1303
|
"role": "Financeiro",
|
|
1254
1304
|
"access": false,
|
|
1255
1305
|
"is_primary": true,
|
|
1256
|
-
"customer_external_id": "ERP 1042"
|
|
1306
|
+
"customer_external_id": "ERP 1042",
|
|
1307
|
+
"custom_fields": [
|
|
1308
|
+
{
|
|
1309
|
+
"key": "matricula",
|
|
1310
|
+
"label": "Matrícula",
|
|
1311
|
+
"value": "4471",
|
|
1312
|
+
"visibility": "interno"
|
|
1313
|
+
}
|
|
1314
|
+
]
|
|
1257
1315
|
},
|
|
1258
1316
|
"message": "Acesso retirado"
|
|
1259
1317
|
}
|
|
@@ -1269,7 +1327,15 @@
|
|
|
1269
1327
|
"role": "Financeiro",
|
|
1270
1328
|
"access": false,
|
|
1271
1329
|
"is_primary": true,
|
|
1272
|
-
"customer_external_id": "ERP 1042"
|
|
1330
|
+
"customer_external_id": "ERP 1042",
|
|
1331
|
+
"custom_fields": [
|
|
1332
|
+
{
|
|
1333
|
+
"key": "matricula",
|
|
1334
|
+
"label": "Matrícula",
|
|
1335
|
+
"value": "4471",
|
|
1336
|
+
"visibility": "interno"
|
|
1337
|
+
}
|
|
1338
|
+
]
|
|
1273
1339
|
}
|
|
1274
1340
|
}
|
|
1275
1341
|
},
|
|
@@ -1824,6 +1890,54 @@
|
|
|
1824
1890
|
}
|
|
1825
1891
|
}
|
|
1826
1892
|
},
|
|
1893
|
+
{
|
|
1894
|
+
"id": "people.identifiers.list/by_extra",
|
|
1895
|
+
"op": "people.identifiers.list",
|
|
1896
|
+
"args": {
|
|
1897
|
+
"person_external_id": "crm-p5"
|
|
1898
|
+
},
|
|
1899
|
+
"exchanges": [
|
|
1900
|
+
{
|
|
1901
|
+
"retry": false,
|
|
1902
|
+
"request": {
|
|
1903
|
+
"method": "GET",
|
|
1904
|
+
"path": "/api/v1/integration/people/crm-p5/identifiers",
|
|
1905
|
+
"query": {},
|
|
1906
|
+
"body": null
|
|
1907
|
+
},
|
|
1908
|
+
"response": {
|
|
1909
|
+
"status": 200,
|
|
1910
|
+
"headers": {},
|
|
1911
|
+
"body": {
|
|
1912
|
+
"code": 200,
|
|
1913
|
+
"data": {
|
|
1914
|
+
"external_id": "app-77",
|
|
1915
|
+
"identifiers": [
|
|
1916
|
+
{
|
|
1917
|
+
"external_id": "crm-p5",
|
|
1918
|
+
"label": null,
|
|
1919
|
+
"source": "api"
|
|
1920
|
+
}
|
|
1921
|
+
]
|
|
1922
|
+
},
|
|
1923
|
+
"message": "Executado com sucesso"
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
],
|
|
1928
|
+
"expect": {
|
|
1929
|
+
"result": {
|
|
1930
|
+
"external_id": "app-77",
|
|
1931
|
+
"identifiers": [
|
|
1932
|
+
{
|
|
1933
|
+
"external_id": "crm-p5",
|
|
1934
|
+
"label": null,
|
|
1935
|
+
"source": "api"
|
|
1936
|
+
}
|
|
1937
|
+
]
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
},
|
|
1827
1941
|
{
|
|
1828
1942
|
"id": "people.identifiers.remove/not_found",
|
|
1829
1943
|
"op": "people.identifiers.remove",
|
|
@@ -62,6 +62,7 @@ OPS: Dict[str, Callable[[Bfocus], Callable[..., Any]]] = {
|
|
|
62
62
|
"people.list": lambda bf: bf.people.list,
|
|
63
63
|
"people.delete": lambda bf: bf.people.delete,
|
|
64
64
|
"people.batch": lambda bf: bf.people.batch,
|
|
65
|
+
"people.identifiers.list": lambda bf: bf.people.identifiers.list,
|
|
65
66
|
"people.identifiers.add": lambda bf: bf.people.identifiers.add,
|
|
66
67
|
"people.identifiers.remove": lambda bf: bf.people.identifiers.remove,
|
|
67
68
|
"products.list": lambda bf: bf.products.list,
|
|
@@ -15,7 +15,9 @@ from bfocus import (
|
|
|
15
15
|
UNSET,
|
|
16
16
|
Bfocus,
|
|
17
17
|
BfocusError,
|
|
18
|
+
ConflictError,
|
|
18
19
|
NetworkError,
|
|
20
|
+
NotFoundError,
|
|
19
21
|
Page,
|
|
20
22
|
RateLimitError,
|
|
21
23
|
ServerError,
|
|
@@ -436,6 +438,30 @@ class ErrorShapeTest(_ServerCase):
|
|
|
436
438
|
self.assertIn("req-9", str(err))
|
|
437
439
|
self.assertIsNone(err.retry_after)
|
|
438
440
|
|
|
441
|
+
def test_data_traz_o_dono_do_contato_tomado(self) -> None:
|
|
442
|
+
"""409 acionável: `data` diz de QUEM é o e-mail, e a API repete em `validation`."""
|
|
443
|
+
dono = {"field": "email", "owner_external_id": "app-12", "owner_name": "Paula Reis",
|
|
444
|
+
"owner_customer_external_id": "erp-1042"}
|
|
445
|
+
bf = self.client([{
|
|
446
|
+
"status": 409, "headers": {},
|
|
447
|
+
"body": {"code": 409, "data": dono, "message": "PERSON_EMAIL_TAKEN",
|
|
448
|
+
"error": "PERSON_EMAIL_TAKEN", "validation": dono, "request_id": "req-9"},
|
|
449
|
+
}])
|
|
450
|
+
with self.assertRaises(ConflictError) as ctx:
|
|
451
|
+
bf.people.upsert("erp-1042", "app-77", email="paula@padaria.example")
|
|
452
|
+
err = ctx.exception
|
|
453
|
+
self.assertEqual(err.code, "PERSON_EMAIL_TAKEN")
|
|
454
|
+
self.assertEqual(err.data, dono)
|
|
455
|
+
self.assertEqual(err.data["owner_customer_external_id"], "erp-1042")
|
|
456
|
+
self.assertEqual(err.validation, dono)
|
|
457
|
+
|
|
458
|
+
def test_data_vazio_quando_o_erro_nao_traz_detalhe(self) -> None:
|
|
459
|
+
bf = self.client([{"status": 404, "headers": {},
|
|
460
|
+
"body": {"code": 404, "data": None, "error": "CUSTOMER_NOT_FOUND"}}])
|
|
461
|
+
with self.assertRaises(NotFoundError) as ctx:
|
|
462
|
+
bf.customers.get("erp-1042")
|
|
463
|
+
self.assertEqual(ctx.exception.data, {})
|
|
464
|
+
|
|
439
465
|
def test_2xx_sem_envelope_e_invalid_response(self) -> None:
|
|
440
466
|
bodies = [
|
|
441
467
|
{"status": 200, "headers": {}, "body": "<html>proxy</html>"},
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|