@telepapo/sdk 0.4.0 → 0.5.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telepapo/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "SDK do TelePapo Developers: autorize contas comerciais e receba e responda mensagens com criptografia de ponta a ponta (Signal).",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -18,7 +18,7 @@ const { Signal } = require('../signal/telepapo_signal.js');
18
18
  export { ArmazenamentoArquivo, ArmazenamentoMemoria, verificarAssinatura };
19
19
  export { cifrarAnexo, decifrarAnexo } from './anexos.js';
20
20
  export { validarBotoes, validarLista, botaoEscolhido, limparTexto } from './botoes.js';
21
- export { pedidoDe } from './pedido.js';
21
+ export { pedidoDe, pedidoAcaoDe, enderecoDe, FORMAS_PAGAMENTO } from './pedido.js';
22
22
 
23
23
  export class ErroTelePapo extends Error {
24
24
  constructor(status, corpo) {
@@ -59,7 +59,7 @@ export class TelePapo {
59
59
  }
60
60
 
61
61
  async http(metodo, caminho, { corpo, token, basic } = {}) {
62
- const headers = { 'User-Agent': 'telepapo-sdk/0.4' };
62
+ const headers = { 'User-Agent': 'telepapo-sdk/0.5' };
63
63
  if (corpo !== undefined) headers['Content-Type'] = 'application/json';
64
64
  if (token) headers.Authorization = `Bearer ${token}`;
65
65
  if (basic) headers.Authorization = 'Basic ' + Buffer.from(`${this.clientId}:${this.credencial}`).toString('base64');
@@ -218,7 +218,7 @@ class Conta {
218
218
  const token = await this.token();
219
219
  const r = await this.cliente.fetch(`${this.cliente.apiUrl}/v1/eu/catalogo/codigo/${encodeURIComponent(codigo)}/foto`, {
220
220
  method: 'PUT',
221
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': mime, 'User-Agent': 'telepapo-sdk/0.4' },
221
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': mime, 'User-Agent': 'telepapo-sdk/0.5' },
222
222
  body: imagem,
223
223
  });
224
224
  if (!r.ok) {
@@ -227,6 +227,48 @@ class Conta {
227
227
  }
228
228
  }
229
229
 
230
+ // ------------------------------------------------------------ pedidos
231
+ // Permissão `pedidos.gerenciar` (docs/API.md, seção 6.7). O servidor guarda só
232
+ // o registro (de quem, quando, situação e andamento); itens, preços, entrega e
233
+ // pagamento vêm na própria mensagem, cifrados (`pedidoDe(mensagem.conteudo)`).
234
+
235
+ /**
236
+ * Pedidos recebidos, do mais novo para o mais antigo.
237
+ * @param {{situacao?: 'ABERTO'|'CONCLUIDO'|'CANCELADO', cliente?: string, antes?: string, limite?: number}} [filtro]
238
+ * @returns {Promise<{pedidos: Array<{mensagemId: string, cliente: {id: string, nome: string|null, username: string|null, telefone: string}, situacao: string, criadoEm: string, concluidoEm: string|null, acoes: Array<{codigo: string, descricao: string, em: string}>}>, proximo: string|null}>}
239
+ */
240
+ listarPedidos(filtro = {}) {
241
+ const q = new URLSearchParams(Object.entries(filtro).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)]));
242
+ return this.api('GET', `/v1/eu/pedidos${q.size ? `?${q}` : ''}`);
243
+ }
244
+
245
+ pedido(mensagemId) {
246
+ return this.api('GET', `/v1/eu/pedidos/${encodeURIComponent(mensagemId)}`);
247
+ }
248
+
249
+ /**
250
+ * Grava uma resposta no andamento do pedido **e** avisa o cliente na
251
+ * conversa (texto citando o pedido, com `pedidoAcao`). `CONCLUIDO` conclui.
252
+ * Sem `descricao`, vale a das respostas do pedido configuradas no perfil.
253
+ * @param {string} cliente id de quem fez o pedido (`mensagem.de`)
254
+ * @param {string} mensagemId id da mensagem do pedido (`mensagem.conteudo.id`)
255
+ */
256
+ async atualizarPedido(cliente, mensagemId, codigo, { descricao } = {}) {
257
+ const p = await this.api('POST', `/v1/eu/pedidos/${encodeURIComponent(mensagemId)}/acoes`, { codigo, ...(descricao ? { descricao } : {}) });
258
+ const feita = p.acoes[p.acoes.length - 1];
259
+ await this.enviar(cliente, {
260
+ tipo: 'texto',
261
+ texto: `${codigo === 'CONCLUIDO' ? '✅' : '📦'} ${feita.descricao}`,
262
+ resposta: { id: mensagemId, autorId: cliente, trecho: '🛒 Pedido' },
263
+ pedidoAcao: { pedido: mensagemId, codigo, descricao: feita.descricao },
264
+ });
265
+ return p;
266
+ }
267
+
268
+ concluirPedido(cliente, mensagemId) {
269
+ return this.atualizarPedido(cliente, mensagemId, 'CONCLUIDO');
270
+ }
271
+
230
272
  // ------------------------------------------------------------ receber
231
273
 
232
274
  /** Busca o que está pendente (alternativa/complemento ao webhook), decifra e confirma. */
@@ -405,7 +447,7 @@ class Conta {
405
447
  const token = await this.token();
406
448
  const r = await this.cliente.fetch(`${this.cliente.apiUrl}/v1/anexos/${id}/conteudo`, {
407
449
  method: 'PUT',
408
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream', 'User-Agent': 'telepapo-sdk/0.4' },
450
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream', 'User-Agent': 'telepapo-sdk/0.5' },
409
451
  body: blob,
410
452
  });
411
453
  if (!r.ok) {
@@ -420,7 +462,7 @@ class Conta {
420
462
  async baixarAnexo(anexo) {
421
463
  const token = await this.token();
422
464
  const r = await this.cliente.fetch(`${this.cliente.apiUrl}/v1/anexos/${encodeURIComponent(anexo.id)}`, {
423
- headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'telepapo-sdk/0.4' },
465
+ headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'telepapo-sdk/0.5' },
424
466
  });
425
467
  if (!r.ok) {
426
468
  const texto = await r.text();
package/src/pedido.js CHANGED
@@ -1,18 +1,71 @@
1
1
  /**
2
- * Pedido do carrinho (docs/MENSAGENS-RICAS.md §6 do TelePapo): quem conversa
3
- * com a conta comercial monta o carrinho com itens do catálogo e envia.
2
+ * Pedido do carrinho (docs/MENSAGENS-RICAS.md §6, §8, §9 e §10 do TelePapo):
3
+ * quem conversa com a conta comercial monta o carrinho com itens do catálogo,
4
+ * escolhe entrega e pagamento, e envia.
4
5
  *
5
6
  * O preço é o que o CLIENTE VIU, montado no aparelho dele: confira código,
6
7
  * preço e disponibilidade no seu cadastro antes de confirmar o pedido.
7
8
  */
8
9
 
9
10
  const inteiro = (v, min, max) => Number.isInteger(v) && v >= min && v <= max;
10
- const texto = (v, max) => (typeof v === 'string' && v.trim() ? v.trim().slice(0, max) : null);
11
+ // Controles e direção do texto (disfarçam texto); tudo vira uma linha.
12
+ const CONTROLES = /[\u0000-\u001F\u007F‪-‮⁦-⁩]/g;
13
+ const texto = (v, max) => {
14
+ if (typeof v !== 'string') return null;
15
+ const t = v.replace(CONTROLES, ' ').replace(/\s+/g, ' ').trim();
16
+ return t ? t.slice(0, max) : null;
17
+ };
18
+
19
+ /** Formas de pagamento (§10): código → nome. */
20
+ export const FORMAS_PAGAMENTO = {
21
+ PIX: 'Pix',
22
+ DINHEIRO: 'Dinheiro',
23
+ CREDITO: 'Cartão de crédito',
24
+ DEBITO: 'Cartão de débito',
25
+ VALE_REFEICAO: 'Vale-refeição',
26
+ VALE_ALIMENTACAO: 'Vale-alimentação',
27
+ BOLETO: 'Boleto',
28
+ TRANSFERENCIA: 'Transferência bancária',
29
+ };
30
+
31
+ const UFS = new Set('AC AL AP AM BA CE DF ES GO MA MT MS MG PA PB PR PE PI RJ RN RS RO RR SC SP SE TO'.split(' '));
32
+
33
+ /**
34
+ * Endereço (§8) de uma mensagem (`conteudo.endereco`) ou de um pedido
35
+ * (`pedido.entrega`). Inválido: null.
36
+ * @returns {null | {rotulo: string|null, logradouro: string, numero: string, complemento: string|null, bairro: string|null, cidade: string, uf: string, cep: string|null, referencia: string|null}}
37
+ */
38
+ export function enderecoDe(e) {
39
+ if (!e || typeof e !== 'object') return null;
40
+ const logradouro = texto(e.logradouro, 120);
41
+ const numero = texto(e.numero, 10);
42
+ const cidade = texto(e.cidade, 60);
43
+ const uf = typeof e.uf === 'string' && UFS.has(e.uf) ? e.uf : null;
44
+ const cep = typeof e.cep === 'string' && /^\d{5}-\d{3}$/.test(e.cep) ? e.cep : null;
45
+ if (!logradouro || !numero || !cidade || !uf) return null;
46
+ return {
47
+ rotulo: texto(e.rotulo, 30),
48
+ logradouro, numero,
49
+ complemento: texto(e.complemento, 60),
50
+ bairro: texto(e.bairro, 60),
51
+ cidade, uf, cep,
52
+ referencia: texto(e.referencia, 120),
53
+ };
54
+ }
55
+
56
+ /** Forma de pagamento escolhida no pedido (§10), ou null. */
57
+ function pagamentoDe(p) {
58
+ if (!p || typeof p !== 'object' || typeof p.codigo !== 'string' || !/^[A-Z0-9_]{1,30}$/.test(p.codigo)) return null;
59
+ const descricao = FORMAS_PAGAMENTO[p.codigo] ?? texto(p.descricao, 40);
60
+ if (!descricao) return null;
61
+ const troco = p.codigo === 'DINHEIRO' && inteiro(p.trocoParaCentavos, 1, 100_000_000) ? p.trocoParaCentavos : null;
62
+ return { codigo: p.codigo, descricao, trocoParaCentavos: troco };
63
+ }
11
64
 
12
65
  /**
13
66
  * Lê o pedido de uma mensagem recebida (`mensagem.conteudo`). Itens
14
67
  * malformados são descartados; sem nenhum item válido, devolve null.
15
- * @returns {null | { loja: string|null, itens: Array<{produtoId: string|null, codigo: string|null, descricao: string, quantidade: number, precoCentavos: number, totalCentavos: number}>, totalCentavos: number, moeda: string, observacao: string|null }}
68
+ * @returns {null | { loja: string|null, itens: Array<{produtoId: string|null, codigo: string|null, descricao: string, quantidade: number, precoCentavos: number, totalCentavos: number}>, totalCentavos: number, moeda: string, observacao: string|null, entrega: ReturnType<typeof enderecoDe>, pagamento: null | {codigo: string, descricao: string, trocoParaCentavos: number|null} }}
16
69
  */
17
70
  export function pedidoDe(conteudo) {
18
71
  const p = conteudo?.pedido;
@@ -37,5 +90,22 @@ export function pedidoDe(conteudo) {
37
90
  totalCentavos: itens.reduce((s, i) => s + i.totalCentavos, 0),
38
91
  moeda: 'BRL',
39
92
  observacao: texto(p.observacao, 500),
93
+ entrega: enderecoDe(p.entrega),
94
+ pagamento: pagamentoDe(p.pagamento),
40
95
  };
41
96
  }
97
+
98
+ /**
99
+ * Andamento de pedido numa mensagem (§9): a resposta da loja ou o
100
+ * cancelamento do cliente. Confira quem mandou: `CANCELADO` só vale do cliente
101
+ * do pedido; os outros códigos, só da loja.
102
+ * @returns {null | {pedido: string, codigo: string, descricao: string}}
103
+ */
104
+ export function pedidoAcaoDe(conteudo) {
105
+ const a = conteudo?.pedidoAcao;
106
+ if (!a || typeof a !== 'object') return null;
107
+ const pedido = typeof a.pedido === 'string' && /^[A-Za-z0-9_-]{8,64}$/.test(a.pedido) ? a.pedido : null;
108
+ const codigo = typeof a.codigo === 'string' && /^[A-Z0-9_]{1,30}$/.test(a.codigo) ? a.codigo : null;
109
+ const descricao = texto(a.descricao, 40);
110
+ return pedido && codigo && descricao ? { pedido, codigo, descricao } : null;
111
+ }