DocumentaçãoDocumentationDocumentaciónOne Conecta
ÍndiceIndexÍndice
Baixar .mdDownload .mdBajar .md
Você está vendo esta documentação online. No topo você também pode baixar o PDF (mesmo conteúdo desta página, no idioma e modo atuais) e o Markdown (Funcional ou Técnica).You are viewing this documentation online. At the top you can also download the PDF (same content as this page, in the current language and mode) and the Markdown (Functional or Technical).Está viendo esta documentación en línea. Arriba también puede bajar el PDF (mismo contenido de esta página, en el idioma y modo actuales) y el Markdown (Funcional o Técnica).
Feature · Insights de concorrênciaFeature · Competitor insightsFeature · Insights de competencia

Insights de concorrênciaCompetitor insightsInsights de competencia

Um formulário de registro único, aberto de dentro de uma visita: o representante de vendas escolhe a empresa concorrente e a atividade observada no varejo, informa o período, descreve o que viu, anexa 1 a 3 fotos e pode marcar o caso como crítico. O envio sai por uma transação do Dispatcher (CompetitorInsightsUploadAPI). A leitura é apenas das duas listas de opções (empresas e atividades), sincronizadas por representante e servidas do cache. Não há histórico na tela: cada abertura é um formulário em branco. A single-record form, opened from inside a visit: the sales rep picks the competitor company and the activity observed at the retail, sets the period, describes what was seen, attaches 1 to 3 photos and may flag the case as critical. Submission goes out through one Dispatcher transaction (CompetitorInsightsUploadAPI). The read side is only the two option lists (companies and activities), synced per rep and served from cache. There is no history on screen: every visit to the screen is a blank form. Un formulario de registro único, abierto desde dentro de una visita: el representante de ventas elige la empresa competidora y la actividad observada en el punto de venta, informa el periodo, describe lo que vio, adjunta 1 a 3 fotos y puede marcar el caso como crítico. El envío sale por una transacción del Dispatcher (CompetitorInsightsUploadAPI). La lectura es solo de las dos listas de opciones (empresas y actividades), sincronizadas por representante y servidas del caché. No hay historial en la pantalla: cada apertura es un formulario en blanco.

PúblicoAudiencePúblico
Representante · QA · Suporte · DevRep · QA · Support · DevRepresentante · QA · Soporte · Dev
Onde ficaWhereDónde
Detalhe da visita → grade de ferramentas → "Competitor Insights"Visit detail → tools grid → "Competitor Insights"Detalle de la visita → grilla de herramientas → "Competitor Insights"
AtualizadoUpdatedActualizado
30/07/20262026-07-30
Disponível emAvailable inDisponible en ZA
01

O que é e para que serveWhat it is and what it's forQué es y para qué sirve

Os Insights de concorrência são o canal pelo qual o representante de vendas reporta uma ação da concorrência vista no ponto de venda: uma promoção de um concorrente, uma troca de display, um combo de preço. O registro é feito durante a visita e vai direto para o backend — não fica guardado no aparelho e não gera lista para consulta posterior dentro do app. Competitor insights is the channel through which the sales rep reports a competitor action seen at the point of sale: a competitor promotion, a display swap, a price bundle. The record is made during the visit and goes straight to the backend — it is not kept on the device and produces no list to browse later inside the app. Los Insights de competencia son el canal por el cual el representante de ventas reporta una acción de la competencia vista en el punto de venta: una promoción de un competidor, un cambio de display, un combo de precio. El registro se hace durante la visita y va directo al backend — no queda guardado en el dispositivo y no genera lista para consultar después dentro de la app.

O que se registra?What gets recorded?¿Qué se registra?

Empresa concorrente, atividade observada, período (data inicial e final), descrição em texto livre, de 1 a 3 fotos e um marcador Crítico.Competitor company, observed activity, period (initial and final date), free-text description, 1 to 3 photos and a Critical flag.Empresa competidora, actividad observada, periodo (fecha inicial y final), descripción en texto libre, de 1 a 3 fotos y un marcador Crítico.

Tudo é obrigatório?Is everything required?¿Todo es obligatorio?

Sim. O botão Enviar só liga com empresa, atividade, as duas datas, descrição não vazia e ao menos uma foto. O toggle Crítico é o único campo opcional.Yes. The Send button only enables with company, activity, both dates, a non-empty description and at least one photo. The Critical toggle is the only optional field.Sí. El botón Enviar solo se habilita con empresa, actividad, las dos fechas, descripción no vacía y al menos una foto. El toggle Crítico es el único campo opcional.

Fica um histórico?Is there a history?¿Queda un historial?

Não na tela. O único rastro no aparelho é o registro de envio, visível na Central de dados.Not on the screen. The only trace on the device is the dispatch record, visible in the Data center.No en la pantalla. El único rastro en el dispositivo es el registro de envío, visible en el Centro de datos.

Só na África do SulSouth Africa onlySolo Sudáfrica A ferramenta aparece na grade de ferramentas da visita apenas na África do Sul (ZA). Não confunda com o tile Ações de concorrência do Brasil (competitor_actions): esse abre a tela de Pesquisas filtrada numa categoria, é outra feature e outra transação. Detalhe em Mercados. The tool appears in the visit tools grid only in South Africa (ZA). Don't confuse it with Brazil's Competitor actions tile (competitor_actions): that one opens the Surveys screen filtered on a category — a different feature and a different transaction. Detail in Markets. La herramienta aparece en la grilla de herramientas de la visita solo en Sudáfrica (ZA). No lo confunda con el tile Acciones de la competencia de Brasil (competitor_actions): ese abre la pantalla de Encuestas filtrada en una categoría, es otra feature y otra transacción. Detalle en Mercados.

02

Como acessarHow to openCómo acceder

um único caminho para esta tela em todo o app — não existe atalho na Home, no menu lateral nem deep link.There is one single path to this screen in the whole app — no Home shortcut, no side menu entry, no deep link.Hay un único camino a esta pantalla en toda la app — no existe atajo en la Home, en el menú lateral ni deep link.

  1. Abra uma visitaOpen a visitAbra una visitaDa lista de Visitas, entre no Detalhe da visita do varejo desejado.From the Visits list, open the Visit detail of the desired retail.Desde la lista de Visitas, entre al Detalle de la visita del punto de venta deseado.
  2. Toque em "Competitor Insights"Tap "Competitor Insights"Toque "Competitor Insights"Na grade de ferramentas, o tile com o ícone de estrela/explosão. Ele é o 8º de 9 tiles na configuração da África do Sul.In the tools grid, the tile with the burst icon. It is the 8th of 9 tiles in the South Africa configuration.En la grilla de herramientas, el tile con el ícono de estrella/explosión. Es el 8.º de 9 tiles en la configuración de Sudáfrica.
  3. A visita precisa estar iniciadaThe visit must be startedLa visita debe estar iniciadaTodo tile da grade passa por uma guarda de início de visita. Se a visita não está iniciada, um modal pergunta se você quer iniciá-la; ao confirmar, a visita é iniciada mas a ferramenta não abre sozinha — você precisa tocar no tile de novo.Every grid tile goes through a visit-start guard. If the visit isn't started, a modal asks whether you want to start it; on confirm the visit starts but the tool does not open by itself — you have to tap the tile again.Todo tile de la grilla pasa por una guarda de inicio de visita. Si la visita no está iniciada, un modal pregunta si desea iniciarla; al confirmar, la visita se inicia pero la herramienta no se abre sola — hay que tocar el tile de nuevo.
  4. O formulário abre em brancoThe form opens blankEl formulario abre en blancoCom uma seta de voltar no topo. Nada é pré-preenchido e nada é rascunhado: sair da tela descarta o que foi digitado e apaga as fotos capturadas.With a back arrow at the top. Nothing is pre-filled and nothing is drafted: leaving the screen discards what was typed and deletes the captured photos.Con una flecha de volver arriba. Nada viene pre-completado y nada se guarda como borrador: salir de la pantalla descarta lo escrito y borra las fotos capturadas.
03

Estrutura da telaScreen structureEstructura de la pantalla

Uma coluna rolável, de cima para baixo. Não há barra de resumo, não há aba e não há pull-to-refresh.A single scrollable column, top to bottom. There is no summary bar, no tab and no pull-to-refresh.Una columna desplazable, de arriba a abajo. No hay barra de resumen, no hay pestañas y no hay pull-to-refresh.

Data de sincronizaçãoSync dateFecha de sincronización
Quando as listas de opções (empresas e atividades) foram sincronizadas pela última vez. É o dado da própria feature, não do representante.When the option lists (companies and activities) were last synced. It is the feature's own data, not the rep's.Cuándo se sincronizaron por última vez las listas de opciones (empresas y actividades). Es el dato de la propia feature, no del representante.
CabeçalhoHeaderEncabezado
Ícone da ferramenta + o título "Competitor Insights". Não mostra o varejo nem a visita.Tool icon + the "Competitor Insights" title. It shows neither the retail nor the visit.Ícono de la herramienta + el título "Competitor Insights". No muestra el punto de venta ni la visita.
EmpresaCompanyEmpresa
Seletor de escolha única que abre um modal com a lista de empresas concorrentes vinda da sincronização.A single-choice selector that opens a modal with the competitor company list coming from the sync.Selector de elección única que abre un modal con la lista de empresas competidoras que viene de la sincronización.
AtividadeActivityActividad
Mesmo componente, para a lista de atividades. As duas listas são independentes — escolher a empresa não filtra as atividades.Same component, for the activity list. The two lists are independent — picking the company does not filter the activities.Mismo componente, para la lista de actividades. Las dos listas son independientes — elegir la empresa no filtra las actividades.
PeríodoPeriodPeriodo
Dois cartões lado a lado — Data inicial e Data final — cada um abrindo um calendário. Sem valor, o cartão mostra "—".Two cards side by side — Initial date and Final date — each opening a calendar. With no value, the card shows "—".Dos tarjetas lado a lado — Fecha inicial y Fecha final — cada una abriendo un calendario. Sin valor, la tarjeta muestra "—".
DescriçãoDescriptionDescripción
Campo de texto de 3 a 4 linhas, com dica "Descreva a atividade realizada pelo Concorrente". Sem limite de caracteres.A 3 to 4 line text field, hinting "Describe the activity being carried out by the Competition". No character limit.Campo de texto de 3 a 4 líneas, con pista "Describe la actividad realizada por el Competidor". Sin límite de caracteres.
FotosPhotosFotos
Uma faixa clicável com ícone de câmera que abre a câmera, e abaixo as miniaturas do que já foi capturado (cada uma com um botão de remover). Ao chegar em 3 fotos a faixa deixa de responder ao toque — sem nenhuma mudança visual: rótulo, ícone e borda continuam idênticos.A tappable strip with a camera icon that opens the camera, and below it the thumbnails of what was already captured (each with a remove button). Once 3 photos are reached the strip stops responding to taps — with no visual change at all: label, icon and border stay identical.Una franja clicable con ícono de cámara que abre la cámara, y abajo las miniaturas de lo ya capturado (cada una con un botón de quitar). Al llegar a 3 fotos la franja deja de responder al toque — sin ningún cambio visual: rótulo, ícono y borde siguen idénticos.
CríticoCriticalCrítico
Um interruptor com ícone de alerta. Desligado, o rótulo fica em cinza claro; ligado, fica em destaque. É o único campo opcional.A switch with a warning icon. Off, the label is light grey; on, it is highlighted. It is the only optional field.Un interruptor con ícono de alerta. Apagado, el rótulo queda en gris claro; encendido, se destaca. Es el único campo opcional.
Botão EnviarSend buttonBotón Enviar
Botão preenchido de largura cheia no fim da coluna. Fica desabilitado até o formulário estar completo e mostra um indicador de carregamento durante o envio.A full-width filled button at the end of the column. It stays disabled until the form is complete and shows a loading indicator during submission.Botón relleno de ancho completo al final de la columna. Queda deshabilitado hasta que el formulario esté completo y muestra un indicador de carga durante el envío.
Opções não sincronizadasOptions not syncedOpciones no sincronizadas
Se o cache das duas listas (empresas e atividades) está vazio, a tela abre em estado de erro, com botão de tentar de novo — não em formulário vazio. O botão repete a leitura local, então só resolve depois que o varredor de frescor tiver populado o cache (roda a cada 60 s e trata ausência de sincronização como dado obsoleto). É o estado que o suporte recebe em print quando o rep abre a ferramenta antes da primeira sincronização.If the cache of both lists (companies and activities) is empty, the screen opens in an error state with a retry button — not as an empty form. The button repeats the local read, so it only clears once the freshness sweep has populated the cache (it runs every 60 s and treats a missing sync as stale data). This is the state support gets screenshots of when the rep opens the tool before the first sync.Si el caché de las dos listas (empresas y actividades) está vacío, la pantalla abre en estado de error, con botón de reintentar — no como formulario vacío. El botón repite la lectura local, así que solo se resuelve cuando el barrido de frescura haya poblado el caché (corre cada 60 s y trata la ausencia de sincronización como dato obsoleto). Es el estado del que soporte recibe capturas cuando el rep abre la herramienta antes de la primera sincronización.
04

Estados do formulário e do envioForm and submission statesEstados del formulario y del envío

Um insight não tem "status" de negócio como um pedido — ele existe apenas como um envio. O que muda é o estado do formulário e o resultado do envio:An insight has no business "status" like an order — it exists only as a submission. What changes is the form state and the submission outcome:Un insight no tiene "estado" de negocio como un pedido — existe solo como un envío. Lo que cambia es el estado del formulario y el resultado del envío:

incompletoincompleteincompleto pronto para enviarready to sendlisto para enviar enviandosendingenviando falha no enviosend failedfalla en el envío
IncompletoIncompleteIncompleto
Falta pelo menos um dos seis obrigatórios (empresa, atividade, data inicial, data final, descrição, uma foto). O botão Enviar fica desabilitado e não há mensagem apontando o que falta.At least one of the six required items is missing (company, activity, initial date, final date, description, one photo). The Send button stays disabled and there is no message pointing out what is missing.Falta al menos uno de los seis obligatorios (empresa, actividad, fecha inicial, fecha final, descripción, una foto). El botón Enviar queda deshabilitado y no hay mensaje que indique lo que falta.
Pronto para enviarReady to sendListo para enviar
Os seis campos estão preenchidos. O botão fica ativo. Não há modal de confirmação — o toque envia.The six fields are filled. The button becomes active. There is no confirmation modal — the tap sends.Los seis campos están completos. El botón queda activo. No hay modal de confirmación — el toque envía.
EnviandoSendingEnviando
O botão mostra o indicador de carregamento enquanto as fotos são convertidas e a transação é despachada. Um segundo toque não reenvia.The button shows the loading indicator while the photos are converted and the transaction is dispatched. A second tap does not resend.El botón muestra el indicador de carga mientras las fotos se convierten y la transacción se despacha. Un segundo toque no reenvía.
EnviadoSentEnviado
Aviso verde de confirmação e volta automática para o Detalhe da visita. Nada é gravado localmente além do registro de envio.A green confirmation notice and an automatic return to Visit detail. Nothing is stored locally beyond the dispatch record.Aviso verde de confirmación y vuelta automática al Detalle de la visita. Nada se graba localmente más allá del registro de envío.
Falha no envioSend failedFalla en el envío
Aviso vermelho e a tela permanece, com tudo preenchido — dá para tocar em Enviar de novo. O registro de envio fica marcado como erro na Central de dados, de onde é possível reenviar.A red notice and the screen stays, everything still filled in — you can tap Send again. The dispatch record is flagged as an error in the Data center, from where it can be resent.Aviso rojo y la pantalla permanece, con todo completo — se puede tocar Enviar de nuevo. El registro de envío queda marcado como error en el Centro de datos, desde donde se puede reenviar.

Sem rede, o envio falha na horaOffline, the submission fails on the spotSin red, el envío falla al instante Esta transação não entra na fila offline do app (que hoje atende só o envio de visita). Sem conexão, o envio falha imediatamente, o aviso vermelho aparece e o registro é gravado como erro — não como pendente. Ele não é reenviado sozinho quando a rede volta; o reenvio é manual, pela Central de dados. Detalhe em Pendências. This transaction does not enter the app's offline queue (which today only serves the visit submission). With no connection the submission fails immediately, the red notice appears and the record is stored as an error — not as pending. It is not retried on its own when the network returns; resending is manual, through the Data center. Detail in Pending items. Esta transacción no entra en la cola offline de la app (que hoy atiende solo el envío de visita). Sin conexión el envío falla de inmediato, aparece el aviso rojo y el registro se graba como error — no como pendiente. No se reenvía solo cuando la red vuelve; el reenvío es manual, por el Centro de datos. Detalle en Pendientes.

05

Ações: preencher, fotografar, enviarActions: fill in, photograph, sendAcciones: completar, fotografiar, enviar

Escolher empresa / atividadePick company / activityElegir empresa / actividad
Toque no seletor, escolha um item no modal. A escolha substitui a anterior; não há opção "limpar" nem escolha múltipla. Se a sincronização não trouxe opções, a lista abre vazia.Tap the selector, pick an item in the modal. The choice replaces the previous one; there is no "clear" option and no multi-select. If the sync brought no options, the list opens empty.Toque el selector, elija un ítem en el modal. La elección reemplaza la anterior; no hay opción "limpiar" ni selección múltiple. Si la sincronización no trajo opciones, la lista abre vacía.
Definir o períodoSet the periodDefinir el periodo
Cada cartão abre um calendário de data única com botão "Confirmar". Duas regras: escolher uma data inicial posterior à final já escolhida limpa a data final; e tentar escolher uma data final anterior à inicial é ignorado em silêncio (o cartão simplesmente não muda). O calendário não tem limite — datas passadas e futuras são aceitas.Each card opens a single-date calendar with a "Confirm" button. Two rules: picking an initial date later than the already-chosen final date clears the final date; and trying to pick a final date earlier than the initial one is silently ignored (the card simply doesn't change). The calendar has no bounds — past and future dates are both accepted.Cada tarjeta abre un calendario de fecha única con botón "Confirmar". Dos reglas: elegir una fecha inicial posterior a la final ya elegida limpia la fecha final; e intentar elegir una fecha final anterior a la inicial se ignora en silencio (la tarjeta simplemente no cambia). El calendario no tiene límite — se aceptan fechas pasadas y futuras.
DescreverDescribeDescribir
Texto livre. Só espaços não contam como preenchido — o texto é aparado antes da checagem e antes do envio.Free text. Spaces alone don't count as filled — the text is trimmed before the check and before sending.Texto libre. Solo espacios no cuenta como completo — el texto se recorta antes de la verificación y antes del envío.
FotografarPhotographFotografiar
A faixa de fotos abre a câmera. Cada imagem é comprimida para JPEG mirando 300 KB e guardada numa pasta temporária da sessão. Toque no × da miniatura para remover (o arquivo é apagado do aparelho). Ao sair da tela, a pasta da sessão é apagada inteira.The photos strip opens the camera. Each image is compressed to JPEG targeting 300 KB and stored in a temporary session folder. Tap the thumbnail's × to remove it (the file is deleted from the device). On leaving the screen the whole session folder is deleted.La franja de fotos abre la cámara. Cada imagen se comprime a JPEG apuntando a 300 KB y se guarda en una carpeta temporal de la sesión. Toque la × de la miniatura para quitarla (el archivo se borra del dispositivo). Al salir de la pantalla, la carpeta de la sesión se borra entera.
Marcar como críticoFlag as criticalMarcar como crítico
O interruptor viaja no envio como um sinalizador. Ele não muda nada no app — nem valida, nem bloqueia, nem prioriza; o tratamento é do backend.The switch travels in the submission as a flag. It changes nothing in the app — it doesn't validate, block or prioritise; handling is on the backend.El interruptor viaja en el envío como un indicador. No cambia nada en la app — no valida, no bloquea, no prioriza; el tratamiento es del backend.
EnviarSendEnviar
Converte as fotos, monta o pacote da transação e despacha. Sucesso → aviso verde + volta. Falha → aviso vermelho e a tela fica.Converts the photos, assembles the transaction package and dispatches. Success → green notice + return. Failure → red notice and the screen stays.Convierte las fotos, arma el paquete de la transacción y despacha. Éxito → aviso verde + vuelta. Falla → aviso rojo y la pantalla permanece.

Fotos: de 1 a 3, só pela câmeraPhotos: 1 to 3, camera onlyFotos: de 1 a 3, solo por cámara Ao menos uma foto é obrigatória e o máximo é 3. O rótulo da faixa diz "Escolha ou Tire Foto (até 3 imagens)", mas só a câmera está ligada na tela — não há escolha pela galeria (ver Pendências). Se um passeio pela câmera for cancelado, nada é adicionado. At least one photo is mandatory and the maximum is 3. The strip's label reads "Choose or Take Photo (Up to 3 images)", but only the camera is wired on the screen — there is no gallery pick (see Pending items). If the camera trip is cancelled, nothing is added. Al menos una foto es obligatoria y el máximo es 3. El rótulo de la franja dice "Escoge o Toma Foto (hasta 3 imágenes)", pero solo la cámara está conectada en la pantalla — no hay elección por galería (ver Pendientes). Si se cancela el paso por la cámara, nada se agrega.

06

Arquitetura e fluxo de dadosArchitecture & data flowArquitectura y flujo de datos

Clean Architecture + Riverpod + Freezed + ObjectBox. Há dois fluxos distintos e independentes: a leitura das opções de seleção (RPC próprio, cache write-through) e a escrita do insight (Dispatcher, sem persistência de domínio).Clean Architecture + Riverpod + Freezed + ObjectBox. There are two distinct, independent flows: the read of the selection options (own RPC, write-through cache) and the write of the insight (Dispatcher, with no domain persistence).Clean Architecture + Riverpod + Freezed + ObjectBox. Hay dos flujos distintos e independientes: la lectura de las opciones de selección (RPC propio, caché write-through) y la escritura del insight (Dispatcher, sin persistencia de dominio).

Leitura · opções de seleção (cache-first)Read · selection options (cache-first)Lectura · opciones de selección (cache-first)

O build() do Notifier chama execute() com o default DataSourceType.local — ou seja, ao abrir a tela nada vai à rede: as duas listas saem do ObjectBox. O caminho remoto existe e é acionado pelo sweep de frescor de dado (TTL de 24 h) ou por um execute(source: remote) explícito, e regrava o cache (write-through).The Notifier's build() calls execute() with the DataSourceType.local default — meaning opening the screen hits no network: both lists come out of ObjectBox. The remote path exists and is triggered by the data-freshness sweep (24 h TTL) or by an explicit execute(source: remote), and rewrites the cache (write-through).El build() del Notifier llama execute() con el default DataSourceType.local — es decir, al abrir la pantalla nada va a la red: las dos listas salen del ObjectBox. El camino remoto existe y lo dispara el sweep de frescura de dato (TTL de 24 h) o un execute(source: remote) explícito, y regraba el caché (write-through).

  • CompetitorInsightConectaRepServicegRPC · getCompetitorInsightOptions
    • locationHierarchySfidCompetitorInsightsRemoteDataSource
      • toDTOCompetitorInsightOptionsDTOlastSyncAt carimbado aqui
        • toDomain + write-throughCompetitorInsightsRepositoryImplmock | local | remote+fallback
          • saveCompetitorInsightOptionsCompetitorInsightsLocalDataSourceObjectBox · 3 boxes · clear + put
            • execute(source: local)GetCompetitorInsightOptionsUseCase
              • build(visitSfid)CompetitorInsightsNotifier + State
                • → UICompetitorInsightsPage

Escrita · insight via DispatcherWrite · insight via the DispatcherEscritura · insight vía Dispatcher

O botão Enviar chama submit() no Notifier. Ele reúne o que o builder puro não obtém sozinho — a ResourceEntity da sessão, o accountSfid (lido da visita em cache por getCachedBySfid) e as fotos já em base64 —, monta o CompetitorInsightsDispatcherPayloadInput com entities cruas (§36) e despacha um envelope único. Nenhum dado do insight passa pelo Repository nem pelo ObjectBox de domínio.The Send button calls submit() on the Notifier. It gathers what the pure builder can't obtain on its own — the session's ResourceEntity, the accountSfid (read from the cached visit via getCachedBySfid) and the photos already in base64 —, assembles the CompetitorInsightsDispatcherPayloadInput with raw entities (§36) and dispatches a single envelope. No insight data goes through the Repository or the domain ObjectBox.El botón Enviar llama submit() en el Notifier. Reúne lo que el builder puro no obtiene solo — la ResourceEntity de la sesión, el accountSfid (leído de la visita en caché por getCachedBySfid) y las fotos ya en base64 —, arma el CompetitorInsightsDispatcherPayloadInput con entities crudas (§36) y despacha un sobre único. Ningún dato del insight pasa por el Repository ni por el ObjectBox de dominio.

  • CompetitorInsightsPageCustomButton "Enviar"
    • submit()CompetitorInsightsNotifierisSaving = true
      • currentResourceProvider + getCachedBySfidResourceEntity + accountSfidcontexto de sessão e varejo
        • readAsBase64 (1..3)FileCaptureServiceJPEG → base64
          • build(input)BuildCompetitorInsightsDispatcherPayloadUseCase→ DispatcherEnvelope · 13 chaves
            • submit(envelope)SubmitCompetitorInsightsUseCase
              • dispatchDispatcherOrchestratorsem fila offline p/ este tipo
                • sendTransactionDispatcherGatewayendpoint "competitor" · CompetitorInsightsUploadAPI

O insight não tem casa localThe insight has no local homeEl insight no tiene casa local Não existe proto, DTO, Model, Entity nem box para o insight enviado — só para as opções. O único vestígio no aparelho é a linha do histórico de despachos (Central de dados), e nela o payload é anulado no sucesso (para não inflar o banco com as fotos em base64) e preservado no erro (é o que permite o reenvio manual). There is no proto, DTO, Model, Entity or box for the submitted insight — only for the options. The only trace on the device is the dispatch history row (Data center), and there the payload is nulled on success (so the base64 photos don't bloat the database) and preserved on error (which is what makes the manual resend possible). No existe proto, DTO, Model, Entity ni box para el insight enviado — solo para las opciones. El único vestigio en el dispositivo es la fila del historial de despachos (Centro de datos), y ahí el payload se anula en el éxito (para no inflar la base con las fotos en base64) y se preserva en el error (es lo que permite el reenvío manual).

07

Modelo de dadosData modelModelo de datos

O dado de leitura existe em quatro representaçõesProto (wire gRPC) → DTO (Freezed) → Model (ObjectBox) → Entity (domínio) — ligadas por mappers, com cache write-through. O agregado é o container CompetitorInsightOptions: 3 campos (o lastSyncAt + as duas listas) e duas sub-estruturas de lookup puro, CompetitorCompany e CompetitorActivity, cada uma com 2 campos (sfid + name). O contrato não tem nenhum enum — as 4 messages são só escalares.The read data exists in four representationsProto (gRPC wire) → DTO (Freezed) → Model (ObjectBox) → Entity (domain) — linked by mappers, with a write-through cache. The aggregate is the CompetitorInsightOptions container: 3 fields (the lastSyncAt + the two lists) and two sub-structures of pure lookup, CompetitorCompany and CompetitorActivity, each with 2 fields (sfid + name). The contract has no enum at all — the 4 messages are scalars only.El dato de lectura existe en cuatro representacionesProto (wire gRPC) → DTO (Freezed) → Model (ObjectBox) → Entity (dominio) — unidas por mappers, con caché write-through. El agregado es el container CompetitorInsightOptions: 3 campos (el lastSyncAt + las dos listas) y dos sub-estructuras de lookup puro, CompetitorCompany y CompetitorActivity, cada una con 2 campos (sfid + name). El contrato no tiene ningún enum — las 4 messages son solo escalares.

O dado de escrita (o insight) não usa nenhuma dessas quatro representações: ele nasce e morre num CompetitorInsightsDispatcherPayloadInput (Freezed, 10 campos) que o builder serializa direto em JSON de transação. A seguir, nesta ordem: o proto de leitura, as estruturas campo-a-campo por camada, a estrutura de escrita, os mappers e os deltas. ¹ marca campo optional no proto.The write data (the insight) uses none of those four representations: it is born and dies inside a CompetitorInsightsDispatcherPayloadInput (Freezed, 10 fields) that the builder serialises straight into transaction JSON. Next, in this order: the read proto, the field-by-field structures per layer, the write structure, the mappers and the deltas. ¹ marks an optional proto field.El dato de escritura (el insight) no usa ninguna de esas cuatro representaciones: nace y muere en un CompetitorInsightsDispatcherPayloadInput (Freezed, 10 campos) que el builder serializa directo en JSON de transacción. A continuación, en este orden: el proto de lectura, las estructuras campo a campo por capa, la estructura de escritura, los mappers y los deltas. ¹ marca campo optional en el proto.

Proto

CompetitorInsightConectaRep.proto · proto3 · package mn.bat.conectarep.streambridge. Um service, um RPC, 4 messages, zero enums. O canal é o streambridge. Não há RPC de escrita — o envio do insight passa 100% pelo Dispatcher genérico.CompetitorInsightConectaRep.proto · proto3 · package mn.bat.conectarep.streambridge. One service, one RPC, 4 messages, zero enums. The channel is streambridge. There is no write RPC — the insight submission goes 100% through the generic Dispatcher.CompetitorInsightConectaRep.proto · proto3 · package mn.bat.conectarep.streambridge. Un service, un RPC, 4 messages, cero enums. El canal es el streambridge. No hay RPC de escritura — el envío del insight pasa 100% por el Dispatcher genérico.

getCompetitorInsightOptionsunary
MétodoMethodMétodo

rpc getCompetitorInsightOptions(CompetitorInsightOptionsRequest) returns (CompetitorInsightOptionsReply)

/mn.bat.conectarep.streambridge.CompetitorInsightConectaRepService/getCompetitorInsightOptions

Request · CompetitorInsightOptionsRequest
locationHierarchySfid
string · #1 · resolvido no Repository a partir do currentResourceProvider (resource.locationHierarchyId, nunca o sfid do representante) — §25.resolved in the Repository from currentResourceProvider (resource.locationHierarchyId, never the rep's sfid) — §25.resuelto en el Repository a partir de currentResourceProvider (resource.locationHierarchyId, nunca el sfid del representante) — §25.
lastModifiedDate¹
optional string · #2 · gancho de sincronização incremental. É parâmetro do datasource, mas nenhum caller o preenche — inerte (ver Pendências).incremental-sync hook. It is a datasource parameter, but no caller fills it — inert (see Pending items).gancho de sincronización incremental. Es parámetro del datasource, pero ningún caller lo completa — inerte (ver Pendientes).
Reply · CompetitorInsightOptionsReply

repeated CompetitorCompany companies = 1 · repeated CompetitorActivity activities = 2duas listas independentes e nada mais; sem timestamp. O detalhe campo-a-campo está nas Estruturas de dados abaixo.two independent lists and nothing else; no timestamp. The field-by-field detail is in Data structures below.dos listas independientes y nada más; sin timestamp. El detalle campo a campo está en Estructuras de datos abajo.

Estruturas de dadosData structuresEstructuras de datos

Um dropdown por estrutura, aninhados pela hierarquia. Cada tabela tem uma coluna por camada — Proto · DTO · Model · Entity; o texto em azul marca onde o tipo (ou o nome) primeiro muda lendo Proto→DTO→Model→Entity. A linha id é a chave primária do ObjectBox — não é campo de domínio e existe só no Model.One dropdown per structure, nested by hierarchy. Each table has one column per layer — Proto · DTO · Model · Entity; the blue text marks where the type (or the name) first changes reading Proto→DTO→Model→Entity. The id row is the ObjectBox primary key — not a domain field, and it exists only in the Model.Un dropdown por estructura, anidados por jerarquía. Cada tabla tiene una columna por capa — Proto · DTO · Model · Entity; el texto en azul marca dónde primero cambia el tipo (o el nombre) leyendo Proto→DTO→Model→Entity. La fila id es la clave primaria del ObjectBox — no es campo de dominio y existe solo en el Model.

  • CompetitorInsightOptions CompetitorInsightOptionsReply 3 campos (+ PK do Model)fields (+ Model PK)campos (+ PK del Model)
    CampoProtoDTOModelEntity
    idint @Id()
    lastSyncAtausenteDateTimeDateTimeDateTime
    companiesrepeated CompetitorCompanyList<…DTO>ToMany<…Model>List<…Entity>
    activitiesrepeated CompetitorActivityList<…DTO>ToMany<…Model>List<…Entity>

    Defaults dos Freezed (DTO e Entity): as duas listas são lista vazia; lastSyncAt é required e não nulável. No Model o lastSyncAt é @Property(type: PropertyType.date). É single-row: o box guarda no máximo um registro. Nenhuma das três estruturas tem getter.Freezed defaults (DTO and Entity): both lists are an empty list; lastSyncAt is required and non-nullable. In the Model, lastSyncAt is @Property(type: PropertyType.date). It is single-row: the box holds at most one record. None of the three structures has a getter.Defaults de Freezed (DTO y Entity): las dos listas son lista vacía; lastSyncAt es required y no nulable. En el Model, lastSyncAt es @Property(type: PropertyType.date). Es single-row: el box guarda como máximo un registro. Ninguna de las tres estructuras tiene getter.

    • CompetitorCompany CompetitorInsightOptions.companies[] 2 campos (+ PK do Model)fields (+ Model PK)campos (+ PK del Model)
      CampoProtoDTOModelEntity
      idint @Id()
      sfidstringStringStringString
      namestringStringStringString

      Lookup puro: nada de status, nada de vínculo com varejo. O name é o que aparece no seletor; o sfid é o que vai no payload como companyId.Pure lookup: no status, no retail link. name is what shows in the selector; sfid is what goes in the payload as companyId.Lookup puro: nada de estado, nada de vínculo con punto de venta. El name es lo que aparece en el selector; el sfid es lo que va en el payload como companyId.

    • CompetitorActivity CompetitorInsightOptions.activities[] 2 campos (+ PK do Model)fields (+ Model PK)campos (+ PK del Model)
      CampoProtoDTOModelEntity
      idint @Id()
      sfidstringStringStringString
      namestringStringStringString

      Idêntica à de empresa, em box próprio. As duas listas não têm relação entre si no contrato — o app é que as combina no formulário. O sfid vai no payload como activityId.Identical to the company one, in its own box. The two lists have no relation to each other in the contract — it is the app that combines them in the form. sfid goes in the payload as activityId.Idéntica a la de empresa, en box propio. Las dos listas no tienen relación entre sí en el contrato — es la app la que las combina en el formulario. El sfid va en el payload como activityId.

Estrutura de escritaWrite structureEstructura de escritura

CompetitorInsightsDispatcherPayloadInput domain/entities/dispatcher 10 campos · todos requiredfields · all requiredcampos · todos required
CampoTipoTypeTipoQuem injeta / de onde vemWho injects it / where fromQuién lo inyecta / de dónde viene
resourceResourceEntityNotifier, entity crua de currentResourceProvider. A escolha primary/secondary é feita no builder.Notifier, raw entity from currentResourceProvider. The primary/secondary choice is made in the builder.Notifier, entity cruda de currentResourceProvider. La elección primary/secondary se hace en el builder.
accountSfidStringNotifier, de visit.accountData.sfid — lookup cache-only por getCachedBySfid.Notifier, from visit.accountData.sfidcache-only lookup via getCachedBySfid.Notifier, de visit.accountData.sfid — lookup cache-only por getCachedBySfid.
companyCompetitorCompanyEntityState (selectedCompany), entity crua — o builder extrai o sfid.State (selectedCompany), raw entity — the builder extracts the sfid.State (selectedCompany), entity cruda — el builder extrae el sfid.
activityCompetitorActivityEntityState (selectedActivity), entity crua.State (selectedActivity), raw entity.State (selectedActivity), entity cruda.
descriptionStringState, sem aparar — o trim() é do builder.State, untrimmed — the trim() belongs to the builder.State, sin recortar — el trim() es del builder.
startDateDateTimeState (initialDate) como DateTimenunca data já formatada.State (initialDate) as a DateTimenever a pre-formatted date.State (initialDate) como DateTimenunca fecha ya formateada.
endDateDateTimeState (finalDate), idem.State (finalDate), same.State (finalDate), ídem.
imagesBase64List<String>Notifier — I/O já resolvido (a única pré-resolução que o §36 permite, porque o builder é puro).Notifier — I/O already resolved (the only pre-resolution §36 allows, because the builder is pure).Notifier — I/O ya resuelto (la única pre-resolución que el §36 permite, porque el builder es puro).
isCriticalboolState, o toggle da UI.State, the UI toggle.State, el toggle de la UI.
submittedAtDateTimeNotifier, DateTimeUtils.now() — relógio único (§14); a formatação do dateReference é do builder.Notifier, DateTimeUtils.now() — single clock (§14); the dateReference formatting belongs to the builder.Notifier, DateTimeUtils.now() — reloj único (§14); el formateo del dateReference es del builder.

Zero @Default, zero nulável, zero getter. O JSON que sai daqui está detalhado no builder, em UseCases.Zero @Default, zero nullable, zero getters. The JSON produced from it is detailed on the builder, in UseCases.Cero @Default, cero nulable, cero getters. El JSON que sale de aquí está detallado en el builder, en UseCases.

Mappers

As cinco direções existem para as três estruturas de leitura, como extensions em competitor_insight_options_mapper.dart, competitor_company_mapper.dart e competitor_activity_mapper.dart. A escrita não tem mapper — é o builder.All five directions exist for the three read structures, as extensions in competitor_insight_options_mapper.dart, competitor_company_mapper.dart and competitor_activity_mapper.dart. The write side has no mapper — it has the builder.Las cinco direcciones existen para las tres estructuras de lectura, como extensions en competitor_insight_options_mapper.dart, competitor_company_mapper.dart y competitor_activity_mapper.dart. La escritura no tiene mapper — tiene el builder.

DireçãoDirectionDirecciónCompetitorInsightOptionsCompetitorCompanyCompetitorActivityNotaNoteNota
JSON → DTO…OptionsDTOMapper.fromMap…CompanyDTOMapper.fromMap…ActivityDTOMapper.fromMapcarimba lastSyncAt com now(); coage sfid/name ausentes para ""stamps lastSyncAt with now(); coerces missing sfid/name to ""sella lastSyncAt con now(); coacciona sfid/name ausentes a ""
Proto → DTO…ReplyProtoMapper.toDTO…CompanyProtoMapper.toDTO…ActivityProtoMapper.toDTOcarimba lastSyncAt com now() (o Reply não traz timestamp)stamps lastSyncAt with now() (the Reply carries no timestamp)sella lastSyncAt con now() (el Reply no trae timestamp)
DTO → Entity…OptionsDTOMapper.toDomain…CompanyDTOMapper.toDomain…ActivityDTOMapper.toDomaincópia 1:1, sem transformação1:1 copy, no transformationcopia 1:1, sin transformación
Entity → Model…OptionsEntityMapper.toModel…CompanyEntityMapper.toModel…ActivityEntityMapper.toModelas listas viram ToMany via addAll; id fica em 0the lists become ToMany via addAll; id stays at 0las listas se vuelven ToMany vía addAll; id queda en 0
Model → Entity…OptionsModelMapper.toDomain…CompanyModelMapper.toDomain…ActivityModelMapper.toDomaino id do ObjectBox é descartadothe ObjectBox id is droppedel id del ObjectBox se descarta

Os únicos deltasThe only deltasLos únicos deltas

  • campo sintetizado · lastSyncAt não existe no proto: os dois mappers de fronteira (JSON e Proto) o carimbam com DateTimeUtils.now(). Logo o relógio do aparelho é a autoridade, e reprocessar o mesmo payload produz um lastSyncAt novo — o round-trip não é estável no tempo. É legítimo por §21 (o backend não envia o campo).synthesized field · lastSyncAt does not exist in the proto: both boundary mappers (JSON and Proto) stamp it with DateTimeUtils.now(). So the device clock is the authority, and re-processing the same payload yields a new lastSyncAt — the round-trip is not stable in time. This is legitimate under §21 (the backend doesn't send the field).campo sintetizado · lastSyncAt no existe en el proto: los dos mappers de frontera (JSON y Proto) lo sellan con DateTimeUtils.now(). Por eso el reloj del dispositivo es la autoridad, y reprocesar el mismo payload produce un lastSyncAt nuevo — el round-trip no es estable en el tiempo. Es legítimo por §21 (el backend no envía el campo).
  • relação · companies e activities: repeated (proto) e List (DTO/Entity) viram ToMany<…Model> no Model, em dois boxes próprios.relation · companies and activities: repeated (proto) and List (DTO/Entity) become ToMany<…Model> in the Model, in two boxes of their own.relación · companies y activities: repeated (proto) y List (DTO/Entity) se vuelven ToMany<…Model> en el Model, en dos boxes propios.
  • chave primária · o int id do ObjectBox nasce e morre no Model: o Model→Entity não o lê e o Entity→Model sempre devolve 0. Um round-trip Model→Entity→Model destrói a PK — inofensivo aqui só porque o datasource sempre faz clear nos três boxes antes do put (o ToMany não apaga em cascata; é essa limpeza manual que evita linhas órfãs).primary key · the ObjectBox int id is born and dies in the Model: Model→Entity doesn't read it and Entity→Model always yields 0. A Model→Entity→Model round-trip destroys the PK — harmless here only because the datasource always clears all three boxes before the put (ToMany does not cascade-delete; that manual wipe is what prevents orphan rows).clave primaria · el int id del ObjectBox nace y muere en el Model: el Model→Entity no lo lee y el Entity→Model siempre devuelve 0. Un round-trip Model→Entity→Model destruye la PK — inofensivo aquí solo porque el datasource siempre hace clear en los tres boxes antes del put (el ToMany no borra en cascada; esa limpieza manual es lo que evita filas huérfanas).
  • coerção só no caminho JSON · fromMap transforma sfid/name ausentes ou nulos em "", sem log e sem descartar o item. O caminho proto não precisa disso (proto3 já default-a escalar para ""). Consequência: um item de mock sem sfid entraria no seletor como opção de nome vazio.coercion on the JSON path only · fromMap turns missing or null sfid/name into "", with no log and without dropping the item. The proto path doesn't need it (proto3 already defaults scalars to ""). Consequence: a mock item with no sfid would enter the selector as an empty-named option.coerción solo en el camino JSON · fromMap transforma sfid/name ausentes o nulos en "", sin log y sin descartar el ítem. El camino proto no lo necesita (proto3 ya default-a escalares a ""). Consecuencia: un ítem de mock sin sfid entraría en el selector como opción de nombre vacío.
  • as duas datas do insight nunca se encontram · na escrita, startDate/endDate são DateTime serializados como dd/MM/yyyy. Não há nenhuma estrutura de leitura correspondente — o app não recebe de volta o que enviou.the insight's two dates never meet · on the write side, startDate/endDate are DateTimes serialised as dd/MM/yyyy. There is no matching read structure — the app never receives back what it sent.las dos fechas del insight nunca se encuentran · en la escritura, startDate/endDate son DateTime serializados como dd/MM/yyyy. No hay ninguna estructura de lectura correspondiente — la app no recibe de vuelta lo que envió.

Não confundir: CompetitorCheckDon't confuse: CompetitorCheckNo confundir: CompetitorCheck Existe no app uma terceira estrutura com "competitor" no nome — CompetitorCheck, dentro de Visit.competitorChecks no proto de Visitas. Ela alimenta o indicador de status "Há produtos de concorrência no local?" do Detalhe da visita (declarado em BR e CL, mas em BR com isVisible: false — só o Chile o renderiza) e não tem relação nenhuma com esta feature: nenhum arquivo de competitor_insights a lê. There is a third structure in the app with "competitor" in its name — CompetitorCheck, inside Visit.competitorChecks in the Visits proto. It feeds the "Are competitor products present at the location?" status indicator of Visit detail (declared in BR and CL, but in BR with isVisible: false — only Chile renders it) and has no relation whatsoever to this feature: no competitor_insights file reads it. Existe en la app una tercera estructura con "competitor" en el nombre — CompetitorCheck, dentro de Visit.competitorChecks en el proto de Visitas. Alimenta el indicador de estado "¿Se observan productos de la competencia?" del Detalle de la visita (declarado en BR y CL, pero en BR con isVisible: false — solo Chile lo renderiza) y no tiene relación alguna con esta feature: ningún archivo de competitor_insights la lee.

08

Repository

O CompetitorInsightsRepositoryImpl tem 4 métodos públicos (a interface declara exatamente esses 4) e cobre só a leitura das opções. A escrita do insight não passa por aqui — vai pelo Dispatcher. Ele injeta Ref para resolver o locationHierarchySfid (§25) e recebe os três datasources + o serviço de conectividade + a flag de mock.CompetitorInsightsRepositoryImpl has 4 public methods (the interface declares exactly those 4) and covers the read side only. The insight write does not go through here — it goes via the Dispatcher. It injects Ref to resolve the locationHierarchySfid (§25) and takes the three datasources + the connectivity service + the mock flag.El CompetitorInsightsRepositoryImpl tiene 4 métodos públicos (la interface declara exactamente esos 4) y cubre solo la lectura de las opciones. La escritura del insight no pasa por aquí — va por el Dispatcher. Inyecta Ref para resolver el locationHierarchySfid (§25) y recibe los tres datasources + el servicio de conectividad + la flag de mock.

Um dropdown por método — assinatura, retorno e comportamento. A árvore de decisão vive dentro do método que a tem.One dropdown per method — signature, return and behavior. The decision tree lives inside the method that owns it.Un dropdown por método — firma, retorno y comportamiento. El árbol de decisión vive dentro del método que lo tiene.

getCompetitorInsightOptions({source = DataSourceType.local}) mock | local | remote

RetornaReturnsDevuelve Result<CompetitorInsightOptionsEntity, Failure>valor não nulável: cache vazio virá como Error, nunca como sucesso vazio.a non-nullable value: an empty cache comes back as Error, never as an empty success.valor no nulable: caché vacío vendrá como Error, nunca como éxito vacío.

O default do parâmetro é local, então a chamada da tela é cache-first. A árvore de decisão:The parameter default is local, so the screen's call is cache-first. The decision tree:El default del parámetro es local, así que la llamada de la pantalla es cache-first. El árbol de decisión:

  • getCompetitorInsightOptions(source)
    • useMock || source == mock → _fetchFromMock()
      • mockDataSource → toDomain → save → Success grava no cache tambémwrites to cache toograba en el caché también
      • catch → Error(failure) sem fallback de cacheno cache fallbacksin fallback de caché
    • source == local || !isConnected → _fetchFromCacheOrFail()
      • cache com valor → Success(value)
      • cache vazio → Error(NetworkFailure)
    • else → _fetchFromRemoteWithFallback()
      • currentResourceProvider == null → _fetchFromCacheOrFail()
      • remote(locationHierarchySfid) → toDomain → save → Success
      • catch → cache com valor ? Success(value) : Error(failure)

O lastModifiedDate do request nunca é passado: todo fetch remoto traz o catálogo inteiro e regrava o cache do zero.The request's lastModifiedDate is never passed: every remote fetch brings the whole catalog and rewrites the cache from scratch.El lastModifiedDate del request nunca se pasa: todo fetch remoto trae el catálogo entero y regraba el caché desde cero.

getCachedCompetitorInsightOptions() local

RetornaReturnsDevuelve Result<CompetitorInsightOptionsEntity?, Failure>

Cache puro, nunca dispara remoto. Aqui o valor é nulável: "não tem cache" volta como Success(null), e é justamente por isso que os dois helpers privados precisam do when value != null. Erro de leitura passa pelo FailureMapper e é logado com traceId.Pure cache, it never fires remote. Here the value is nullable: "no cache" comes back as Success(null), and that is precisely why both private helpers need the when value != null. A read error goes through FailureMapper and is logged with a traceId.Caché puro, nunca dispara remoto. Aquí el valor es nulable: "no hay caché" vuelve como Success(null), y por eso mismo los dos helpers privados necesitan el when value != null. Un error de lectura pasa por el FailureMapper y se loguea con traceId.

getCachedCompetitorInsightOptionsLastSyncAt() local

RetornaReturnsDevuelve Future<DateTime?>é o único método da interface que não vem embrulhado em Result, porque o contrato de frescor de dado espera um DateTime? cru.the only interface method not wrapped in a Result, because the data-freshness contract expects a raw DateTime?.es el único método de la interface que no viene envuelto en Result, porque el contrato de frescura de dato espera un DateTime? crudo.

Consumido pelo sweep de frescor. Numa exceção de leitura ele engole o erro (loga e devolve null) — e como o motor de frescor trata null como "obsoleto", um box corrompido é lido como "nunca sincronizado".Consumed by the freshness sweep. On a read exception it swallows the error (logs and returns null) — and since the freshness engine treats null as "stale", a corrupt box reads as "never synced".Consumido por el sweep de frescura. En una excepción de lectura se traga el error (loguea y devuelve null) — y como el motor de frescura trata null como "obsoleto", un box corrupto se lee como "nunca sincronizado".

saveCompetitorInsightOptions({entity}) local · cache-writerlocal · cache-writerlocal · cache-writer

RetornaReturnsDevuelve Result<void, Failure>

Escreve o agregado no ObjectBox. É cache-writer, não escrita de negócio — chamado pelos dois caminhos de fetch (mock e remoto) depois do sucesso, o que faz o cache ser write-through. Encaminha o entity.lastSyncAt recebido e não chama DateTimeUtils.now() (§21).Writes the aggregate into ObjectBox. It is a cache-writer, not a business write — called by both fetch paths (mock and remote) after success, which makes the cache write-through. It forwards the received entity.lastSyncAt and does not call DateTimeUtils.now() (§21).Escribe el agregado en ObjectBox. Es cache-writer, no escritura de negocio — llamado por los dos caminos de fetch (mock y remoto) tras el éxito, lo que hace el caché write-through. Reenvía el entity.lastSyncAt recibido y no llama DateTimeUtils.now() (§21).

09

Datasources

Os três datasources canônicos. O mock e o remoto têm um método cada; o local tem quatro e é sincronizado (sem Future). Nenhum deles participa do envio do insight.The three canonical datasources. Mock and remote have one method each; local has four and is synchronous (no Future). None of them takes part in the insight submission.Los tres datasources canónicos. El mock y el remoto tienen un método cada uno; el local tiene cuatro y es sincrónico (sin Future). Ninguno participa del envío del insight.

Remote CompetitorInsightsRemoteDataSource gRPC

Envio: CompetitorInsightConectaRepServiceClient no canal streambridge, com os interceptors padrão. Fluxo de uso: acionado só pelo caminho remoto do Repository (sweep de frescor ou source: remote explícito). Erro: GrpcError vai ao GrpcExceptionHandler; qualquer outro vira ServerException logável.Sends: CompetitorInsightConectaRepServiceClient on the streambridge channel, with the standard interceptors. Usage flow: triggered only by the Repository's remote path (freshness sweep or an explicit source: remote). Error: GrpcError goes to GrpcExceptionHandler; anything else becomes a loggable ServerException.Envío: CompetitorInsightConectaRepServiceClient en el canal streambridge, con los interceptors estándar. Flujo de uso: disparado solo por el camino remoto del Repository (sweep de frescura o source: remote explícito). Error: GrpcError va al GrpcExceptionHandler; cualquier otro se vuelve ServerException logueable.

getCompetitorInsightOptions({locationHierarchySfid, lastModifiedDate})
MétodoMethodMétodo
getCompetitorInsightOptions (unary)
EnvioSendsEnvío
monta o CompetitorInsightOptionsRequest com o locationHierarchySfid; o lastModifiedDate só é setado se vier não nulo e não vazio — e nenhum caller o manda.builds the CompetitorInsightOptionsRequest with the locationHierarchySfid; lastModifiedDate is only set when non-null and non-empty — and no caller sends it.arma el CompetitorInsightOptionsRequest con el locationHierarchySfid; el lastModifiedDate solo se setea si viene no nulo y no vacío — y ningún caller lo manda.
RetornoReturnRetorno
Future<CompetitorInsightOptionsDTO>
Fluxo de usoUsage flowFlujo de uso
reply → toDTO() (carimba o lastSyncAt) → Repository converte e grava.reply → toDTO() (stamps the lastSyncAt) → the Repository converts and stores.reply → toDTO() (sella el lastSyncAt) → el Repository convierte y graba.
Tratamento de erroError handlingManejo de error
o Repository captura e cai no cache; se o cache está vazio, propaga a Failure.the Repository catches it and falls back to cache; if the cache is empty, it propagates the Failure.el Repository lo captura y cae al caché; si el caché está vacío, propaga la Failure.
Local CompetitorInsightsLocalDataSource ObjectBox · 3 boxes

Envio / fluxo: sem rede. Métodos sincronizados sobre três boxes — CompetitorInsightOptionsModel (raiz, single-row) + CompetitorCompanyModel + CompetitorActivityModel. A leitura é _box.getAll().firstOrNull, sem QueryBuilder. Erro: cada método embrulha qualquer exceção numa CacheException logável com mensagem própria.Sends / flow: no network. Synchronous methods over three boxes — CompetitorInsightOptionsModel (root, single-row) + CompetitorCompanyModel + CompetitorActivityModel. The read is _box.getAll().firstOrNull, with no QueryBuilder. Error: each method wraps any exception in a loggable CacheException with its own message.Envío / flujo: sin red. Métodos sincrónicos sobre tres boxes — CompetitorInsightOptionsModel (raíz, single-row) + CompetitorCompanyModel + CompetitorActivityModel. La lectura es _box.getAll().firstOrNull, sin QueryBuilder. Error: cada método envuelve cualquier excepción en una CacheException logueable con mensaje propio.

getCompetitorInsightOptions()
RetornoReturnRetorno
CompetitorInsightOptionsEntity?
ComportamentoBehaviorComportamiento
primeiro (e único) registro do box → toDomain(); box vazio → null. É a fonte da tela.the box's first (and only) record → toDomain(); empty box → null. It is the screen's source.primer (y único) registro del box → toDomain(); box vacío → null. Es la fuente de la pantalla.
getCompetitorInsightOptionsLastSyncAt()
RetornoReturnRetorno
DateTime?
ComportamentoBehaviorComportamiento
o lastSyncAt do único registro — alimenta o DataLoadInfo da tela e o motor de frescor.the single record's lastSyncAt — feeds the screen's DataLoadInfo and the freshness engine.el lastSyncAt del único registro — alimenta el DataLoadInfo de la pantalla y el motor de frescura.
saveCompetitorInsightOptions({entity})
RetornoReturnRetorno
void
ComportamentoBehaviorComportamiento
substituição total, nunca upsert: chama clearCompetitorInsightOptions() e só então faz put do toModel().full replacement, never an upsert: it calls clearCompetitorInsightOptions() and only then puts the toModel().reemplazo total, nunca upsert: llama clearCompetitorInsightOptions() y solo entonces hace put del toModel().
clearCompetitorInsightOptions()
RetornoReturnRetorno
void
ComportamentoBehaviorComportamiento
limpa os três boxes, nesta ordem: empresas, atividades, raiz. É limpeza manual e obrigatória — o ToMany do ObjectBox não apaga os alvos em cascata, então sem isso as linhas de empresa/atividade viravam órfãs a cada sincronização.clears all three boxes, in this order: companies, activities, root. This wipe is manual and mandatory — ObjectBox's ToMany does not cascade-delete the targets, so without it the company/activity rows would be orphaned on every sync.limpia los tres boxes, en este orden: empresas, actividades, raíz. Es limpieza manual y obligatoria — el ToMany del ObjectBox no borra los objetivos en cascada, así que sin ella las filas de empresa/actividad quedarían huérfanas en cada sincronización.
Mock CompetitorInsightsMockDataSource assets JSON

Envio / fluxo: lê um asset de assets/mocks/competitor_insights/jsons/ escolhido pelo mercado ativo e pela flag de mock real. Com a flag ligada busca {mercado}_real_competitor_insights.json e, se o arquivo não existir, devolve "{}" em silêncio; com a flag desligada busca {mercado}_competitor_insights.json e lança se faltar. Erro: qualquer falha vira CacheException citando o mercado.Sends / flow: reads an asset from assets/mocks/competitor_insights/jsons/ chosen by the active market and the real mock flag. With the flag on it looks for {market}_real_competitor_insights.json and, if the file doesn't exist, returns "{}" silently; with the flag off it looks for {market}_competitor_insights.json and throws if it's missing. Error: any failure becomes a CacheException naming the market.Envío / flujo: lee un asset de assets/mocks/competitor_insights/jsons/ elegido por el mercado activo y por la flag de mock real. Con la flag activa busca {mercado}_real_competitor_insights.json y, si el archivo no existe, devuelve "{}" en silencio; con la flag apagada busca {mercado}_competitor_insights.json y lanza si falta. Error: cualquier falla se vuelve CacheException citando el mercado.

getCompetitorInsightOptions()
RetornoReturnRetorno
Future<CompetitorInsightOptionsDTO>
ComportamentoBehaviorComportamiento
jsonDecodefromMap. Um {} resolve para as duas listas vazias + lastSyncAt = now() — a tela abre com os seletores vazios e sem erro.jsonDecodefromMap. A {} resolves to both lists empty + lastSyncAt = now() — the screen opens with empty selectors and no error.jsonDecodefromMap. Un {} resuelve a las dos listas vacías + lastSyncAt = now() — la pantalla abre con los selectores vacíos y sin error.
Fluxo de usoUsage flowFlujo de uso
o Repository converte e grava no cache (o caminho mock também é write-through).the Repository converts it and writes to cache (the mock path is write-through too).el Repository lo convierte y graba en el caché (el camino mock también es write-through).
10

Enums e labelsEnums & labelsEnums y labels

A feature não tem enum de dado próprio: empresa e atividade são lookups de texto vindos do backend, e o proto tem zero enums. Os enums relevantes são os de gating (o tile na grade), de transação (o canal de escrita), de sincronização (o frescor do dado) e de captura de arquivo.The feature has no data enum of its own: company and activity are text lookups coming from the backend, and the proto has zero enums. The relevant enums are the gating one (the grid tile), the transaction one (the write channel), the sync one (data freshness) and the file-capture one.La feature no tiene enum de dato propio: empresa y actividad son lookups de texto que vienen del backend, y el proto tiene cero enums. Los enums relevantes son los de gating (el tile en la grilla), de transacción (el canal de escritura), de sincronización (la frescura del dato) y de captura de archivo.

ModuleDetailType 80 valores · 3 são de concorrência (2 tiles + 1 indicador)values · 3 are competitor-related (2 tiles + 1 indicator)valores · 3 son de competencia (2 tiles + 1 indicador)
casevaluedestinodestinationdestino
visitDetailToolCompetitorInsights"competitor_insights"esta feature. A grade mapeia o case para AppRouter.goToCompetitorInsights(visitSfid:). Ícone ConectaIcons.visitDetailCompetitorInsights, rótulo visit_detail_competitor_insights.this feature. The grid maps the case to AppRouter.goToCompetitorInsights(visitSfid:). Icon ConectaIcons.visitDetailCompetitorInsights, label visit_detail_competitor_insights.esta feature. La grilla mapea el case a AppRouter.goToCompetitorInsights(visitSfid:). Ícono ConectaIcons.visitDetailCompetitorInsights, rótulo visit_detail_competitor_insights.
visitDetailToolCompetitorActions"competitor_actions"outra feature. Mapeia para AppRouter.goToSurveys(category: surveyCompetitorActions) — abre Pesquisas filtrada, com ícone e rótulo próprios, e envia por SurveyResultUploadAPI.a different feature. Maps to AppRouter.goToSurveys(category: surveyCompetitorActions) — opens Surveys filtered, with its own icon and label, and submits via SurveyResultUploadAPI.otra feature. Mapea a AppRouter.goToSurveys(category: surveyCompetitorActions) — abre Encuestas filtrada, con ícono y rótulo propios, y envía por SurveyResultUploadAPI.
unknown"unknown"fallback do fromString: comparação por igualdade exata; ao falhar registra logUnmappedEnumValue (que dispara assert(false) em debug) e devolve unknown. Um valor não reconhecido no EMC faz o tile desaparecer da grade, porque só cases com prefixo visitDetailTool entram.fromString fallback: exact-equality comparison; on failure it records logUnmappedEnumValue (which fires assert(false) in debug) and returns unknown. An unrecognised EMC value makes the tile disappear from the grid, because only cases prefixed visitDetailTool get in.fallback del fromString: comparación por igualdad exacta; al fallar registra logUnmappedEnumValue (que dispara assert(false) en debug) y devuelve unknown. Un valor no reconocido en el EMC hace que el tile desaparezca de la grilla, porque solo entran cases con prefijo visitDetailTool.
visitDetailStatusCompetitors"status_competitors"nem tile, nem tela. É o terceiro case com "competitor" no nome: alimenta o indicador de status do Detalhe da visita, a partir de Visit.competitorChecks. Declarado em BR e CL, mas em BR com isVisible: false — só o Chile o renderiza. Não abre nada.neither tile nor screen. It is the third case with "competitor" in the name: it feeds the Visit detail's status indicator, from Visit.competitorChecks. Declared in BR and CL, but in BR with isVisible: false — only Chile renders it. It opens nothing.ni tile, ni pantalla. Es el tercer case con "competitor" en el nombre: alimenta el indicador de estado del Detalle de la visita, desde Visit.competitorChecks. Declarado en BR y CL, pero en BR con isVisible: false — solo Chile lo renderiza. No abre nada.

O enum tem 80 valores no total (tiles da visita, indicadores de status, módulos da Home, etc.); os três acima são os que importam aqui. A lista completa está no doc do Início.The enum has 80 values in total (visit tiles, status indicators, Home modules, etc.); the three above are the ones that matter here. The full list is in the Home doc.El enum tiene 80 valores en total (tiles de la visita, indicadores de estado, módulos de la Home, etc.); los tres de arriba son los que importan aquí. La lista completa está en el doc de Inicio.

DispatcherType · competitorInsights serviceName · destination · mercadosmarketsmercados
caseserviceNamedestinationmercadosmarketsmercadosresendMayDuplicate
competitorInsightsCompetitorInsightsUploadAPIcompetitorZAtrue

Único canal de escrita da feature. Três fatos que valem registrar: (a) é o único dos 42 DispatcherType com destination: competitor — esse valor vira o campo endpoint do request de transporte; (b) o serviceName é resolvido com hasPromotion: false fixo, então o prefixo Promo_ é inalcançável aqui; (c) só 3 tipos estão no conjunto lightweight (leitura de notificação, resposta de tarefa e conferência de preço), logo este e os outros 39 são marcados como "reenvio pode duplicar". A relação completa está em Central de dados.The feature's only write channel. Three facts worth recording: (a) it is the only one of the 42 DispatcherTypes with destination: competitor — that value becomes the transport request's endpoint field; (b) the serviceName is resolved with a hardcoded hasPromotion: false, so the Promo_ prefix is unreachable here; (c) only 3 types are in the lightweight set (notification read, task answer and price check), so this one and the other 39 are flagged "resend may duplicate". The full list is in Data center.Único canal de escritura de la feature. Tres hechos que vale registrar: (a) es el único de los 42 DispatcherType con destination: competitor — ese valor se vuelve el campo endpoint del request de transporte; (b) el serviceName se resuelve con hasPromotion: false fijo, así que el prefijo Promo_ es inalcanzable aquí; (c) solo 3 tipos están en el conjunto lightweight (lectura de notificación, respuesta de tarea y verificación de precio), por lo que este y los otros 39 están marcados como "el reenvío puede duplicar". La lista completa está en Centro de datos.

DispatcherDestination 4 valores · todosvalues · all of themvalores · todos
casevalueusouseuso
salesforce"salesforce"o default do construtor do DispatcherType.the DispatcherType constructor's default.el default del constructor del DispatcherType.
batchApi"sfbatchapi"usado por conferência de preço, notas e outros.used by price check, notes and others.usado por verificación de precio, notas y otros.
competitor"competitor"exclusivo desta feature.exclusive to this feature.exclusivo de esta feature.
none""tipos sem endpoint dedicado (aprovações do Conecta Você).types with no dedicated endpoint (Conecta Você approvals).tipos sin endpoint dedicado (aprobaciones de Conecta Você).

Enum sem parser e sem fallback — o valor é sempre escolhido em tempo de compilação pelo DispatcherType.An enum with no parser and no fallback — the value is always chosen at compile time by the DispatcherType.Enum sin parser y sin fallback — el valor siempre lo elige en tiempo de compilación el DispatcherType.

DataSyncType · competitorInsights key · mercados · lotemarkets · batchmercados · lote
casekeymercadosmarketsmercadoslotebatchlote
competitorInsights"competitorInsights"ZASyncBatch.rest

A key é o nome da chave de TTL no arquivo de configuração de mercado (ttlSecondsByType.competitorInsights) — camelCase, diferente do competitor_insights em snake_case do tile. O fromKey deste enum é o único parser envolvido na feature que devolve null em vez de um sentinela, e não registra log de valor não mapeado. O enum tem 26 valores; a matriz completa está em Central de dados.The key is the TTL key name in the market configuration file (ttlSecondsByType.competitorInsights) — camelCase, unlike the tile's snake_case competitor_insights. This enum's fromKey is the only parser involved in the feature that returns null instead of a sentinel, and it does not record an unmapped-value log. The enum has 26 values; the full matrix is in Data center.La key es el nombre de la clave de TTL en el archivo de configuración de mercado (ttlSecondsByType.competitorInsights) — camelCase, distinto del competitor_insights en snake_case del tile. El fromKey de este enum es el único parser involucrado en la feature que devuelve null en vez de un centinela, y no registra log de valor no mapeado. El enum tiene 26 valores; la matriz completa está en Centro de datos.

CaptureSource 2 valores · todosvalues · all of themvalores · todos
casemapeia paramaps tomapea aalcançável nesta tela?reachable on this screen?¿alcanzable en esta pantalla?
cameraImageSource.camerasim — é o único valor que a faixa de fotos passa.yes — the only value the photos strip passes. — el único valor que pasa la franja de fotos.
galleryImageSource.gallerynão — existe um método pickPhotoFromGallery() no Notifier, mas nenhum widget o chama (ver Pendências).no — a pickPhotoFromGallery() method exists on the Notifier, but no widget calls it (see Pending items).no — existe un método pickPhotoFromGallery() en el Notifier, pero ningún widget lo llama (ver Pendientes).

Enum sem value de wire e sem parser — é só um seletor interno do serviço de captura. O irmão AllowedFileType (anexo de documento) não é usado aqui: esta tela só aceita imagem.An enum with no wire value and no parser — just an internal selector for the capture service. Its sibling AllowedFileType (document attachment) is not used here: this screen only accepts images.Enum sin value de wire y sin parser — es solo un selector interno del servicio de captura. Su hermano AllowedFileType (adjunto de documento) no se usa aquí: esta pantalla solo acepta imagen.

DateFormatType 26 valores · 2 usados aquivalues · 2 used herevalores · 2 usados aquí
casepatternondewheredónde
dayMonthYearSlashdd/MM/yyyyos campos startDate e endDate do payload da transação — formato de exibição usado no wire, herdado do legado. Descarta a hora.the transaction payload's startDate and endDate fields — a display format used on the wire, inherited from the legacy app. Time of day is dropped.los campos startDate y endDate del payload de la transacción — formato de exhibición usado en el wire, heredado del legado. Descarta la hora.
isoDateyyyy-MM-ddo dateReference do envelope, a partir do submittedAt. É o default do formatDate, aplicado por omissão do parâmetro.the envelope's dateReference, from submittedAt. It is formatDate's default, applied by omitting the parameter.el dateReference del sobre, a partir del submittedAt. Es el default del formatDate, aplicado por omisión del parámetro.

O enum tem 26 patterns; só estes dois entram nesta feature. Na tela, os cartões de data não usam este enum — usam formatLocaleDate, que resolve pelo locale ativo (ver Pendências).The enum has 26 patterns; only these two are involved in this feature. On screen the date cards do not use this enum — they use formatLocaleDate, which resolves by the active locale (see Pending items).El enum tiene 26 patterns; solo estos dos entran en esta feature. En la pantalla, las tarjetas de fecha no usan este enum — usan formatLocaleDate, que resuelve por el locale activo (ver Pendientes).

11

UseCases

Um dropdown por UseCase; dentro, cada método com assinatura, o que retorna e uso. São 3 próprios da feature (1 de leitura + 2 de escrita) e 1 emprestado de Visitas.One dropdown per UseCase; inside, each method with its signature, what it returns and use. There are 3 of the feature's own (1 read + 2 write) and 1 borrowed from Visits.Un dropdown por UseCase; dentro, cada método con su firma, qué devuelve y uso. Son 3 propios de la feature (1 de lectura + 2 de escritura) y 1 prestado de Visitas.

LeituraReadLectura

GetCompetitorInsightOptionsUseCase 3 métodosmethodsmétodos
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
execute({source = DataSourceType.local})Result<CompetitorInsightOptionsEntity, Failure>fonte da tela. Repasse fino ao repository. Chamado pelo build() do Notifier sem argumento (logo, cache-first) e pelo sweep de frescor com source forçado (remoto, ou mock em sessão de mock).the screen's source. A thin passthrough to the repository. Called by the Notifier's build() with no argument (hence cache-first) and by the freshness sweep with a forced source (remote, or mock in a mock session).fuente de la pantalla. Repase fino al repository. Llamado por el build() del Notifier sin argumento (por eso, cache-first) y por el sweep de frescura con source forzado (remoto, o mock en sesión de mock).
getCached()Result<CompetitorInsightOptionsEntity?, Failure>cache-only. Nenhum consumidor hoje — o Notifier usa o execute() com o default local.cache-only. No consumer today — the Notifier uses execute() with the local default.cache-only. Ningún consumidor hoy — el Notifier usa el execute() con el default local.
getCachedLastSyncAt()Future<DateTime?>consumido pelo alvo de sincronização do orquestrador de frescor, para decidir se as opções estão obsoletas (TTL de 24 h).consumed by the freshness orchestrator's sync target, to decide whether the options are stale (24 h TTL).consumido por el objetivo de sincronización del orquestador de frescura, para decidir si las opciones están obsoletas (TTL de 24 h).

Não se encaixa nem na Categoria A nem na B do §28: as opções são um lookup global de uma linha por representante, sem busca por sfid e sem variação por varejo. Segue a forma de 3 operações do padrão de agregado sincronizado, com os nomes curtos (getCached, getCachedLastSyncAt) que o alvo do orquestrador exige.It fits neither §28's Category A nor B: the options are a single-row global lookup per rep, with no sfid lookup and no per-retail variation. It follows the 3-operation shape of the synced-aggregate pattern, with the short names (getCached, getCachedLastSyncAt) the orchestrator's target requires.No encaja ni en la Categoría A ni en la B del §28: las opciones son un lookup global de una fila por representante, sin búsqueda por sfid y sin variación por punto de venta. Sigue la forma de 3 operaciones del patrón de agregado sincronizado, con los nombres cortos (getCached, getCachedLastSyncAt) que el objetivo del orquestador exige.

GetVisitsUseCase de Visitasfrom Visitsde Visitas 1 · usado no envioused on submitusado en el envío
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
getCachedBySfid({visitSfid})Result<VisitEntity, Failure>chamado no envio para traduzir o visitSfid (que a rota carrega) no accountSfid do varejo. É lookup de item único por sfid, cache-only por desenho (§28, Categoria A) — não dispara rede. Se a visita não estiver em cache, o envio aborta com falha genérica.called on submit to translate the visitSfid (carried by the route) into the retail's accountSfid. It is a single-item lookup by sfid, cache-only by design (§28, Category A) — it fires no network. If the visit isn't cached, the submission aborts with a generic failure.llamado en el envío para traducir el visitSfid (que lleva la ruta) al accountSfid del punto de venta. Es lookup de ítem único por sfid, cache-only por diseño (§28, Categoría A) — no dispara red. Si la visita no está en caché, el envío aborta con falla genérica.

EscritaWriteEscritura

BuildCompetitorInsightsDispatcherPayloadUseCase 1 · builder · 13 chaveskeysclaves
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
build({input: CompetitorInsightsDispatcherPayloadInput})DispatcherEnvelopeimplements DispatcherPayloadBuilder (§36), sem dependência nenhuma — é const e puro. Toda construção wire mora aqui: a escolha primary/secondary do representante, o trim() da descrição, a formatação das duas datas e do dateReference, e os quatro campos fixos vazios.implements DispatcherPayloadBuilder (§36), with no dependency at all — it is const and pure. All wire construction lives here: the rep's primary/secondary choice, the description's trim(), the formatting of both dates and of the dateReference, and the four fixed empty fields.implements DispatcherPayloadBuilder (§36), sin ninguna dependencia — es const y puro. Toda la construcción wire vive aquí: la elección primary/secondary del representante, el trim() de la descripción, el formateo de las dos fechas y del dateReference, y los cuatro campos fijos vacíos.

O envelopeThe envelopeEl sobre

type
DispatcherType.competitorInsightsfixo.fixed.fijo.
serviceName
CompetitorInsightsUploadAPIresolvido com hasPromotion: false fixo.resolved with a hardcoded hasPromotion: false.resuelto con hasPromotion: false fijo.
payload
um único wrapper {"CompetitorInsightsUploadAPI": {…}} com as 13 chaves da tabela abaixo.a single {"CompetitorInsightsUploadAPI": {…}} wrapper with the 13 keys in the table below.un único wrapper {"CompetitorInsightsUploadAPI": {…}} con las 13 claves de la tabla de abajo.
account
DispatchAccountEntity(sfid: accountSfid)só o sfid; é por este campo que o registro em erro é contado nas pendências de encerramento de visita.the sfid only; it is through this field that an errored record is counted in the visit-end pending items.solo el sfid; es por este campo que el registro en error se cuenta en los pendientes de cierre de visita.
transactionReference
accountSfidum identificador de domínio, não um UUID gerado. Dois insights do mesmo varejo compartilham a mesma referência (ver Pendências).a domain identifier, not a generated UUID. Two insights for the same retail share the same reference (see Pending items).un identificador de dominio, no un UUID generado. Dos insights del mismo punto de venta comparten la misma referencia (ver Pendientes).
dateReference
submittedAt formatado como yyyy-MM-dd (default do formatDate).submittedAt formatted as yyyy-MM-dd (formatDate's default).submittedAt formateado como yyyy-MM-dd (default del formatDate).
tid
0 — o builder não o seta; é o default do envelope. O reenvio manual é que reusa o tid gravado.0 — the builder doesn't set it; it is the envelope's default. It is the manual resend that reuses the stored tid.0 — el builder no lo setea; es el default del sobre. Es el reenvío manual el que reusa el tid grabado.

O JSON — 13 chavesThe JSON — 13 keysEl JSON — 13 claves

Campo JSONJSON fieldCampo JSONTipoTypeTipoOrigem do dadoData sourceOrigen del datoRegra / observaçãoRule / noteRegla / observación
repAccountIdStringResourceEntityisPrimaryResource ? primaryResourceSfid : secondaryResourceSfid — derivado no build().isPrimaryResource ? primaryResourceSfid : secondaryResourceSfid — derived inside build().isPrimaryResource ? primaryResourceSfid : secondaryResourceSfid — derivado en el build().
retailerIdStringinput.accountSfidrepasse. Origem a montante: visit.accountData.sfid do cache.passthrough. Upstream origin: visit.accountData.sfid from cache.repase. Origen aguas arriba: visit.accountData.sfid del caché.
companyIdStringinput.company.sfido builder extrai o sfid da entity crua.the builder extracts the sfid from the raw entity.el builder extrae el sfid de la entity cruda.
activityIdStringinput.activity.sfididem.same.ídem.
brandIdStringliteral fixofixed literalliteral fijosempre "" — o app não tem campo de marca (ver Pendências).always "" — the app has no brand field (see Pending items).siempre "" — la app no tiene campo de marca (ver Pendientes).
brandVariantIdsList<String>literal fixofixed literalliteral fijosempre [].always [].siempre [].
brandVersionIdsList<String>literal fixofixed literalliteral fijosempre [].always [].siempre [].
descriptionStringinput.description.trim() aplicado aqui, não no Notifier. Sem limite de tamanho..trim() applied here, not in the Notifier. No length limit..trim() aplicado aquí, no en el Notifier. Sin límite de tamaño.
startDateStringinput.startDateformatado como dd/MM/yyyy; a hora é descartada.formatted as dd/MM/yyyy; time of day is dropped.formateado como dd/MM/yyyy; la hora se descarta.
endDateStringinput.endDateidem dd/MM/yyyy.same, dd/MM/yyyy.ídem dd/MM/yyyy.
imagesList<String>input.imagesBase64repasse. 1 a 3 strings base64 puras (sem prefixo data URI), inline no JSON.passthrough. 1 to 3 plain base64 strings (no data URI prefix), inline in the JSON.repase. 1 a 3 strings base64 puras (sin prefijo data URI), inline en el JSON.
answersList<Object>literal fixofixed literalliteral fijosempre [] — o formulário dinâmico de itens de atividade do legado não foi modelado.always [] — the legacy app's dynamic activity-item form was not modelled.siempre [] — el formulario dinámico de ítems de actividad del legado no fue modelado.
isCriticalboolinput.isCriticalrepasse do toggle. O app não deriva nada dele.passthrough of the toggle. The app derives nothing from it.repase del toggle. La app no deriva nada de él.

O detalhamento completo da transação (incluindo exemplo de JSON) vive em 13 · CompetitorInsightsUploadAPI.The transaction's full breakdown (including a JSON example) lives in 13 · CompetitorInsightsUploadAPI.El detalle completo de la transacción (incluyendo ejemplo de JSON) vive en 13 · CompetitorInsightsUploadAPI.

SubmitCompetitorInsightsUseCase 1 · transportetransporttransporte
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
submit({envelope})Result<DispatcherAck, Failure>Delegate de transporte fino: encaminha o envelope ao DispatcherOrchestrator.dispatch. Um único envelope por envio — sem fatiamento e sem Future.wait, mesmo com 3 fotos. Não decide DispatcherType nem monta payload (isso é do builder).A thin transport delegate: forwards the envelope to DispatcherOrchestrator.dispatch. One single envelope per submission — no chunking and no Future.wait, even with 3 photos. It decides neither the DispatcherType nor the payload (that's the builder's job).Delegate de transporte fino: reenvía el sobre a DispatcherOrchestrator.dispatch. Un único sobre por envío — sin corte y sin Future.wait, incluso con 3 fotos. No decide DispatcherType ni arma payload (eso es del builder).

No orquestrador, o envelope não é enfileirado quando o aparelho está offline (a fila atende só o tipo de visita): ele vai ao transporte, falha e é gravado com status de erro, com o payload preservado — o que permite o reenvio manual. No sucesso, o payload é anulado no registro.In the orchestrator the envelope is not queued when the device is offline (the queue only serves the visit type): it goes to transport, fails and is stored with an error status, with the payload preserved — which is what makes the manual resend possible. On success the payload is nulled in the record.En el orquestador, el sobre no se encola cuando el dispositivo está offline (la cola atiende solo el tipo de visita): va al transporte, falla y se graba con estado de error, con el payload preservado — lo que permite el reenvío manual. En el éxito, el payload se anula en el registro.

Input cru (§36)Raw input (§36)Input crudo (§36)O CompetitorInsightsDispatcherPayloadInput carrega entities cruas (ResourceEntity, CompetitorCompanyEntity, CompetitorActivityEntity), as duas datas como DateTime, a descrição sem aparar e o relógio como submittedAt: DateTime — nunca uma data já formatada. A única pré-resolução é o imagesBase64, exceção explícita do padrão porque o builder precisa ser puro (sem I/O).The CompetitorInsightsDispatcherPayloadInput carries raw entities (ResourceEntity, CompetitorCompanyEntity, CompetitorActivityEntity), both dates as DateTime, the description untrimmed and the clock as submittedAt: DateTime — never a pre-formatted date. The only pre-resolution is imagesBase64, an explicit exception to the pattern because the builder must stay pure (no I/O).El CompetitorInsightsDispatcherPayloadInput lleva entities crudas (ResourceEntity, CompetitorCompanyEntity, CompetitorActivityEntity), las dos fechas como DateTime, la descripción sin recortar y el reloj como submittedAt: DateTime — nunca una fecha ya formateada. La única pre-resolución es el imagesBase64, excepción explícita del patrón porque el builder debe ser puro (sin I/O).

12

Notifier & State

O CompetitorInsightsNotifier (@riverpod, family por visitSfid, autoDispose) é o cérebro da tela. É um Notifier de formulário: não tem refresh() nem _load() — a §37 dispensa o refresh() em telas de formulário sem pull-to-refresh, e o build() monta o State direto. O State (CompetitorInsightsState, Freezed, 13 campos + 2 getters) é a fonte única de verdade: guarda as opções disponíveis, tudo que o representante escolheu e os dois indicadores de "em andamento".CompetitorInsightsNotifier (@riverpod, family by visitSfid, autoDispose) is the screen's brain. It is a form Notifier: it has neither refresh() nor _load() — §37 waives refresh() for form screens with no pull-to-refresh, and build() assembles the State directly. The State (CompetitorInsightsState, Freezed, 13 fields + 2 getters) is the single source of truth: it holds the available options, everything the rep picked and the two "in progress" flags.El CompetitorInsightsNotifier (@riverpod, family por visitSfid, autoDispose) es el cerebro de la pantalla. Es un Notifier de formulario: no tiene refresh() ni _load() — el §37 dispensa el refresh() en pantallas de formulario sin pull-to-refresh, y el build() arma el State directo. El State (CompetitorInsightsState, Freezed, 13 campos + 2 getters) es la fuente única de verdad: guarda las opciones disponibles, todo lo que el representante eligió y los dos indicadores de "en curso".

MétodosMethodsMétodos

build({visitSfid}) cache-first

RetornoReturnRetorno FutureOr<CompetitorInsightsState>

Faz três coisas, nesta ordem: (1) abre uma sessão de captura de arquivo (um identificador novo) e registra o onDispose que apaga a pasta da sessão ao sair da tela; (2) observa o UseCase de opções e chama execute() — sem argumento, logo cache-first; (3) monta o State com visitSfid, lastSyncAt e as duas listas. Em Error lança BusinessFailure(genericError), o que leva a Page ao estado de erro com botão de tentar de novo.It does three things, in order: (1) opens a file capture session (a fresh identifier) and registers the onDispose that deletes the session folder on leaving the screen; (2) watches the options UseCase and calls execute() — with no argument, hence cache-first; (3) assembles the State with visitSfid, lastSyncAt and both lists. On Error it throws BusinessFailure(genericError), which takes the Page to the error state with a retry button.Hace tres cosas, en este orden: (1) abre una sesión de captura de archivo (un identificador nuevo) y registra el onDispose que borra la carpeta de la sesión al salir de la pantalla; (2) observa el UseCase de opciones y llama execute() — sin argumento, por lo tanto cache-first; (3) arma el State con visitSfid, lastSyncAt y las dos listas. En Error lanza BusinessFailure(genericError), lo que lleva la Page al estado de error con botón de reintentar.

É a única observação reativa do Notifier (ref.watch); todo o resto é ref.read, então nenhuma ação do formulário provoca reconstrução.It is the Notifier's only reactive watch (ref.watch); everything else is ref.read, so no form action triggers a rebuild.Es la única observación reactiva del Notifier (ref.watch); todo lo demás es ref.read, así que ninguna acción del formulario provoca reconstrucción.

selectCompany({company}) · selectActivity({activity})

RetornoReturnRetorno void

Cada um faz null-guard em state.value e grava a entity escolhida (selectedCompany / selectedActivity). Sem validação, sem efeito colateral: escolher a empresa não filtra as atividades.Each null-guards state.value and stores the chosen entity (selectedCompany / selectedActivity). No validation, no side effect: picking the company does not filter the activities.Cada uno hace null-guard en state.value y graba la entity elegida (selectedCompany / selectedActivity). Sin validación, sin efecto colateral: elegir la empresa no filtra las actividades.

setInitialDate({date}) com limpeza da data finalclears the final datecon limpieza de la fecha final

RetornoReturnRetorno void

Grava a data inicial e, se a data final já escolhida for anterior à nova inicial, zera a data final no mesmo copyWith. É a única normalização de intervalo do formulário — e força o representante a reescolher o fim.Stores the initial date and, if the already-chosen final date is earlier than the new initial one, clears the final date in the same copyWith. It is the form's only range normalisation — and it forces the rep to re-pick the end.Graba la fecha inicial y, si la fecha final ya elegida es anterior a la nueva inicial, limpia la fecha final en el mismo copyWith. Es la única normalización de intervalo del formulario — y obliga al representante a volver a elegir el fin.

setFinalDate({date}) rejeição silenciosasilent rejectionrechazo silencioso

RetornoReturnRetorno void

Se já existe data inicial e a data escolhida é anterior a ela, o método retorna sem fazer nada — sem aviso, sem mensagem. Da perspectiva de quem usa, o calendário fecha e o cartão continua igual.If an initial date already exists and the picked date is earlier than it, the method returns doing nothing — no notice, no message. From the user's perspective the calendar closes and the card stays the same.Si ya existe fecha inicial y la fecha elegida es anterior a ella, el método retorna sin hacer nada — sin aviso, sin mensaje. Desde la perspectiva de quien usa, el calendario cierra y la tarjeta sigue igual.

setDescription({value}) · setIsCritical({value})

RetornoReturnRetorno void

Gravam o valor cru. A descrição não é aparada aqui (só na checagem do canSave e no builder) e não tem limite de tamanho. O isCritical não dispara nada além da mudança de cor do rótulo.They store the raw value. The description is not trimmed here (only in the canSave check and in the builder) and has no length limit. isCritical triggers nothing beyond the label's colour change.Graban el valor crudo. La descripción no se recorta aquí (solo en la verificación del canSave y en el builder) y no tiene límite de tamaño. El isCritical no dispara nada más allá del cambio de color del rótulo.

capturePhoto({source}) câmera ou galeriacamera or gallerycámara o galería

RetornoReturnRetorno Future<void>

Três guardas na entrada: State nulo, já com 3 fotos e captura em andamento. Liga isCapturingPhoto, chama o serviço de captura com a sessão da tela, e ao voltar relê o State (o usuário pode ter mudado algo enquanto a câmera estava aberta). Captura cancelada → só desliga a flag. Exceção → é logada e a flag é desligada, sem nenhum aviso ao usuário.Three entry guards: null State, already 3 photos and capture in progress. It turns isCapturingPhoto on, calls the capture service with the screen's session, and on return re-reads the State (the user may have changed something while the camera was open). Cancelled capture → it only turns the flag off. Exception → it is logged and the flag turned off, with no notice to the user whatsoever.Tres guardas en la entrada: State nulo, ya con 3 fotos y captura en curso. Enciende isCapturingPhoto, llama al servicio de captura con la sesión de la pantalla, y al volver relee el State (el usuario puede haber cambiado algo mientras la cámara estaba abierta). Captura cancelada → solo apaga la flag. Excepción → se loguea y la flag se apaga, sin ningún aviso al usuario.

A compressão acontece dentro do serviço, não aqui: cada imagem é reencodada para JPEG numa cascata de 6 passos (1600/80, 1600/60, 1280/55, 1024/50, 800/45, 640/40), parando no primeiro resultado <= 300 KB. Se nenhum passo atingir, grava o menor obtido; se todos falharem, copia o original sem compressão — o alvo de 300 KB é best-effort, não um teto rígido.Compression happens inside the service, not here: each image is re-encoded to JPEG through a 6-step cascade (1600/80, 1600/60, 1280/55, 1024/50, 800/45, 640/40), stopping at the first result <= 300 KB. If no step gets there it writes the smallest obtained; if all fail it copies the original uncompressed — the 300 KB target is best-effort, not a hard ceiling.La compresión ocurre dentro del servicio, no aquí: cada imagen se recodifica a JPEG en una cascada de 6 pasos (1600/80, 1600/60, 1280/55, 1024/50, 800/45, 640/40), parando en el primer resultado <= 300 KB. Si ningún paso lo logra, graba el menor obtenido; si todos fallan, copia el original sin compresión — el objetivo de 300 KB es best-effort, no un techo rígido.

pickPhotoFromGallery() sem chamadorno callersin llamador

RetornoReturnRetorno Future<void>

Atalho de uma linha para capturePhoto(source: gallery). Nenhum widget o chama em todo o projeto — está documentado aqui porque explica por que o rótulo da faixa promete "escolher" uma foto que a tela não oferece (ver Pendências).A one-line shortcut to capturePhoto(source: gallery). No widget calls it anywhere in the project — it is documented here because it explains why the strip's label promises "choosing" a photo the screen doesn't offer (see Pending items).Atajo de una línea para capturePhoto(source: gallery). Ningún widget lo llama en todo el proyecto — está documentado aquí porque explica por qué el rótulo de la franja promete "escoger" una foto que la pantalla no ofrece (ver Pendientes).

removePhotoAt({index})

RetornoReturnRetorno Future<void>

Null-guard + checagem de limites do índice. Remove da lista e publica o State primeiro; só depois apaga o arquivo do disco. A ordem importa: a miniatura desaparece na hora, sem esperar o I/O.Null-guard + index bounds check. It removes from the list and publishes the State first; only then deletes the file from disk. The order matters: the thumbnail disappears at once, without waiting for the I/O.Null-guard + verificación de límites del índice. Quita de la lista y publica el State primero; solo después borra el archivo del disco. El orden importa: la miniatura desaparece al instante, sin esperar el I/O.

submit() o enviothe submissionel envío

RetornoReturnRetorno Future<Failure?> · null = sucessonull = successnull = éxito

Passo a passo: guarda de State nulo e de canSave (é ele que impede o reenvio, já que canSave é false enquanto isSaving) → liga isSaving → lê a ResourceEntity da sessão → resolve o accountSfid pela visita em cache → converte as fotos para base64 uma a uma, em série → monta o input e chama o builder → despacha → desliga isSaving e devolve a Failure (ou null).Step by step: null-State and canSave guards (it is canSave that blocks a resend, since it is false while isSaving) → turns isSaving on → reads the session's ResourceEntity → resolves the accountSfid from the cached visit → converts the photos to base64 one by one, serially → assembles the input and calls the builder → dispatches → turns isSaving off and returns the Failure (or null).Paso a paso: guarda de State nulo y de canSave (es él el que impide el reenvío, ya que canSave es false mientras isSaving) → enciende isSaving → lee la ResourceEntity de la sesión → resuelve el accountSfid por la visita en caché → convierte las fotos a base64 una a una, en serie → arma el input y llama al builder → despacha → apaga isSaving y devuelve la Failure (o null).

Duas rotas de aborto passam pelo privado _failSubmission(), que desliga isSaving e devolve UnknownFailure: representante ausente na sessão e visita ausente do cache. As checagens de empresa/atividade/datas nesse mesmo if são redundantes — o canSave já as garantiu. A Page traduz qualquer UnknownFailure na mensagem de erro padrão da feature.Two abort routes go through the private _failSubmission(), which turns isSaving off and returns an UnknownFailure: missing rep in the session and visit missing from cache. The company/activity/date checks in that same if are redundant — canSave already guaranteed them. The Page translates any UnknownFailure into the feature's default error message.Dos rutas de aborto pasan por el privado _failSubmission(), que apaga isSaving y devuelve UnknownFailure: representante ausente en la sesión y visita ausente del caché. Las verificaciones de empresa/actividad/fechas en ese mismo if son redundantes — el canSave ya las garantizó. La Page traduce cualquier UnknownFailure en el mensaje de error estándar de la feature.

State disponível para a PageState available to the PageState disponible para la Page

CompetitorInsightsState 13 campos + 2 gettersfields + 2 getterscampos + 2 getters
campotipodefault
visitSfidStringrequired
lastSyncAtDateTime?null
availableCompaniesList<CompetitorCompanyEntity>[]
availableActivitiesList<CompetitorActivityEntity>[]
selectedCompanyCompetitorCompanyEntity?null
selectedActivityCompetitorActivityEntity?null
initialDateDateTime?null
finalDateDateTime?null
descriptionString""
photosList<CapturedFileEntity>[]
isCriticalboolfalse
isCapturingPhotoboolfalse
isSavingboolfalse

Getters: canAddMorePhotos (photos.length < kCompetitorInsightsMaxPhotos, com a constante de módulo = 3) e canSave, que devolve false em cascata para isSaving, empresa nula, atividade nula, data inicial nula, data final nula, descrição aparada vazia e lista de fotos vazia. O canSave não olha o isCapturingPhoto — ver Pendências. O lastSyncAt exibido é o das opções da própria feature, nunca o do representante (§23).Getters: canAddMorePhotos (photos.length < kCompetitorInsightsMaxPhotos, with the module constant = 3) and canSave, which returns false in cascade for isSaving, null company, null activity, null initial date, null final date, empty trimmed description and an empty photo list. canSave does not look at isCapturingPhoto — see Pending items. The displayed lastSyncAt is the feature's own options one, never the rep's (§23).Getters: canAddMorePhotos (photos.length < kCompetitorInsightsMaxPhotos, con la constante de módulo = 3) y canSave, que devuelve false en cascada para isSaving, empresa nula, actividad nula, fecha inicial nula, fecha final nula, descripción recortada vacía y lista de fotos vacía. El canSave no mira el isCapturingPhoto — ver Pendientes. El lastSyncAt exhibido es el de las opciones de la propia feature, nunca el del representante (§23).

13

Page e widgetsPage & widgetsPage y widgets

A CompetitorInsightsPage (ConsumerWidget) recebe só o visitSfid (§17), observa competitorInsightsProvider(visitSfid:) e delega o corpo a um _CompetitorInsightsBody privado. Loading e erro são globais (asyncState.when): erro → FailureStateView com retry por ref.invalidate. São 5 widgets em widgets/ + 2 privados; não existe pasta widgets/modals/ — os dois modais da tela são componentes compartilhados.CompetitorInsightsPage (ConsumerWidget) takes only the visitSfid (§17), watches competitorInsightsProvider(visitSfid:) and delegates the body to a private _CompetitorInsightsBody. Loading and error are global (asyncState.when): error → FailureStateView with retry via ref.invalidate. There are 5 widgets in widgets/ + 2 private ones; there is no widgets/modals/ folder — the screen's two modals are shared components.La CompetitorInsightsPage (ConsumerWidget) recibe solo el visitSfid (§17), observa competitorInsightsProvider(visitSfid:) y delega el cuerpo a un _CompetitorInsightsBody privado. Loading y error son globales (asyncState.when): error → FailureStateView con retry por ref.invalidate. Son 5 widgets en widgets/ + 2 privados; no existe carpeta widgets/modals/ — los dos modales de la pantalla son componentes compartidos.

  • CompetitorInsightsPage
    • AppPageShell displayBackButton · fundo default do shellshell default backgroundfondo default del shell
      • CustomLoadingIndicator loading
      • FailureStateView error → ref.invalidate
      • _CompetitorInsightsBody → SingleChildScrollView → Column data
        • DataLoadInfo state.lastSyncAt
        • CompetitorInsightsHeaderWidget ícone + título · sem parâmetroicon + title · no parameterícono + título · sin parámetro
        • CustomDropdown<CompetitorCompanyEntity>.single options: availableCompanies · labelBuilder: company.name
          • CustomDropdown modal de opçõesoptions modalmodal de opciones devolve a escolha → selectCompanyreturns the choice → selectCompanydevuelve la elección → selectCompany
        • CustomDropdown<CompetitorActivityEntity>.single options: availableActivities · labelBuilder: activity.name
          • CustomDropdown modal de opçõesoptions modalmodal de opciones devolve a escolha → selectActivityreturns the choice → selectActivitydevuelve la elección → selectActivity
        • CompetitorInsightsPeriodSectionWidget rótulo + 2 cartõeslabel + 2 cardsrótulo + 2 tarjetas
          • _DateCard inicial · data por locale ou "—"initial · locale date or "—"inicial · fecha por locale o "—"
            • ConectaModal.show<DateTime> → CustomCalendarModalContent calendário sem limite → setInitialDateunbounded calendar → setInitialDatecalendario sin límite → setInitialDate
          • _DateCard finalfinalfinal
            • ConectaModal.show<DateTime> → CustomCalendarModalContent mesmo calendário → setFinalDatesame calendar → setFinalDatemismo calendario → setFinalDate
        • CompetitorInsightsDescriptionFieldWidget o único stateful da featurethe feature's only stateful widgetel único stateful de la feature
          • TextField minLines 3 · maxLines 4 · onChanged → setDescription
        • CompetitorInsightsPhotosSectionWidget
          • InkWell faixa de câmeracamera stripfranja de cámara canAddMorePhotos && !isCapturingPhoto → capturePhoto(camera)
          • Wrap → CustomFileThumbnail só se houver foto · onRemove → removePhotoAt(index)only when there is a photo · onRemove → removePhotoAt(index)solo si hay foto · onRemove → removePhotoAt(index)
        • CompetitorInsightsCriticalToggleWidget
          • CustomSwitch → setIsCritical
        • CustomButton filled · stadium · enable: canSave · loading: isSaving → submit()

Fluxo do envio na Page: o toque no botão chama _onSendPressed, que só aciona o método do Notifier e depois trata a UI — sucesso → ConectaNotice.success + AppRouter.back; falha → ConectaNotice.error com a Failure e a mensagem padrão da feature como reserva (§30). O disparo do envio mora no Notifier (§39); modal e navegação ficam no widget porque precisam de BuildContext. Componentes compartilhados reusados (§31): AppPageShell, CustomLoadingIndicator, FailureStateView, DataLoadInfo, CustomDropdown, ConectaModal, CustomCalendarModalContent, CustomFileThumbnail, CustomSwitch, CustomButton, CustomText, CustomIcon e ConectaNotice — nenhum deles reimplementado localmente. Não há ConectaPullToRefresh, CustomEmptyState nem ConectaSummaryBar nesta tela, e nenhum deles faria sentido num formulário de envio único.Submission flow on the Page: the button tap calls _onSendPressed, which only invokes the Notifier's method and then handles the UI — success → ConectaNotice.success + AppRouter.back; failure → ConectaNotice.error with the Failure and the feature's default message as a fallback (§30). The submission trigger lives in the Notifier (§39); modal and navigation stay in the widget because they need a BuildContext. Reused shared components (§31): AppPageShell, CustomLoadingIndicator, FailureStateView, DataLoadInfo, CustomDropdown, ConectaModal, CustomCalendarModalContent, CustomFileThumbnail, CustomSwitch, CustomButton, CustomText, CustomIcon and ConectaNotice — none of them reimplemented locally. There is no ConectaPullToRefresh, CustomEmptyState or ConectaSummaryBar on this screen, and none of them would make sense in a single-submission form.Flujo del envío en la Page: el toque del botón llama _onSendPressed, que solo acciona el método del Notifier y luego trata la UI — éxito → ConectaNotice.success + AppRouter.back; falla → ConectaNotice.error con la Failure y el mensaje estándar de la feature como reserva (§30). El disparo del envío vive en el Notifier (§39); modal y navegación quedan en el widget porque necesitan BuildContext. Componentes compartidos reutilizados (§31): AppPageShell, CustomLoadingIndicator, FailureStateView, DataLoadInfo, CustomDropdown, ConectaModal, CustomCalendarModalContent, CustomFileThumbnail, CustomSwitch, CustomButton, CustomText, CustomIcon y ConectaNotice — ninguno reimplementado localmente. No hay ConectaPullToRefresh, CustomEmptyState ni ConectaSummaryBar en esta pantalla, y ninguno tendría sentido en un formulario de envío único.

Notas por mercadoMarket notesNotas por mercado

A feature é dirigida por configuração de mercado (End Market Configuration) em um único ponto: a chave competitor_insights na grade de ferramentas do detalhe da visita. Ela existe apenas na África do Sul. Os dois gates de código — o tipo de sincronização e o tipo de transação — declaram o mesmo mercado único.The feature is driven by market configuration (End Market Configuration) at one single point: the competitor_insights key in the visit-detail tools grid. It exists only in South Africa. Both code gates — the sync type and the transaction type — declare the same single market.La feature se rige por configuración de mercado (End Market Configuration) en un único punto: la clave competitor_insights en la grilla de herramientas del detalle de la visita. Existe solo en Sudáfrica. Los dos gates de código — el tipo de sincronización y el tipo de transacción — declaran el mismo mercado único.

BR CL ZAx AR PY PE
disponívelavailabledisponible presente, desligadopresent, offpresente, apagado ausenteabsentausente

Todos os cinco mercados sem a feature são ausência de chave, não desligamento: em BR e CL a chave competitor_insights simplesmente não é declarada na grade, e AR/PY/PE não têm o bloco visitDetailConfig. Não existe nenhum isVisible: false para esta ferramenta em mercado algum. Os três arquivos de configuração (produção, UAT e pré-produção) são idênticos para tudo que diz respeito a esta feature — a varredura chave-a-chave achou 4 divergências prod↔UAT e 7 prod↔pré-produção, nenhuma tocando as chaves de concorrência.All five markets without the feature are a missing key, not a switch-off: in BR and CL the competitor_insights key is simply not declared in the grid, and AR/PY/PE have no visitDetailConfig block at all. There is no isVisible: false for this tool in any market. All three configuration files (production, UAT and pre-production) are identical for everything concerning this feature — the key-by-key sweep found 4 divergences prod↔UAT and 7 prod↔pre-production, none touching the competitor keys.Los cinco mercados sin la feature son ausencia de clave, no apagado: en BR y CL la clave competitor_insights simplemente no se declara en la grilla, y AR/PY/PE no tienen el bloque visitDetailConfig. No existe ningún isVisible: false para esta herramienta en mercado alguno. Los tres archivos de configuración (producción, UAT y preproducción) son idénticos para todo lo que concierne a esta feature — el barrido clave por clave encontró 4 divergencias prod↔UAT y 7 prod↔preproducción, ninguna tocando las claves de competencia.

Gate na grade de ferramentas da visitaGate in the visit tools gridGate en la grilla de herramientas de la visita

ChaveKeyClaveBRCLZAARPYPE
visitDetailConfigxxx
modules[visit_detail_tools_grid]xxx
↳↳ isVisibletruetruetrue
↳↳ details[]total de tilestile counttotal de tiles9109
↳↳↳ details[competitor_insights].isVisibletrue
↳↳↳ posição do tiletile positionposición del tile8/9
↳↳↳ details[competitor_actions].isVisibletrue
↳↳↳ posição do tiletile positionposición del tile9/9

Cada detalhe de tile tem exatamente 2 chaves no arquivo — moduleDetailName e isVisible. Não há name, icon nem order: a ordem é posicional e ícone e rótulo vêm do código. É a mesma forma que a grade usa para Pesquisas e para as outras ferramentas da visita.Each tile detail has exactly 2 keys in the file — moduleDetailName and isVisible. There is no name, icon or order: the order is positional and icon and label come from code. It is the same shape the grid uses for Surveys and the other visit tools.Cada detalle de tile tiene exactamente 2 claves en el archivo — moduleDetailName e isVisible. No hay name, icon ni order: el orden es posicional y el ícono y el rótulo vienen del código. Es la misma forma que la grilla usa para Encuestas y para las otras herramientas de la visita.

Frescor do dado e gates de códigoData freshness and code gatesFrescura del dato y gates de código

ChaveKeyClaveBRCLZAARPYPE
dataFreshnessConfigxxx
ttlSecondsByType.competitorInsights864008640086400
ttlSecondsByTypetotal de chaveskey counttotal de claves181818
defaultTtlSeconds300300300
sweepIntervalSeconds606060
DataSyncType.competitorInsights.enabledMarketsx
DispatcherType.competitorInsights.enabledMarketsx
visitEndConfig.categories[]categoria de concorrênciacompetitor categorycategoría de competencia

24 h de TTL põe as opções na faixa mais lenta do app (288× o default de 5 min), junto com catálogo de produto, material de merchandising, FAQ, dados de referência e calculadora de margem — coerente com um lookup que quase nunca muda. Duas leituras importantes: (a) BR e CL declaram o TTL de competitorInsights sem ter a feature — configuração morta, já que o tipo de sincronização é ZA-only e o tile não existe lá; (b) nenhum mercado declara categoria de concorrência no encerramento de visita. Um insight que falhou é contado no encerramento apenas pela categoria genérica unsynced_transactions — que existe em BR, CL e ZA e é não bloqueante nos três: a contagem casa por account.sfid (que o envelope preenche) e por "status diferente de sucesso", então o registro em erro é visto, só não impede fechar a visita.A 24 h TTL puts the options in the app's slowest tier (288× the 5-minute default), alongside product catalog, merchandising material, FAQ, reference data and margin calculator — coherent for a lookup that almost never changes. Two important readings: (a) BR and CL declare the competitorInsights TTL without having the feature — dead configuration, since the sync type is ZA-only and the tile doesn't exist there; (b) no market declares a competitor category at visit end. A failed insight is counted at visit end only through the generic unsynced_transactions category — present in BR, CL and ZA and non-blocking in all three: the count matches by account.sfid (which the envelope fills) and by "status other than success", so the errored record is seen, it just doesn't prevent closing the visit.24 h de TTL pone las opciones en la franja más lenta de la app (288× el default de 5 min), junto con catálogo de producto, material de merchandising, FAQ, datos de referencia y calculadora de margen — coherente con un lookup que casi nunca cambia. Dos lecturas importantes: (a) BR y CL declaran el TTL de competitorInsights sin tener la feature — configuración muerta, ya que el tipo de sincronización es ZA-only y el tile no existe allí; (b) ningún mercado declara categoría de competencia en el cierre de visita. Un insight que falló se cuenta en el cierre solo por la categoría genérica unsynced_transactions — que existe en BR, CL y ZA y es no bloqueante en los tres: el conteo casa por account.sfid (que el sobre completa) y por "estado distinto de éxito", así que el registro en error se ve, solo no impide cerrar la visita.

Mocks por mercadoMocks per marketMocks por mercado

Arquivo · conteúdoFile · contentArchivo · contenidoBRCLZAARPYPE
{mercado}_competitor_insights.jsonxxxxxx
companies / activities4 / 53 / 45 / 6{}{}{}
tamanhosizetamaño596 B488 B635 B3 B3 B3 B
{mercado}_real_competitor_insights.jsonx
companies / activities19 / 19
tamanhosizetamaño2 432 B

O arquivo sintético existe para os seis mercados, mas só BR, CL e ZA têm conteúdo; os de AR, PY e PE são {} — os três são byte-idênticos (mesmo hash), objetos vazios de propósito para o carregador não falhar. O único arquivo de mock real é o da África do Sul: 19 empresas e 19 atividades com identificadores numéricos de backend (por exemplo 1 · PMI, 5 · JTI, 11 · 2 Pack Deal, 12 · Carton Price Deal), tratados como dado real (§38). Os sintéticos usam identificadores fabricados (ci-co-… / ci-ac-…). Com a flag de mock real ligada, o caminho normal usa o arquivo _real_; fora de ZA ele não existe e o carregador devolve {} em silêncio — nada falha, os seletores só ficam vazios.The synthetic file exists for all six markets, but only BR, CL and ZA have content; AR, PY and PE are {} — the three are byte-identical (same hash), empty objects on purpose so the loader doesn't fail. The only real mock file is South Africa's: 19 companies and 19 activities with numeric backend identifiers (for instance 1 · PMI, 5 · JTI, 11 · 2 Pack Deal, 12 · Carton Price Deal), treated as real data (§38). The synthetic ones use fabricated identifiers (ci-co-… / ci-ac-…). With the real-mock flag on, the normal path uses the _real_ file; outside ZA it doesn't exist and the loader silently returns {} — nothing fails, the selectors are just empty.El archivo sintético existe para los seis mercados, pero solo BR, CL y ZA tienen contenido; los de AR, PY y PE son {} — los tres son byte-idénticos (mismo hash), objetos vacíos a propósito para que el cargador no falle. El único archivo de mock real es el de Sudáfrica: 19 empresas y 19 actividades con identificadores numéricos de backend (por ejemplo 1 · PMI, 5 · JTI, 11 · 2 Pack Deal, 12 · Carton Price Deal), tratados como dato real (§38). Los sintéticos usan identificadores fabricados (ci-co-… / ci-ac-…). Con la flag de mock real activa, el camino normal usa el archivo _real_; fuera de ZA no existe y el cargador devuelve {} en silencio — nada falla, los selectores solo quedan vacíos.

TraduçõesTranslationsTraducciones

As 19 chaves envolvidas (17 da tela + 2 rótulos de tile) existem nos seis mercados — zero violação da §10. Os quatro mercados de espanhol têm textos idênticos entre si. A tabela lista cada chave com o texto real, porque a assimetria interessante é semântica, não de ausência.The 19 keys involved (17 for the screen + 2 tile labels) exist in all six markets — zero §10 violations. The four Spanish markets have identical texts among themselves. The table lists each key with its real text, because the interesting asymmetry is semantic, not about absence.Las 19 claves involucradas (17 de la pantalla + 2 rótulos de tile) existen en los seis mercados — cero violación del §10. Los cuatro mercados de español tienen textos idénticos entre sí. La tabla lista cada clave con el texto real, porque la asimetría interesante es semántica, no de ausencia.

ChaveKeyClaveBRCL · AR · PY · PEZA
competitor_insights_titleCompetidoresCompetidoresCompetitor Insights
competitor_insights_company_labelEmpresaEmpresaCompany
competitor_insights_company_placeholderSelecione uma empresaSeleccione una empresaSelect a company
competitor_insights_activity_labelAtividadeActividadActivity
competitor_insights_activity_placeholderSelecione uma atividadeSeleccione una actividadSelect an activity
competitor_insights_period_labelPeríodoPeriodoPeriod
competitor_insights_initial_date_labelData inicialFecha inicialInitial date
competitor_insights_final_date_labelData finalFecha finalFinal date
competitor_insights_description_labelDescriçãoDescripciónDescription
competitor_insights_description_placeholderDescreva a atividade realizada pelo Concorrente.Describe la actividad realizada por el Competidor.Describe the activity being carried out by the Competition.
competitor_insights_photos_labelFotosFotosPhotos
competitor_insights_photos_helperEscolha ou Tire Foto (até 3 imagens)Escoge o Toma Foto (hasta 3 imágenes)Choose or Take Photo (Up to 3 images)
competitor_insights_critical_labelCríticoCríticoCritical
competitor_insights_send_buttonEnviarEnviarSend
competitor_insights_confirm_buttonConfirmarConfirmarConfirm
competitor_insights_submit_confirmationInformação enviada com sucessoInformación enviada con éxitoCompetitor insight sent successfully
competitor_insights_submit_error_messageErro ao enviar a informação. Tente novamente.Error al enviar la información. Inténtalo de nuevo.Error sending the information. Please try again.
visit_detail_competitor_insightsCompetidoresCompetidoresCompetitor Insights
visit_detail_competitor_actionsPesquisa de ConcorrênciaAcciones de la CompetenciaCompetitor Actions

Três observações. (1) Cobertura completa sem alcance: as 17 chaves da tela estão nos seis mercados, mas só ZA consegue renderizá-las — as outras cinco são texto inalcançável. (2) O nome do conceito divergiu: BR e os quatro de espanhol dizem "Competidores" onde ZA diz "Competitor Insights". (3) O rótulo do tile de BR é o mais honesto do conjunto: "Pesquisa de Concorrência" descreve corretamente o destino real daquele tile (a tela de Pesquisas), enquanto ZA e espanhol traduzem "Competitor Actions" literalmente — e BR é o único mercado que renderiza essa chave. Chave faltante resolve para o próprio nome da chave em tela, sem exceção nem log — mas nenhuma delas falta aqui.Three observations. (1) Full coverage without reach: the screen's 17 keys are in all six markets, but only ZA can render them — the other five are unreachable text. (2) The concept name diverged: BR and the four Spanish markets say "Competidores" where ZA says "Competitor Insights". (3) BR's tile label is the most honest of the set: "Pesquisa de Concorrência" correctly describes that tile's actual destination (the Surveys screen), while ZA and Spanish translate "Competitor Actions" literally — and BR is the only market that renders that key. A missing key resolves to the key name itself on screen, with no exception and no log — but none of these is missing.Tres observaciones. (1) Cobertura completa sin alcance: las 17 claves de la pantalla están en los seis mercados, pero solo ZA logra renderizarlas — las otras cinco son texto inalcanzable. (2) El nombre del concepto divergió: BR y los cuatro de español dicen "Competidores" donde ZA dice "Competitor Insights". (3) El rótulo del tile de BR es el más honesto del conjunto: "Pesquisa de Concorrência" describe correctamente el destino real de ese tile (la pantalla de Encuestas), mientras ZA y español traducen "Competitor Actions" literalmente — y BR es el único mercado que renderiza esa clave. Una clave faltante resuelve al propio nombre de la clave en pantalla, sin excepción ni log — pero ninguna de estas falta.

ZA

O único mercado com a featureThe only market with the featureEl único mercado con la feature É o único mercado que declara competitor_insights na grade da visita (8º de 9 tiles), o único no enabledMarkets do tipo de sincronização e do tipo de transação, e o único com mock real (19 empresas × 19 atividades, com identificadores de backend). É também o único cujos textos nomeiam o conceito como "Competitor Insights". Na grade de ZA a ferramenta convive com conferência de preço e calculadora de margem — as três exclusivas deste mercado. It is the only market declaring competitor_insights in the visit grid (8th of 9 tiles), the only one in the sync type's and the transaction type's enabledMarkets, and the only one with a real mock (19 companies × 19 activities, with backend identifiers). It is also the only one whose texts name the concept "Competitor Insights". In ZA's grid the tool sits alongside price check and margin calculator — all three exclusive to this market. Es el único mercado que declara competitor_insights en la grilla de la visita (8.º de 9 tiles), el único en el enabledMarkets del tipo de sincronización y del tipo de transacción, y el único con mock real (19 empresas × 19 actividades, con identificadores de backend). Es también el único cuyos textos nombran el concepto como "Competitor Insights". En la grilla de ZA la herramienta convive con verificación de precio y calculadora de margen — las tres exclusivas de este mercado.

BR

Tem outra coisa com nome parecidoHas a different thing with a similar nameTiene otra cosa con nombre parecido O Brasil não tem esta feature: a chave competitor_insights não existe na sua grade. O que ele tem, no 9º e último tile, é competitor_actions — uma categoria de pesquisa que abre a tela de Pesquisas filtrada e envia por SurveyResultUploadAPI. Diferem em tudo o que importa: enum, rota, tela, proto, transação, ícone e rótulo. O Brasil também é um dos dois mercados que declaram o indicador de status "Há produtos de concorrência no local?" no detalhe da visita — terceira coisa distinta, também sem relação com esta feature. Brazil does not have this feature: the competitor_insights key does not exist in its grid. What it has, in the 9th and last tile, is competitor_actions — a survey category that opens the Surveys screen filtered and submits via SurveyResultUploadAPI. They differ in everything that matters: enum, route, screen, proto, transaction, icon and label. Brazil is also one of the two markets declaring the "Are competitor products present at the location?" status indicator in visit detail — a third distinct thing, also unrelated to this feature. Brasil no tiene esta feature: la clave competitor_insights no existe en su grilla. Lo que tiene, en el 9.º y último tile, es competitor_actions — una categoría de encuesta que abre la pantalla de Encuestas filtrada y envía por SurveyResultUploadAPI. Difieren en todo lo que importa: enum, ruta, pantalla, proto, transacción, ícono y rótulo. Brasil es también uno de los dos mercados que declaran el indicador de estado "¿Se observan productos de la competencia?" en el detalle de la visita — una tercera cosa distinta, también sin relación con esta feature.

CL

Nem uma nem outraNeither one nor the otherNi una ni otra O Chile é o mercado com a grade mais cheia (10 tiles) e não declara nenhuma das duas chaves de concorrência. Mesmo assim ele carrega, sem uso: o TTL de competitorInsights no bloco de frescor e um mock sintético com conteúdo (3 empresas, 4 atividades). Como o tipo de sincronização não inclui CL, essas opções nunca são buscadas nem varridas ali. É configuração residual — provavelmente sobra de um plano de habilitar a ferramenta. Chile has the fullest grid (10 tiles) and declares neither competitor key. Even so it carries, unused: the competitorInsights TTL in the freshness block and a synthetic mock with content (3 companies, 4 activities). Since the sync type doesn't include CL, those options are never fetched nor swept there. It is residual configuration — probably a leftover from a plan to enable the tool. Chile tiene la grilla más llena (10 tiles) y no declara ninguna de las dos claves de competencia. Aun así lleva, sin uso: el TTL de competitorInsights en el bloque de frescura y un mock sintético con contenido (3 empresas, 4 actividades). Como el tipo de sincronización no incluye CL, esas opciones nunca se buscan ni se barren allí. Es configuración residual — probablemente sobra de un plan de habilitar la herramienta.

ARPYPE

AR · PY · PE — sem a featureAR · PY · PE — feature absentAR · PY · PE — sin la feature Existem como mercados do app, mas com configuração mínima: quatro blocos de topo cada um (versão, atualização, novo varejo e visitas) — sem visitDetailConfig e sem dataFreshnessConfig. Sem a grade de ferramentas da visita não há como chegar à tela; sem o bloco de frescor não há TTL; e os dois gates de código não os incluem. O mock sintético existe, mas é um objeto vazio. Nada falha — a feature simplesmente não existe nesses mercados. They exist as app markets, but with minimal configuration: four top-level blocks each (version, update, new retail and visits) — no visitDetailConfig and no dataFreshnessConfig. With no visit tools grid there is no way to reach the screen; with no freshness block there is no TTL; and both code gates exclude them. The synthetic mock exists, but is an empty object. Nothing fails — the feature simply doesn't exist in those markets. Existen como mercados de la app, pero con configuración mínima: cuatro bloques de tope cada uno (versión, actualización, nuevo punto de venta y visitas) — sin visitDetailConfig y sin dataFreshnessConfig. Sin la grilla de herramientas de la visita no hay forma de llegar a la pantalla; sin el bloque de frescura no hay TTL; y los dos gates de código no los incluyen. El mock sintético existe, pero es un objeto vacío. Nada falla — la feature simplemente no existe en esos mercados.

Pendências / roadmapPending items / roadmapPendientes / roadmap

  • Sem rede, o insight é perdido. A fila offline do Dispatcher só enfileira o tipo de visita; qualquer outro vai direto ao transporte. Sem conexão, o gateway lança antes de tentar a rede, o registro é gravado com status de erro (não pendente) e o flush que roda quando a rede volta lê só as visitas pendentes — logo nunca alcança este tipo. O representante precisa refazer o formulário ou reenviar à mão pela Central de dados. Numa tela cujo dado inclui 1 a 3 fotos tiradas no momento, isso é trabalho perdido.Offline, the insight is lost. The Dispatcher's offline queue only enqueues the visit type; anything else goes straight to transport. With no connection the gateway throws before attempting the network, the record is stored with an error status (not pending) and the flush that runs when the network returns reads only the pending visits — so it never reaches this type. The rep must redo the form or resend by hand through the Data center. On a screen whose data includes 1 to 3 photos taken on the spot, that is lost work.Sin red, el insight se pierde. La cola offline del Dispatcher solo encola el tipo de visita; cualquier otro va directo al transporte. Sin conexión el gateway lanza antes de intentar la red, el registro se graba con estado de error (no pendiente) y el flush que corre cuando la red vuelve lee solo las visitas pendientes — por lo tanto nunca alcanza este tipo. El representante debe rehacer el formulario o reenviar a mano por el Centro de datos. En una pantalla cuyo dato incluye 1 a 3 fotos tomadas en el momento, eso es trabajo perdido.
  • A referência da transação não identifica o envio. O builder grava transactionReference: accountSfid — o identificador do varejo, não do insight. Dois insights do mesmo varejo, no mesmo dia, produzem registros com a mesma referência (e o mesmo dateReference), o que torna impossível distinguir um do outro no histórico. O mesmo accountSfid é usado por outros 6 builders de transação com escopo de conta — a observação vale para a família, não só para esta transação. Agrava-se porque o tipo não é lightweight, ou seja, está marcado como "reenvio pode duplicar": um reenvio manual não tem chave de idempotência do lado do backend. O projeto tem um gerador de referência única (DispatcherUtils.generateTransactionReference()) que esta transação não usa.The transaction reference does not identify the submission. The builder writes transactionReference: accountSfid — the retail's identifier, not the insight's. Two insights for the same retail on the same day produce records with the same reference (and the same dateReference), which makes it impossible to tell one from the other in the history. The same accountSfid is used by 6 other account-scoped transaction builders — the observation holds for the family, not just this transaction. It is aggravated because the type is not lightweight, i.e. it is flagged "resend may duplicate": a manual resend has no idempotency key on the backend side. The project has a unique-reference generator (DispatcherUtils.generateTransactionReference()) that this transaction does not use.La referencia de la transacción no identifica el envío. El builder graba transactionReference: accountSfid — el identificador del punto de venta, no del insight. Dos insights del mismo punto de venta, el mismo día, producen registros con la misma referencia (y el mismo dateReference), lo que hace imposible distinguir uno del otro en el historial. El mismo accountSfid lo usan otros 6 builders de transacción con alcance de cuenta — la observación vale para la familia, no solo para esta transacción. Se agrava porque el tipo no es lightweight, es decir, está marcado como "el reenvío puede duplicar": un reenvío manual no tiene clave de idempotencia del lado del backend. El proyecto tiene un generador de referencia única (DispatcherUtils.generateTransactionReference()) que esta transacción no usa.
  • O rótulo promete a galeria; só a câmera funciona. Nos seis mercados o texto da faixa de fotos diz "escolha ou tire foto", mas o único toque ligado passa CaptureSource.camera. O método pickPhotoFromGallery() existe no Notifier e não tem nenhum chamador em todo o projeto. Ou se liga a galeria (um segundo botão), ou se corrige o texto — hoje o rótulo mente.The label promises the gallery; only the camera works. In all six markets the photos strip's text says "choose or take photo", but the only wired tap passes CaptureSource.camera. The pickPhotoFromGallery() method exists on the Notifier and has no caller at all in the whole project. Either the gallery gets wired (a second button) or the text gets fixed — today the label lies.El rótulo promete la galería; solo la cámara funciona. En los seis mercados el texto de la franja de fotos dice "escoge o toma foto", pero el único toque conectado pasa CaptureSource.camera. El método pickPhotoFromGallery() existe en el Notifier y no tiene ningún llamador en todo el proyecto. O se conecta la galería (un segundo botón), o se corrige el texto — hoy el rótulo miente.
  • 4 das 13 chaves do payload são sempre vazias. brandId (""), brandVariantIds ([]), brandVersionIds ([]) e answers ([]) são literais fixos no builder. No app legado esses campos vinham de um formulário dinâmico de itens da atividade (marca, variante, versão, e respostas livres) que nunca foi portado. É a lacuna funcional mais visível da feature: o backend recebe o insight sem qualquer informação de produto.4 of the 13 payload keys are always empty. brandId (""), brandVariantIds ([]), brandVersionIds ([]) and answers ([]) are fixed literals in the builder. In the legacy app those fields came from a dynamic activity-item form (brand, variant, version, and free answers) that was never ported. It is the feature's most visible functional gap: the backend receives the insight with no product information whatsoever.4 de las 13 claves del payload son siempre vacías. brandId (""), brandVariantIds ([]), brandVersionIds ([]) y answers ([]) son literales fijos en el builder. En la app legada esos campos venían de un formulario dinámico de ítems de la actividad (marca, variante, versión, y respuestas libres) que nunca fue portado. Es la laguna funcional más visible de la feature: el backend recibe el insight sin ninguna información de producto.
  • O período não tem nenhum limite. O calendário é aberto sem data mínima, sem data máxima e sem predicado de dia selecionável, então é possível registrar uma atividade de concorrência inteiramente no futuro ou anos no passado. A única regra é a ordem relativa entre as duas datas — e a metade dela é silenciosa: escolher uma data final anterior à inicial não faz nada e não avisa nada, o que se lê como "o app travou".The period has no bounds at all. The calendar is opened with no minimum date, no maximum date and no selectable-day predicate, so it is possible to record a competitor activity entirely in the future or years in the past. The only rule is the relative order of the two dates — and half of it is silent: picking a final date earlier than the initial one does nothing and warns nothing, which reads as "the app froze".El periodo no tiene ningún límite. El calendario se abre sin fecha mínima, sin fecha máxima y sin predicado de día seleccionable, así que es posible registrar una actividad de competencia enteramente en el futuro o años en el pasado. La única regla es el orden relativo entre las dos fechas — y la mitad de ella es silenciosa: elegir una fecha final anterior a la inicial no hace nada y no avisa nada, lo que se lee como "la app se colgó".
  • O envio pode chegar a ~1,2 MB de JSON, sem nenhum teto. Três fotos × ~300 KB, mais ~33% do base64, viajam inline no campo de mensagem da transação. E os 300 KB são best-effort: se nenhum dos 6 passos de compressão atingir o alvo, o serviço grava o menor resultado; se todos falharem, copia o original sem compressão. Nenhuma camada valida o tamanho do envelope antes de enviar.The submission can reach ~1.2 MB of JSON, with no ceiling. Three photos × ~300 KB, plus base64's ~33%, travel inline in the transaction's message field. And the 300 KB are best-effort: if none of the 6 compression steps hits the target, the service writes the smallest result; if all fail, it copies the original, uncompressed. No layer validates the envelope size before sending.El envío puede llegar a ~1,2 MB de JSON, sin ningún techo. Tres fotos × ~300 KB, más ~33% del base64, viajan inline en el campo de mensaje de la transacción. Y los 300 KB son best-effort: si ninguno de los 6 pasos de compresión alcanza el objetivo, el servicio graba el menor resultado; si todos fallan, copia el original sin compresión. Ninguna capa valida el tamaño del sobre antes de enviar.
  • O botão Enviar continua ativo enquanto a câmera está aberta. O predicado canSave checa sete condições e não checa isCapturingPhoto. Com uma foto já anexada e uma segunda captura em andamento, o toque em Enviar despacha o insight sem a foto que está sendo tirada. Acrescentar isCapturingPhoto ao canSave é uma linha.The Send button stays active while the camera is open. The canSave predicate checks seven conditions and does not check isCapturingPhoto. With one photo already attached and a second capture in flight, tapping Send dispatches the insight without the photo being taken. Adding isCapturingPhoto to canSave is a one-liner.El botón Enviar sigue activo mientras la cámara está abierta. El predicado canSave verifica siete condiciones y no verifica isCapturingPhoto. Con una foto ya adjunta y una segunda captura en curso, el toque en Enviar despacha el insight sin la foto que se está tomando. Agregar isCapturingPhoto al canSave es una línea.
  • Falha de captura é engolida. Uma exceção ao tirar/comprimir a foto é registrada no log e a flag é desligada — sem nenhum aviso. Para quem usa, o toque no botão de câmera simplesmente não produz nada, indistinguível de um cancelamento. A feature usa ConectaNotice corretamente no envio; falta o mesmo tratamento aqui.A capture failure is swallowed. An exception while taking/compressing the photo is written to the log and the flag is turned off — with no notice at all. To the user, tapping the camera button simply produces nothing, indistinguishable from a cancellation. The feature uses ConectaNotice correctly on submit; the same treatment is missing here.Una falla de captura se traga. Una excepción al tomar/comprimir la foto se registra en el log y la flag se apaga — sin ningún aviso. Para quien usa, el toque en el botón de cámara simplemente no produce nada, indistinguible de una cancelación. La feature usa ConectaNotice correctamente en el envío; falta el mismo tratamiento aquí.
  • Sincronização incremental existe no contrato e está inerte. O lastModifiedDate (campo #2 do request) está plumbado até o datasource, com guarda de vazio, e nenhum caller o preenche — o único chamador do Repository passa só o locationHierarchySfid. Todo fetch remoto traz o catálogo inteiro e reescreve o cache do zero. É a mesma lacuna que outros agregados do app têm; para um lookup de 24 h de TTL o custo é baixo, mas o gancho está lá.Incremental sync exists in the contract and is inert. lastModifiedDate (request field #2) is plumbed down to the datasource, with an emptiness guard, and no caller fills it — the Repository's only caller passes just the locationHierarchySfid. Every remote fetch brings the whole catalog and rewrites the cache from scratch. It is the same gap other aggregates in the app have; for a lookup with a 24 h TTL the cost is low, but the hook is there.La sincronización incremental existe en el contrato y está inerte. El lastModifiedDate (campo #2 del request) está plumbado hasta el datasource, con guarda de vacío, y ningún caller lo completa — el único llamador del Repository pasa solo el locationHierarchySfid. Todo fetch remoto trae el catálogo entero y reescribe el caché desde cero. Es la misma laguna que otros agregados de la app tienen; para un lookup con 24 h de TTL el costo es bajo, pero el gancho está ahí.
  • Descrição sem limite e datas formatadas pelo idioma, não pelo mercado. O campo de texto não tem maxLength nem formatadores, então cabe um texto arbitrariamente longo dentro do JSON da transação. E os dois cartões de data exibem com formatLocaleDate (resolvido pelo locale ativo), enquanto o padrão do projeto para exibição por país é formatDateForMarket — divergência pequena, mas é a convenção de data do mercado que deveria mandar na tela.Unbounded description and dates formatted by language, not by market. The text field has no maxLength and no formatters, so an arbitrarily long text fits inside the transaction's JSON. And both date cards render with formatLocaleDate (resolved by the active locale), whereas the project's standard for per-country display is formatDateForMarket — a small divergence, but it is the market's date convention that should rule the screen.Descripción sin límite y fechas formateadas por el idioma, no por el mercado. El campo de texto no tiene maxLength ni formateadores, así que cabe un texto arbitrariamente largo dentro del JSON de la transacción. Y las dos tarjetas de fecha exhiben con formatLocaleDate (resuelto por el locale activo), mientras el estándar del proyecto para exhibición por país es formatDateForMarket — divergencia pequeña, pero es la convención de fecha del mercado la que debería mandar en la pantalla.
  • Configuração morta em BR e CL, e um método sem consumidor. Os dois mercados declaram o TTL de competitorInsights e mantêm mocks sintéticos com conteúdo (BR 4 empresas × 5 atividades; CL 3 × 4) para uma ferramenta que não têm — o tipo de sincronização é ZA-only, então essas opções nunca são buscadas nem varridas lá. No mesmo espírito, o getCached() do UseCase não tem nenhum consumidor (a tela usa o execute() com o default local).Dead configuration in BR and CL, and a method with no consumer. Both markets declare the competitorInsights TTL and keep synthetic mocks with content (BR 4 companies × 5 activities; CL 3 × 4) for a tool they don't have — the sync type is ZA-only, so those options are never fetched nor swept there. In the same spirit, the UseCase's getCached() has no consumer at all (the screen uses execute() with the local default).Configuración muerta en BR y CL, y un método sin consumidor. Los dos mercados declaran el TTL de competitorInsights y mantienen mocks sintéticos con contenido (BR 4 empresas × 5 actividades; CL 3 × 4) para una herramienta que no tienen — el tipo de sincronización es ZA-only, así que esas opciones nunca se buscan ni se barren allí. En el mismo espíritu, el getCached() del UseCase no tiene ningún consumidor (la pantalla usa el execute() con el default local).
  • Documentação de apoio defasada, em dois lugares. O README dos mocks afirma "Mercados: BR, CL, ZA" (o único mercado da feature é ZA) e cita um arquivo ro_*.json de um mercado que não existe no app — os três arquivos vazios são de AR, PY e PE. E o README antigo da transação declara o valor de tipo de despacho como competitorInsightsUpload, que não existe no enum (o real é competitorInsights); ele também não menciona o destino exclusivo, o mercado único, nem o comportamento offline.Stale supporting documentation, in two places. The mocks README claims "Markets: BR, CL, ZA" (the feature's only market is ZA) and cites a ro_*.json file for a market that does not exist in the app — the three empty files belong to AR, PY and PE. And the transaction's old README declares the dispatch type value as competitorInsightsUpload, which does not exist in the enum (the real one is competitorInsights); it also fails to mention the exclusive destination, the single market, or the offline behaviour.Documentación de apoyo desactualizada, en dos lugares. El README de los mocks afirma "Mercados: BR, CL, ZA" (el único mercado de la feature es ZA) y cita un archivo ro_*.json de un mercado que no existe en la app — los tres archivos vacíos son de AR, PY y PE. Y el README antiguo de la transacción declara el valor de tipo de despacho como competitorInsightsUpload, que no existe en el enum (el real es competitorInsights); tampoco menciona el destino exclusivo, el mercado único, ni el comportamiento offline.
  • Dúvidas de contrato, a confirmar com o backend: (a) as duas datas do período vão como dd/MM/yyyy, um formato de exibição, enquanto o dateReference do mesmo envelope vai como yyyy-MM-dd — confirmar se é intencional; (b) o isCritical não muda nada no app, então todo o significado de "crítico" depende do processamento do backend; (c) o insight enviado nunca volta ao app (não há RPC de leitura de insights), logo não existe reconciliação: um envio duplicado só é detectável do lado do servidor.Contract questions, to confirm with the backend: (a) both period dates go as dd/MM/yyyy, a display format, while the same envelope's dateReference goes as yyyy-MM-dd — confirm whether that is intentional; (b) isCritical changes nothing in the app, so the whole meaning of "critical" depends on backend processing; (c) the submitted insight never comes back to the app (there is no insight-read RPC), so no reconciliation exists: a duplicate submission is only detectable server-side.Dudas de contrato, a confirmar con el backend: (a) las dos fechas del periodo van como dd/MM/yyyy, un formato de exhibición, mientras el dateReference del mismo sobre va como yyyy-MM-dd — confirmar si es intencional; (b) el isCritical no cambia nada en la app, así que todo el significado de "crítico" depende del procesamiento del backend; (c) el insight enviado nunca vuelve a la app (no hay RPC de lectura de insights), por lo que no existe reconciliación: un envío duplicado solo es detectable del lado del servidor.

Onde continuar lendoWhere to read nextDónde seguir leyendo A transação do envio está em 13 · CompetitorInsightsUploadAPI. O atalho que abre esta tela, a guarda de início e o indicador de status de concorrência vivem em Detalhe da visita, cuja lista está em Visitas. O tile competitor_actions do Brasil é descrito em Pesquisas. O histórico de despachos, o reenvio manual e a fila offline estão em Central de dados. Para a outra ferramenta in-visit que também envia fotos, ver Merchandising; a lista de varejos que dá contexto às visitas está em Varejos. As pendências de encerramento de visita ficam na tela de encerramento, ainda sem documento próprio. The submission transaction is in 13 · CompetitorInsightsUploadAPI. The shortcut that opens this screen, the start guard and the competitor status indicator live in Visit detail, whose list is in Visits. Brazil's competitor_actions tile is described in Surveys. The dispatch history, the manual resend and the offline queue are in Data center. For the other in-visit tool that also submits photos, see Merchandising; the retail list that gives visits their context is in Retails. The visit-end pending items live on the visit-end screen, still without a document of its own. La transacción del envío está en 13 · CompetitorInsightsUploadAPI. El atajo que abre esta pantalla, la guarda de inicio y el indicador de estado de competencia viven en Detalle de la visita, cuya lista está en Visitas. El tile competitor_actions de Brasil se describe en Encuestas. El historial de despachos, el reenvío manual y la cola offline están en Centro de datos. Para la otra herramienta in-visit que también envía fotos, ver Merchandising; la lista de puntos de venta que da contexto a las visitas está en Puntos de venta. Los pendientes de cierre de visita están en la pantalla de cierre, aún sin documento propio.