ADR-0018: Namespace business.* (getProfile/updateProfile)
- Status: aceito
- Data: 2026-07-12
Contexto
Sexto item da fila de capabilities novas (ver ADR-0013/0014/0015/0016/0017 para os cinco primeiros). O plano original já sinalizava cobertura mais fraca para este item ("avaliar se vale catálogo completo ou só perfil na primeira PR"). Reconfirmação de escopo (releitura dos 8 relatórios salvos da Epic 7 + verificação ao vivo via gh api/arquivos já cacheados para os 4 providers whatsmeow-based, que o research original não tinha investigado explicitamente para este tópico):
| Capability | waha | evolution | uazapi | zapi | wuzapi | whapi | quepasa | wppconnect | cobertura |
|---|---|---|---|---|---|---|---|---|---|
business.getProfile | — | — | ✅ | — | — | ✅ | — | — | 2/8 |
business.updateProfile | — | — | ✅ | — | — | ✅ | — | ✅ | 3/8 |
Escopo deliberadamente restrito a perfil (getProfile/updateProfile), catálogo/produtos FORA desta rodada — decisão de desenho explícita, já antecipada pelo próprio plano da fila. Todos os 3 relatórios que mencionam business.* (uazapi, Z-API, Whapi) também documentam uma família bem maior de endpoints de catálogo/produtos/coleções/pedidos (business.catalog.*, business.products.*, business.collections.*, business.getOrderItems, business.sendProduct/sendCatalog) — mas os 3 relatórios convergem em tratar isso como "feature de e-commerce do WhatsApp Business, fora do foco de um conector de mensageria" (citação literal do relatório da Z-API) ou de confiança/profundidade insuficiente (uazapi: "não abri o schema completo de cada endpoint... confiança Baixa-Média para virar capability canônica"). Fica fora desta ADR, ver Alternativas.
uazapi 2/2, confiança Alta — POST /business/get/profile (getProfile) e POST /business/update/profile (updateProfile), ambos com payload real documentado. Nuance de resposta parcial: updateProfile usa 207 Multi-Status quando parte dos campos falha ({updated, failed}, com detalhe por campo) — único endpoint do spec inteiro que usa 207. Tratado como falha total (PROVIDER_ERROR) se failed não estiver vazio — ver Decisão.
Z-API 0/2, achado ao vivo que corrige a estimativa original do plano — o relatório documenta GET /instances/{id}/token/{token}/business/profile?phone=... com payload real capturado (description, address, email, websites, categories, businessHours, hasCoverPhoto), e o plano original contava isso como business.getProfile confirmado. Análise mais cuidadosa durante o desenho, cruzando com o padrão já estabelecido no PRÓPRIO adapter Z-API (contacts.getProfilePicture/ getAbout, que recebem chatId e resolvem para o mesmo estilo de query param ?phone=... de UM CONTATO específico), mostra que este endpoint exige um phone de destino — ou seja, é uma consulta ao perfil comercial de UM NÚMERO ESPECÍFICO (provavelmente um contato), não "meu próprio perfil" (que é o que BusinessApi.getProfile() modela, sem nenhum parâmetro). Confirma isso o fato de a Z-API não ter, nesta fase, nenhum endpoint implementado que devolva o número da própria instância conectada (GET /device, que teria esse dado, não é usado por este adapter — citado no relatório original só como candidata de prioridade baixa) — não há como este adapter obter barato o "meu próprio telefone" para testar a hipótese de auto-consulta, e o formato do endpoint (idêntico ao de getProfilePicture/getAbout) sugere fortemente que não é essa a semântica. Não é um gap de confiança no payload — é um descasamento de forma de capability: o endpoint real parece ser mais próximo de um futuro contacts.getBusinessProfile(chatId) do que de business.getProfile(). Fica fora desta ADR (ver Alternativas), corrigindo a contagem original do plano de "3 providers, Z-API incluído" para os 2 providers confirmados abaixo.
Whapi 2/2, confiança Alta, achado ao vivo que corrige o relatório original — o relatório original documentava o endpoint de edição como PATCH /business; verificação ao vivo contra o openapi.yaml cacheado (operationId: editBusinessProfile) mostra que o método real é POST /business, não PATCH. getProfile via GET /business (operationId: getBusinessProfile). Schema de resposta (BusinessProfile, via allOf de BusinessProfileCustom + id) não tem campo categories — diferente de uazapi/Z-API/ WPPConnect, que têm. BusinessProfileCustom (usado tanto na resposta quanto no corpo de updateProfile) também aceita hours/websites na edição, mas esses campos ficam FORA do UpdateBusinessProfileInput desta rodada (ver Decisão) por não serem confirmados como suportados em uazapi (que só documenta description/address/email como editáveis).
Evolution GO e Wuzapi: 0/2, busca negativa confirmada ao vivo — ambos baseados em whatsmeow; verificação ao vivo dos arquivos já cacheados (user_service.go do Evolution GO, handlers.go do Wuzapi) encontrou só um campo BusinessName no perfil de UM CONTATO (indicando se esse contato é uma conta Business) — não uma API para ler/editar o PRÓPRIO perfil comercial da instância. Limitação real, não gap de pesquisa.
WAHA e QuePasa: 0/2 — nenhuma menção a perfil comercial em nenhum dos dois relatórios originais; ambos por natureza mais próximos de "WhatsApp Web genérico" que de uma integração específica com a API oficial de Business.
WPPConnect 1/2 (só updateProfile), achado ao vivo que muda o cálculo de risco — o relatório original não tinha investigado este tópico. Busca ao vivo em routes.ts (cache já existente, seção // Business) encontrou POST /api/{session}/edit-business-profile (SessionController.editBusinessProfile) — mas nenhuma rota de leitura (get-business-profile não existe; as duas outras rotas da seção, get-business-profiles-products e get-order-by-messageId, são de catálogo/pedidos, fora de escopo). Nuance real do payload: o campo de endereço se chama adress (sic — grafia incorreta do provider, confirmada no exemplo #swagger.parameters do controller), não address; o adapter mapeia UpdateBusinessProfileInput.address → adress na saída. Body aceito: {adress?, email?, categories?, websites?} — description não é um campo aceito por este endpoint (nuance real: editBusinessProfile do WPPConnect não tem campo de descrição, diferente de uazapi/Whapi).
Decisão
- Namespace novo
WaAdapter.business?: BusinessApi, inteiramente OPCIONAL — mesmo padrão dechats?/presence?/labels?/channels?(ADR-0012/0015/0016/0017): os 2 métodos também opcionais dentro da interface. - Tipos novos em
src/core/types.ts:BusinessProfile { description?, address?, email?, websites?, categories?, raw }—categoriesnormalizado parastring[](só os nomes/labels de exibição): o shape completo do objeto categoria diverge entre providers que o documentam com payload real (uazapi:{id, localized_display_name}; Z-API, não implementado nesta ADR, ver Contexto:{id, label, displayName}; Whapi nem tem o campo) — mesmo critério de "opaco/simplificado no contrato, detalhe completo só emraw" já usado paraLabelInfo.color(ADR-0016).UpdateBusinessProfileInput { description?, address?, email? }— os 3 campos confirmados como editáveis em uazapi/Whapi (description/address/email); WPPConnect (ver Contexto) confirmaaddress/emailmas não tem campodescriptionno seu endpoint — este adapter ignora silenciosamentedescriptionquando fornecido, caveat documentado no dossiê.websites/hours(aceitos só pelo Whapi) ecategories(nenhum provider confirma edição de categoria via API) ficam fora desta rodada.
BusinessApi:tsinterface BusinessApi { getProfile?(): Promise<BusinessProfile>; updateProfile?(input: UpdateBusinessProfileInput): Promise<void>; }ConnectorBusinessApi— todo método sempre presente (namespace resolvido no conector), gateado por capability + guard-railPROVIDER_ERRORvia um novocallBusinessMethod, réplica exata decallChannelsMethod/callLabelsMethod.- Validação no conector:
updateProfileexige ao menos 1 dos 3 campos (description/address/email) não-undefined— nenhum provider confirmado aceita um update vazio (uazapi documenta isso explicitamente como erro 400; escolhido como regra universal do conector por ser uma proteção óbvia contra uma chamada no-op, mesmo sem confirmação explícita nos outros 2 providers). updateProfileretornaPromise<void>, não o perfil atualizado — nenhum dos 3 providers confirma um retorno de corpo útil e uniforme (Whapi devolve um ack genérico{sent: true}-like; WPPConnect devolve o resultado bruto do client, shape não documentado com confiança; uazapi tem o caso especial do207) — mesmo critério degroups.updateSubject/updateDescription/labels.update(ADR-0009/0016), todosPromise<void>.
Justificativa
- Por que só perfil, sem catálogo/produtos nesta rodada: os 3 relatórios de origem convergem em tratar catálogo como fora do foco do pacote (conector de MENSAGERIA, não e-commerce) e/ou de confiança insuficiente para virar capability canônica — mesmo critério de "não expandir escopo além do núcleo confirmado com confiança" já usado em ADR-0015 (
presence.getfora)/ADR-0017 (updateName/mute/etc. fora dechannels.*). - Por que
categorieséstring[]em vez de preservar{id, ...}: o shape diverge entre os 3 providers que o expõem (uazapi/Z-API têm formatos de objeto diferentes; Whapi nem tem o campo) — normalizar para os nomes de exibição dá um valor útil e comparável entre providers sem forçar um formato de objeto arbitrário; o objeto completo (com IDs específicos do provider) continua disponível viaraw. - Por que
updateProfilenão devolve o perfil atualizado: nenhum provider confirma um corpo de resposta útil e uniforme; forçar um refetch viagetProfile()extra seria uma chamada HTTP a mais não pedida pelo chamador — decisão consistente com o restante do contrato (voidpara updates sem confirmação de retorno útil). - Por que a validação de "ao menos 1 campo" fica no conector, não em cada adapter: é uma regra de negócio cross-provider (evitar um update vazio/no-op), não uma peculiaridade de um provider específico — mesmo critério de centralizar validação no conector já estabelecido no
CONTRIBUTING.md/CLAUDE.md do projeto. - Por que o WPPConnect só implementa
updateProfile, nãogetProfile: busca exaustiva na seção// Businessderoutes.ts(fonte primária, live-verificada) não encontrou nenhuma rota de leitura — limitação real confirmada, não lacuna de pesquisa; mesmo critério de "declarar só o que está confirmado" já aplicado a coberturas assimétricas anteriores (ex.: Evolution GO semchannels.delete/unfollow, ADR-0017).
Consequências
- Enum de capabilities cresce de 64 para 66.
- Cobertura concentrada em 3 dos 8 providers (uazapi 2/2, Whapi 2/2, WPPConnect 1/2 — só
updateProfile, semdescription); WAHA, Evolution GO, Z-API, Wuzapi e QuePasa ficam em 0/2 (limitação real de plataforma para os 4 primeiros; para Z-API, ver Contexto — o único endpoint candidato tem forma incompatível comgetProfile()sem parâmetro). MockAdapterimplementa os 2 métodos em memória (businessProfile, patch parcial acumulado entre chamadas deupdateProfile) — sem getter de inspeção extra, já quegetProfile()já expõe o estado diretamente (diferente deisFollowingChannel/getChatLabelIds, que expõem estado NÃO coberto por um método de leitura do próprio contrato).- Changeset
minor— mudança aditiva, sem breaking change. - Caveat documentado (não resolvido, por design): o
207 Multi-Statusde sucesso parcial da uazapi é tratado como falha total pelo adapter (lançaPROVIDER_ERRORsefailednão estiver vazio) — simplifica o contrato (updateProfilecontinua "tudo ou nada" para o chamador) ao custo de não expor qual campo especificamente falhou; um consumidor que precise desse detalhe hoje só o encontra inspecionando o erro viaraw/mensagem, não um campo estruturado dedicado. - Caveat documentado: o campo
adress(grafia incorreta) do WPPConnect é uma peculiaridade exclusiva desse adapter — a traduçãoaddress→adressacontece só na camada do adapter, o contrato canônico continua com o nome correto.
Alternativas consideradas
- Implementar
business.getProfilena Z-API usando ophoneda própria instância: rejeitado — exigiria um novo endpoint (GET /device) não implementado por nenhuma capability atual deste adapter só para obter o número próprio, e mesmo assim a semântica de auto-consulta não é confirmada (ver Contexto); o endpoint parece modelar antes uma consulta a UM CONTATO específico (mesmo padrão decontacts.getProfilePicture/getAbout), nãobusiness.getProfile()sem argumento. Fica como candidata para uma futuracontacts.getBusinessProfile(chatId)— uma capability DIFERENTE desta ADR, não uma variante dela. - Incluir
business.catalog.*/business.products.*nesta rodada: rejeitado — os 3 relatórios de origem já sinalizam isso como fora do foco de mensageria e/ou confiança insuficiente; ficaria como candidata para uma ADR dedicada futura, caso o roadmap do produto decida expandir para e-commerce. websites/hoursemUpdateBusinessProfileInput: rejeitado nesta rodada — só o Whapi confirma esses campos como editáveis; incluir agora quebraria a convenção de "campo no contrato == confirmado em múltiplos providers" sem trazer benefício imediato (WPPConnect aceitawebsitesmas não via um campo já coberto pelos outros 2 providers com confiança).- Retornar o perfil atualizado em
updateProfile: rejeitado — nenhum provider confirma um corpo de resposta útil e uniforme (ver Decisão/Justificativa); forçar umgetProfile()interno extra dentro do adapter adicionaria uma chamada HTTP não pedida pelo chamador, violando a convenção "uma operação canônica = uma chamada HTTP, sem round-trip extra" salvo necessidade genuína (não é o caso aqui). - Expor o detalhe do
207da uazapi como um retorno estruturado (ex.:{updated: string[], failed: string[]}): rejeitado — nenhum outro provider tem esse conceito de sucesso parcial, e introduzir um retorno rico só para 1/8 providers quebraria a uniformidade do contrato (voidem todo o resto); tratado comoPROVIDER_ERROR(ver Consequências).