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 · Verificação de preçoFeature · Price checkFeature · Verificación de precio

Verificação de preçoPrice checkVerificación de precio

A ferramenta que o representante de vendas usa dentro de uma visita para anotar por quanto aquele varejo está revendendo cada produto — dois preços por item, o do maço e o da unidade. O catálogo de produtos vem do cache; os preços são digitados na tela e enviados ao backend pelo Dispatcher. A tela não exibe nenhum preço da BAT: ela coleta o preço da prateleira. The tool a sales rep uses inside a visit to record what that retail is reselling each product for — two prices per item, pack and single stick. The product catalog comes from cache; the prices are typed on screen and sent to the backend through the Dispatcher. The screen shows no BAT price at all: it collects the shelf price. La herramienta que el representante de ventas usa dentro de una visita para anotar a cuánto ese punto de venta está revendiendo cada producto — dos precios por ítem, el del paquete y el de la unidad. El catálogo de productos viene del caché; los precios se escriben en la pantalla y se envían al backend por el Dispatcher. La pantalla no muestra ningún precio de BAT: recoge el precio de la estantería.

PúblicoAudiencePúblico
Representante · QA · Suporte · DevRep · QA · Support · DevRepresentante · QA · Soporte · Dev
Onde ficaWhereDónde
Detalhe da visita → Ferramentas → Price CheckVisit detail → Tools → Price CheckDetalle de la visita → Herramientas → Price Check
RelacionadoRelatedRelacionado
Visit Detail · ProductPriceCheck
AtualizadoUpdatedActualizado
04/08/20262026-08-04
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

A Verificação de preço é um levantamento de campo: durante a visita, o representante de vendas percorre a lista de produtos que a empresa quer monitorar e anota, produto por produto, por quanto aquele varejo está vendendo ao consumidor. São duas colunas de entrada por produto — maço (a embalagem fechada) e unidade (o cigarro avulso, prática comum na África do Sul). Price check is a field survey: during the visit, the sales rep walks the list of products the company wants to monitor and records, product by product, what that retail is charging the consumer. There are two input columns per product — packs (the sealed pack) and sticks (the loose cigarette, common practice in South Africa). La Verificación de precio es un levantamiento de campo: durante la visita, el representante de ventas recorre la lista de productos que la empresa quiere monitorear y anota, producto por producto, a cuánto ese punto de venta le vende al consumidor. Hay dos columnas de entrada por producto — paquetes (el envase cerrado) y unidades (el cigarrillo suelto, práctica común en Sudáfrica).

Quais produtos aparecem?Which products show up?¿Qué productos aparecen?

Os do catálogo do mercado marcados pelo backend como disponíveis para verificação de preço. A lista é a mesma para todos os varejos — não depende da visita nem do que aquele varejo compra.The ones in the market catalog flagged by the backend as available for price check. The list is the same for every retail — it doesn't depend on the visit or on what that retail buys.Los del catálogo del mercado marcados por el backend como disponibles para verificación de precio. La lista es la misma para todos los puntos de venta — no depende de la visita ni de lo que ese punto de venta compra.

O que se digita?What do you type?¿Qué se escribe?

Só números. Cada produto tem dois campos independentes; pode-se preencher um, o outro ou os dois. Campo vazio significa "não verificado" e o produto simplesmente não entra no envio.Numbers only. Each product has two independent fields; you may fill one, the other or both. An empty field means "not checked" and the product simply doesn't go into the submission.Solo números. Cada producto tiene dos campos independientes; se puede completar uno, el otro o los dos. Un campo vacío significa "no verificado" y el producto simplemente no entra en el envío.

O que acontece ao enviar?What happens on submit?¿Qué pasa al enviar?

O app confirma num modal, monta uma linha por produto preenchido e manda tudo num único envio ao backend. Com sucesso, avisa em verde e volta para a visita.The app asks for confirmation in a modal, builds one row per filled product and sends everything in a single submission. On success it shows a green notice and returns to the visit.La app confirma en un modal, arma una fila por producto completado y manda todo en un único envío al backend. Con éxito avisa en verde y vuelve a la visita.

Coleta, não comparaçãoCollection, not comparisonRecolección, no comparación A tela não mostra o preço da BAT em nenhum lugar — nem preço de tabela, nem preço sugerido, nem margem. Os únicos números visíveis são os que o próprio representante digitou (e os totais que o app soma a partir deles). Quem precisa comparar preço de compra com preço de revenda usa a Calculadora de margem, que é outra ferramenta da mesma grade. The screen never shows the BAT price — no list price, no suggested price, no margin. The only visible numbers are the ones the rep typed (and the totals the app adds up from them). Anyone who needs to compare purchase price against resale price uses the Margin calculator, another tool in the same grid. La pantalla no muestra el precio de BAT en ningún lugar — ni precio de lista, ni precio sugerido, ni margen. Los únicos números visibles son los que el propio representante escribió (y los totales que la app suma a partir de ellos). Quien necesite comparar precio de compra con precio de reventa usa la Calculadora de margen, otra herramienta de la misma grilla.

02

Como acessarHow to openCómo acceder

  1. Um único caminho: a grade de ferramentas da visitaOne single path: the visit tools gridUn único camino: la grilla de herramientas de la visitaAbra o Detalhe da visita, desça até Ferramentas e toque no tile Price Check. Esse é o único acesso do app — não há aba inferior, item de menu lateral, atalho na Home nem entrada pelo encerramento de visita.Open the Visit detail, scroll to Tools and tap the Price Check tile. That is the app's only way in — there is no bottom tab, no side-menu item, no Home shortcut and no entry from visit end.Abra el Detalle de la visita, baje hasta Herramientas y toque el tile Price Check. Ese es el único acceso de la app — no hay pestaña inferior, ítem de menú lateral, atajo en el Home ni entrada por el cierre de visita.
  2. A visita tem de estar iniciadaThe visit must be startedLa visita tiene que estar iniciadaTodo tile da grade passa por uma verificação: se a visita ainda não foi iniciada, aparece um modal oferecendo iniciá-la. Confirmar inicia a visita mas não abre a ferramenta — é preciso tocar no tile de novo.Every tile in the grid goes through a check: if the visit hasn't been started, a modal appears offering to start it. Confirming starts the visit but does not open the tool — you have to tap the tile again.Todo tile de la grilla pasa por una verificación: si la visita todavía no fue iniciada, aparece un modal ofreciendo iniciarla. Confirmar inicia la visita pero no abre la herramienta — hay que tocar el tile de nuevo.
  3. A tela abre com a lista prontaThe screen opens with the list readyLa pantalla abre con la lista listaO que a ferramenta recebe da visita é só o varejo (identificador e código SAP) — nem a visita em si é passada adiante. Os produtos vêm do catálogo já sincronizado; puxar a lista para baixo rebusca o catálogo no servidor.All the tool receives from the visit is the retail (identifier and SAP code) — not even the visit itself is passed along. The products come from the already-synced catalog; pulling the list down re-fetches the catalog from the server.Lo que la herramienta recibe de la visita es solo el punto de venta (identificador y código SAP) — ni la visita en sí se pasa adelante. Los productos vienen del catálogo ya sincronizado; deslizar la lista hacia abajo vuelve a buscar el catálogo en el servidor.

Só na África do SulSouth Africa onlySolo en Sudáfrica O tile só é declarado na configuração da África do Sul. Brasil e Chile têm a grade de ferramentas da visita, mas não declaram a verificação de preço entre os itens dela; Argentina, Paraguai e Peru não têm a grade nenhuma. Fora da África do Sul a tela existe no app e é inalcançável. The tile is only declared in the South Africa configuration. Brazil and Chile do have the visit tools grid, but they don't declare price check among its items; Argentina, Paraguay and Peru have no grid at all. Outside South Africa the screen exists in the app and is unreachable. El tile solo se declara en la configuración de Sudáfrica. Brasil y Chile tienen la grilla de herramientas de la visita, pero no declaran la verificación de precio entre sus ítems; Argentina, Paraguay y Perú no tienen grilla alguna. Fuera de Sudáfrica la pantalla existe en la app y es inalcanzable.

03

Estrutura da telaScreen structureEstructura de la pantalla

Última sincronizaçãoLast syncÚltima sincronización
Faixa no topo com a data e hora em que o catálogo de produtos foi baixado — não é a data da verificação.A strip at the top with the date and time the product catalog was downloaded — not the date of the check.Franja arriba con la fecha y hora en que se bajó el catálogo de productos — no es la fecha de la verificación.
CabeçalhoHeaderEncabezado
Ícone da ferramenta e o título Price Check. Não há cartão do varejo aqui — o nome do varejo fica na tela da visita.The tool icon and the Price Check title. There is no retail card here — the retail name stays on the visit screen.Ícono de la herramienta y el título Price Check. No hay tarjeta del punto de venta aquí — el nombre queda en la pantalla de la visita.
Tabela de produtosProduct tableTabla de productos
Um card único com cabeçalho de três colunas — Marca, Maços, Unidades — e uma linha por produto: botão de informação, nome (até 2 linhas) e os dois campos numéricos.A single card with a three-column header — Brand, Packs, Sticks — and one row per product: info button, name (up to 2 lines) and the two numeric fields.Una tarjeta única con encabezado de tres columnas — Marca, Paquetes, Unidades — y una fila por producto: botón de información, nombre (hasta 2 líneas) y los dos campos numéricos.
Botão de informação do produtoProduct info buttonBotón de información del producto
O ícone à esquerda do nome abre um modal com o nome e a foto do produto. É o mesmo componente usado na vitrine de pedido.The icon left of the name opens a modal with the product's name and photo. It's the same component used in the order showcase.El ícono a la izquierda del nombre abre un modal con el nombre y la foto del producto. Es el mismo componente usado en la vitrina de pedido.
Carregamento por rolagemScroll loadingCarga por desplazamiento
A lista começa com 20 produtos e cresce de 20 em 20 conforme se rola. Enquanto houver produtos por carregar, uma rodinha de carregamento fica fixa embaixo da última linha dentro do card.The list starts with 20 products and grows 20 at a time as you scroll. While products remain to load, a spinner stays pinned under the last row inside the card.La lista empieza con 20 productos y crece de 20 en 20 al desplazar. Mientras queden productos por cargar, una rueda de carga queda fija debajo de la última fila dentro de la tarjeta.
Contador "X de Y""X of Y" counterContador "X de Y"
Rodapé do card: quantos produtos já estão na tela do total do catálogo filtrado.Card footer: how many products are on screen out of the filtered catalog total.Pie de la tarjeta: cuántos productos ya están en pantalla del total del catálogo filtrado.
Barra de resumoSummary barBarra de resumen
Barra fixa no rodapé da tela que só aparece depois do primeiro preço digitado. Fechada, mostra três números (preenchidos, soma dos maços, soma das unidades) e o botão de enviar.A bar pinned to the bottom of the screen that only appears after the first price is typed. Collapsed, it shows three numbers (filled, packs total, sticks total) and the submit button.Barra fija al pie de la pantalla que solo aparece después del primer precio escrito. Cerrada, muestra tres números (completados, suma de paquetes, suma de unidades) y el botón de enviar.
Painel expandido do resumoExpanded summary panelPanel expandido del resumen
Arrastar a barra para cima abre a conferência: os produtos preenchidos agrupados por categoria, com subtotal por categoria, cinco números no rodapé (total de produtos, preenchidos, vazios, soma dos maços, soma das unidades) e o botão Limpar tudo. Produto sem preço não aparece aqui; coluna não preenchida aparece com um marcador de vazio.Dragging the bar up opens the review: the filled products grouped by category, with a subtotal per category, five numbers in the footer (total products, filled, empty, packs total, sticks total) and the Clear all button. A product with no price isn't listed here; an unfilled column shows an empty marker.Arrastrar la barra hacia arriba abre la revisión: los productos completados agrupados por categoría, con subtotal por categoría, cinco números al pie (total de productos, completados, vacíos, suma de paquetes, suma de unidades) y el botón Limpiar todo. Un producto sin precio no aparece aquí; una columna no completada muestra un marcador de vacío.
Puxar para atualizarPull to refreshDeslizar para actualizar
Rebusca o catálogo no servidor. Os preços já digitados são preservados; a lista volta a mostrar os primeiros 20 produtos.Re-fetches the catalog from the server. Prices already typed are preserved; the list goes back to showing the first 20 products.Vuelve a buscar el catálogo en el servidor. Los precios ya escritos se preservan; la lista vuelve a mostrar los primeros 20 productos.

A tela não tem busca, ordenação, filtro nem abas — a lista é o catálogo elegível inteiro, na ordem em que o backend o entrega. É uma diferença deliberada em relação à contagem de estoque, que filtra por categoria e por família de marca.The screen has no search, sort, filter or tabs — the list is the whole eligible catalog, in the order the backend delivers it. This is a deliberate difference from stock count, which filters by category and brand family.La pantalla no tiene búsqueda, orden, filtro ni pestañas — la lista es el catálogo elegible entero, en el orden en que el backend lo entrega. Es una diferencia deliberada respecto al conteo de stock, que filtra por categoría y por familia de marca.

04

Estados da telaScreen statesEstados de la pantalla

A verificação de preço não tem "status" de negócio — nada é aprovado, rejeitado ou fica pendente. O que existe são os estados da própria tela:Price check has no business "status" — nothing gets approved, rejected or left pending. What exists are the screen's own states:La verificación de precio no tiene "estado" de negocio — nada se aprueba, se rechaza ni queda pendiente. Lo que existe son los estados de la propia pantalla:

CarregandoLoadingCargando
Rodinha centralizada com o logo do app, cobrindo a tela inteira, enquanto o catálogo é lido.A centered spinner with the app logo, covering the whole screen, while the catalog is read.Rueda centrada con el logo de la app, cubriendo la pantalla entera, mientras se lee el catálogo.
ErroErrorError
Tela de falha com botão de tentar de novo, que recarrega a ferramenta do zero. Acontece quando o catálogo não pode ser lido nem do cache nem do servidor.A failure screen with a retry button that reloads the tool from scratch. It happens when the catalog can be read neither from cache nor from the server.Pantalla de fallo con botón de reintentar, que recarga la herramienta desde cero. Ocurre cuando el catálogo no puede leerse ni del caché ni del servidor.
Lista sem nada preenchidoList with nothing filledLista sin nada completado
Estado inicial: todos os campos vazios e sem barra de resumo. Não há como enviar nesse estado.Initial state: all fields empty and no summary bar. There is no way to submit in this state.Estado inicial: todos los campos vacíos y sin barra de resumen. No hay manera de enviar en ese estado.
EnviandoSubmittingEnviando
O botão de enviar mostra carregamento e para de aceitar toque. Os campos continuam editáveis.The submit button shows a loading state and stops accepting taps. The fields stay editable.El botón de enviar muestra carga y deja de aceptar toques. Los campos siguen editables.
Catálogo vazioEmpty catalogCatálogo vacío
Se nenhum produto do catálogo estiver marcado como elegível, a tela mostra apenas a faixa de sincronização e o título: o card da tabela não é renderizado e não há mensagem de lista vazia (ver Pendências).If no catalog product is flagged eligible, the screen shows only the sync strip and the title: the table card isn't rendered and there is no empty-list message (see Pending items).Si ningún producto del catálogo está marcado como elegible, la pantalla muestra solo la franja de sincronización y el título: la tarjeta de la tabla no se renderiza y no hay mensaje de lista vacía (ver Pendientes).

Nada é salvo em rascunhoNothing is saved as a draftNada se guarda como borrador Os preços digitados vivem só na memória da tela. Sair pelo botão de voltar — sem modal de confirmação, ao contrário das pesquisas — descarta tudo. E abrir a ferramenta de novo, mesmo depois de um envio bem-sucedido, mostra a lista completa com todos os campos vazios: o app não guarda marca de "já verificado". The typed prices live only in the screen's memory. Leaving via the back button — with no confirmation modal, unlike surveys — discards everything. And reopening the tool, even right after a successful submission, shows the full list with every field empty: the app keeps no "already checked" marker. Los precios escritos viven solo en la memoria de la pantalla. Salir por el botón de volver — sin modal de confirmación, a diferencia de las encuestas — descarta todo. Y abrir la herramienta de nuevo, incluso justo después de un envío exitoso, muestra la lista completa con todos los campos vacíos: la app no guarda marca de "ya verificado".

05

Preencher e enviarFill in and submitCompletar y enviar

Digitar um preçoTyping a priceEscribir un precio

Os dois campos de cada linha aceitam dígitos, ponto e vírgula — qualquer outro caractere é recusado na digitação, e o teclado abre no modo numérico com decimal. A vírgula é tratada como separador decimal. Apagar o conteúdo volta o campo a "não verificado": se as duas colunas do produto ficarem vazias, ele sai do resumo e do envio como se nunca tivesse sido tocado.Both fields on each row accept digits, dot and comma — any other character is refused as you type, and the keyboard opens in decimal-numeric mode. The comma is treated as a decimal separator. Clearing the content returns the field to "not checked": if both of a product's columns end up empty, it drops out of the summary and out of the submission as if never touched.Los dos campos de cada fila aceptan dígitos, punto y coma — cualquier otro carácter se rechaza al escribir, y el teclado abre en modo numérico con decimal. La coma se trata como separador decimal. Borrar el contenido devuelve el campo a "no verificado": si las dos columnas del producto quedan vacías, sale del resumen y del envío como si nunca hubiera sido tocado.

Limpar tudoClear allLimpiar todo

Dentro do painel expandido do resumo. Abre um modal de confirmação; confirmando, todos os preços são apagados de uma vez e a barra de resumo desaparece. Os campos da lista se limpam junto, inclusive o que estiver com o cursor dentro.Inside the expanded summary panel. It opens a confirmation modal; on confirm, all prices are wiped at once and the summary bar disappears. The list fields clear along with it, including one that currently has the cursor in it.Dentro del panel expandido del resumen. Abre un modal de confirmación; al confirmar, todos los precios se borran de una vez y la barra de resumen desaparece. Los campos de la lista se limpian junto, incluso el que tenga el cursor dentro.

EnviarSubmitEnviar

  1. ConfirmaçãoConfirmationConfirmaciónO botão da barra de resumo abre um modal avisando que todos os preços digitados serão enviados. Cancelar volta à lista sem enviar nada.The summary-bar button opens a modal warning that all typed prices will be sent. Cancelling returns to the list without sending anything.El botón de la barra de resumen abre un modal avisando que todos los precios escritos serán enviados. Cancelar vuelve a la lista sin enviar nada.
  2. EnvioSubmissionEnvíoO app monta uma linha por produto preenchido — varejo, produto, preço do maço, preço da unidade e a data de hoje — e manda tudo num único envio. Produtos sem preço não entram.The app builds one row per filled product — retail, product, pack price, stick price and today's date — and sends it all in a single submission. Products with no price are left out.La app arma una fila por producto completado — punto de venta, producto, precio del paquete, precio de la unidad y la fecha de hoy — y manda todo en un único envío. Los productos sin precio no entran.
  3. Resultado em telaOn-screen resultResultado en pantallaSucesso mostra o aviso verde e volta para o detalhe da visita. Falha mostra o aviso vermelho (com o código do erro) e mantém a tela como está, com tudo preenchido, para tentar de novo.Success shows the green notice and returns to the visit detail. Failure shows the red notice (with the error code) and leaves the screen as it is, everything filled in, so you can retry.El éxito muestra el aviso verde y vuelve al detalle de la visita. El fallo muestra el aviso rojo (con el código del error) y deja la pantalla como está, con todo completado, para reintentar.

Sem conexão o envio falha na horaOffline, the submission fails right awaySin conexión el envío falla en el momento A verificação de preço não entra na fila de reenvio automático do Dispatcher — offline ela vai ao transporte, falha e é registrada como erro. O registro fica visível na Central de dados e pode ser reenviado à mão; como este é um dos poucos tipos marcados como reenvio seguro, repetir o envio não gera aviso de duplicidade. Nada bloqueia o encerramento da visita por causa de uma verificação de preço pendente. Price check does not enter the Dispatcher's automatic retry queue — offline it goes to transport, fails and is recorded as an error. The record shows up in the Data center and can be resent by hand; since this is one of the few types flagged as safe to resend, repeating the submission raises no duplicate warning. Nothing blocks closing the visit over a pending price check. La verificación de precio no entra en la cola de reenvío automático del Dispatcher — offline va al transporte, falla y se registra como error. El registro queda visible en la Central de datos y puede reenviarse a mano; como este es uno de los pocos tipos marcados como reenvío seguro, repetir el envío no genera aviso de duplicidad. Nada bloquea el cierre de la visita por una verificación de precio pendiente.

06

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

Clean Architecture + Riverpod + Freezed + gRPC + ObjectBox, com dois caminhos que não se cruzam. A leitura é do catálogo de produtos — um agregado compartilhado por várias features (§29 do CLAUDE.md): um único RPC traz o catálogo do mercado inteiro, que é gravado no ObjectBox, e a verificação de preço lê de lá através do seu UseCase próprio, que filtra pela flag de elegibilidade. A escrita não usa esse proto: os preços digitados vão como JSON pelo Dispatcher.Clean Architecture + Riverpod + Freezed + gRPC + ObjectBox, with two paths that never cross. Reading is the product catalog — an aggregate shared by several features (CLAUDE.md §29): a single RPC brings the whole market catalog, it is written to ObjectBox, and price check reads from there through its own UseCase, which filters on the eligibility flag. Writing doesn't use that proto: the typed prices go as JSON through the Dispatcher.Clean Architecture + Riverpod + Freezed + gRPC + ObjectBox, con dos caminos que no se cruzan. La lectura es del catálogo de productos — un agregado compartido por varias features (§29 del CLAUDE.md): un único RPC trae el catálogo del mercado entero, que se graba en ObjectBox, y la verificación de precio lee de allí a través de su UseCase propio, que filtra por la flag de elegibilidad. La escritura no usa ese proto: los precios escritos van como JSON por el Dispatcher.

Leitura — catálogo de produtosRead — product catalogLectura — catálogo de productos

  • ProductCatalogReplygRPC proto
    • toDTOProductCatalogDTODTO · Freezed
      • toDomain · compute (isolate)ProductCatalogEntitydomain · lastSyncAt gerado aqui
        • toModelProductCatalogModelObjectBox · 11 boxes
          • toDomainProductCatalogEntitydomain · cache
            • GetProductsForPriceCheckUseCasePriceCheckNotifier + Statefiltra isAvailableForPriceCheck
              • → UIPriceCheckPage

Escrita — preços verificadosWrite — checked pricesEscritura — precios verificados

  • PriceCheckState.entriesMap<productSfid, PriceCheckEntry>
    • submitProductPriceCheckDispatcherPayloadInputentities cruas · 5 campos
      • buildDispatcherEnvelope1 envelope · sem fatiamento
        • SubmitProductPriceCheckUseCaseDispatcherOrchestrator
          • sendTransactionProductPriceCheckgRPC · Dispatcher · sfbatchapi
            • ackDispatchTransactionhistórico local

Os dois caminhos não se encontramThe two paths never meetLos dos caminos no se encuentran O envio não grava nada no domínio do catálogo nem em nenhuma box: os preços digitados nunca são persistidos, nem antes nem depois do envio. PriceCheckEntryEntity existe só na camada de domínio — não tem proto, DTO nem Model. A transação vive em 14 · ProductPriceCheck. Submission writes nothing into the catalog domain nor into any box: the typed prices are never persisted, before or after sending. PriceCheckEntryEntity exists only in the domain layer — it has no proto, no DTO and no Model. The transaction lives in 14 · ProductPriceCheck. El envío no graba nada en el dominio del catálogo ni en ninguna box: los precios escritos nunca se persisten, ni antes ni después del envío. PriceCheckEntryEntity existe solo en la capa de dominio — no tiene proto, DTO ni Model. La transacción vive en 14 · ProductPriceCheck.

07

Modelo de dadosData modelModelo de datos

O catálogo de produtos existe em quatro representações quase idênticas ao longo das camadas — Proto (wire gRPC) → DTO (Freezed) → Model (ObjectBox) → Entity (domínio) — e cada fronteira é atravessada por um mapper. Os nomes se mantêm em todas as camadas e o que muda é pouquíssimo: as relações viram ToOne/ToMany no Model, um campo é renomeado no Proto e o lastSyncAt não existe no wire. O fetch é write-through: todo retorno remoto é gravado no ObjectBox (a gravação limpa e regrava 11 boxes) e a tela passa a ler do cache.The product catalog exists in four near-identical representations across the layers — Proto (gRPC wire) → DTO (Freezed) → Model (ObjectBox) → Entity (domain) — and each boundary is crossed by a mapper. Names stay the same across layers and very little changes: relations become ToOne/ToMany in the Model, one field is renamed in the Proto, and lastSyncAt doesn't exist on the wire. Fetch is write-through: every remote response is written to ObjectBox (the write clears and rewrites 11 boxes) and the screen then reads from cache.El catálogo de productos existe en cuatro representaciones casi idénticas a lo largo de las capas — Proto (wire gRPC) → DTO (Freezed) → Model (ObjectBox) → Entity (dominio) — y cada frontera se cruza con un mapper. Los nombres se mantienen en todas las capas y cambia muy poco: las relaciones pasan a ToOne/ToMany en el Model, un campo se renombra en el Proto y el lastSyncAt no existe en el wire. El fetch es write-through: toda respuesta remota se graba en ObjectBox (la grabación limpia y regraba 11 boxes) y la pantalla lee del caché.

O catálogo chega num container ProductCatalogEntity de 2 campos (lastSyncAt gerado no mapper + products[]); cada item é um Product de 24 campos, com 9 sub-estruturas aninhadas. Desses 24 campos, a verificação de preço lê exatamente trêsproductSfid, name e categoryGroup — mais o bloco eligibility (para filtrar) e o lastSyncAt do container. Preço, unidade de medida, histórico de vendas, SOQ e SKUs de fabricação são carregados e ignorados por esta tela. Existe ainda uma quinta estrutura, PriceCheckEntry, que só existe no domínio: é o que o representante digita, e não tem proto, DTO nem Model. A seguir, na ordem: o proto, as estruturas de dados campo-a-campo por camada, e os mappers.The catalog arrives in a 2-field ProductCatalogEntity container (lastSyncAt generated in the mapper + products[]); each item is a 24-field Product with 9 nested sub-structures. Of those 24 fields, price check reads exactly threeproductSfid, name and categoryGroup — plus the eligibility block (to filter) and the container's lastSyncAt. Price, unit of measure, sales history, SOQ and manufacturing SKUs are loaded and ignored by this screen. There is also a fifth structure, PriceCheckEntry, which exists only in the domain: it is what the rep types, and it has no proto, no DTO and no Model. Next, in order: the proto, the field-by-field data structures per layer, and the mappers.El catálogo llega en un container ProductCatalogEntity de 2 campos (lastSyncAt generado en el mapper + products[]); cada ítem es un Product de 24 campos, con 9 sub-estructuras anidadas. De esos 24 campos, la verificación de precio lee exactamente tresproductSfid, name y categoryGroup — más el bloque eligibility (para filtrar) y el lastSyncAt del container. Precio, unidad de medida, historial de ventas, SOQ y SKUs de fabricación se cargan y se ignoran en esta pantalla. Existe además una quinta estructura, PriceCheckEntry, que solo existe en el dominio: es lo que el representante escribe, y no tiene proto, DTO ni Model. A continuación, en orden: el proto, las estructuras de datos campo a campo por capa, y los mappers.

Proto

ProductCatalogConectaRep.proto · proto3 · package mn.bat.conectarep.streambridge. Um serviço (ProductCatalogConectaRepService), um método unário, 12 messages e nenhum enum:One service (ProductCatalogConectaRepService), a single unary method, 12 messages and no enums:Un servicio (ProductCatalogConectaRepService), un método unario, 12 messages y ningún enum:

getProductCatalogunary
MétodoMethodMétodo

rpc getProductCatalog(ProductCatalogRequest) returns (ProductCatalogReply)

path /mn.bat.conectarep.streambridge.ProductCatalogConectaRepService/getProductCatalog

Request · ProductCatalogRequest
locationHierarchySfid
string · #1 · hierarquia do representante de vendas (resolvida no repository, §25)sales rep hierarchy (resolved in the repository, §25)jerarquía del representante de ventas (resuelta en el repository, §25)
dateReference
string · #2 · optional — o datasource aceita, e o único caller não passa (ver Pendências)optional — the datasource accepts it, and the only caller never passes it (see Pending items)optional — el datasource lo acepta, y el único caller no lo pasa (ver Pendientes)
lastModifiedDate
string · #3 · optional — não plumbado em camada nenhuma (ver Pendências)optional — not plumbed in any layer (see Pending items)optional — no plumbeado en ninguna capa (ver Pendientes)
Reply · ProductCatalogReply

repeated Product productso catálogo do mercado inteiro, sem recorte por varejo. Os 24 campos de Product e as 9 sub-estruturas estão detalhados nas Estruturas de dados abaixo.the whole market catalog, with no per-retail slice. Product's 24 fields and the 9 sub-structures are detailed in Data structures below.el catálogo del mercado entero, sin recorte por punto de venta. Los 24 campos de Product y las 9 sub-estructuras están detallados 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 destaque marca onde o tipo primeiro muda. ¹ = optional no proto. Todo Model tem ainda um @Id() int id que não aparece nas outras camadas e não está contado.One dropdown per structure, nested by hierarchy. Each table has one column per layer — Proto · DTO · Model · Entity; the highlighted text marks where the type first changes. ¹ = optional in the proto. Every Model also carries an @Id() int id that doesn't appear in the other layers and isn't counted.Un dropdown por estructura, anidados por jerarquía. Cada tabla tiene una columna por capa — Proto · DTO · Model · Entity; el texto destacado marca dónde primero cambia el tipo. ¹ = optional en el proto. Todo Model lleva además un @Id() int id que no aparece en las otras capas y no está contado.

  • ProductCatalog raiz 2 campos
    CampoProtoDTOModelEntity
    lastSyncAtDateTimeDateTime
    productsrepeated ProductList<…DTO>ToMany<…Model>List<…Entity>
    • Product ProductCatalog.products[] 24 campos
      CampoProtoDTOModelEntity
      productSfidstringStringStringString
      namestringStringStringString
      codestringStringStringString
      erpNumberstringStringStringString
      imageUrlstringStringStringString
      sequenceint32intintint
      categorystringStringStringString
      categoryGroupstringStringStringString
      brandFamilystringStringStringString
      materialGroupstringStringStringString
      brandVariantstringStringStringString
      packContentSizestringStringStringString
      isFreeOfChargeboolboolboolbool
      isVatRetentionbool¹bool?bool?bool?
      splitGroupstringStringStringString
      internalIdstringStringStringString
      invoiceUomstringStringStringString
      uomProductUom…DTOToOne<…Model>…Entity
      halfPackProductHalfPack…DTOToOne<…Model>…Entity
      eligibilityProductEligibility…DTOToOne<…Model>…Entity
      pricingByGrouprepeated PricingGroupList<…DTO>ToMany<…Model>List<…Entity>
      soqByAccountrepeated SoqByAccountList<…DTO>ToMany<…Model>List<…Entity>
      salesHistoryrepeated SalesHistoryList<…DTO>ToMany<…Model>List<…Entity>
      manufacturingSkusrepeated ManufacturingSkuList<…DTO>ToMany<…Model>List<…Entity>
      • ProductEligibility Product.eligibility · o filtro da feature 6 campos
        CampoProtoDTOModelEntity
        isAvailableForOrderboolboolboolbool
        isAvailableForBuybackboolboolboolbool
        isAvailableForVanLoadboolboolboolbool
        isAvailableForPriceCheckboolboolboolbool
        isAvailableForStockCountboolboolboolbool
        isAvailableForPromotionRewardbool¹bool?bool?bool?
      • ProductUom Product.uom 3 campos
        CampoProtoDTOModelEntity
        primaryNamestringStringStringString
        secondaryNamestringStringStringString
        conversionFactordoubledoubledoubledouble
      • ProductHalfPack Product.halfPack 2 campos
        CampoProtoDTOModelEntity
        allowedboolboolboolbool
        incrementdoubledoubledoubledouble
      • PricingGroup Product.pricingByGroup[] 2 campos
        CampoProtoDTOModelEntity
        pricingGroupIdstringStringStringString
        priceEntriesrepeated PriceEntryList<…DTO>ToMany<…Model>List<…Entity>
        • PriceEntry PricingGroup.priceEntries[] 10 campos
          CampoProtoDTOModelEntity
          priceEntryIdstringStringStringString
          manufacturingSkuIdmanufacturingSKUIdStringStringString
          manufacturingSkuBatchIdstringStringStringString
          validFromstringStringStringString
          validTostringStringStringString
          priceWithVatdoubledoubledoubledouble
          priceWithoutVatdoubledoubledoubledouble
          rrpWithVatdouble¹double?double?double?
          withholdingTaxdouble¹double?double?double?
          cashFeePercentagedouble¹double?double?double?
      • SoqByAccount Product.soqByAccount[] 2 campos
        CampoProtoDTOModelEntity
        accountSfidstringStringStringString
        valueint32intintint
      • SalesHistory Product.salesHistory[] 3 campos
        CampoProtoDTOModelEntity
        accountSfidstringStringStringString
        isStockCountboolboolboolbool
        recordsrepeated SalesHistoryRecordList<…DTO>ToMany<…Model>List<…Entity>
        • SalesHistoryRecord SalesHistory.records[] 4 campos
          CampoProtoDTOModelEntity
          dateReferencestringStringStringString
          ppqint32intintint
          psqstringStringStringString
          hasCountStockboolboolboolbool
      • ManufacturingSku Product.manufacturingSkus[] 5 campos
        CampoProtoDTOModelEntity
        manufacturingSkuSfidstringStringStringString
        batchIdstringStringStringString
        namestringStringStringString
        codestringStringStringString
        erpNumberstringStringStringString

A estrutura que o representante preenche não faz parte desta hierarquia e existe só no domínio:The structure the rep fills in is not part of this hierarchy and exists only in the domain:La estructura que el representante completa no forma parte de esta jerarquía y existe solo en el dominio:

  • PriceCheckEntry domain-only · nunca persistido 3 campos
    CampoProtoDTOModelEntity
    productSfidString
    packPricedouble?
    stickPricedouble?

Mappers

São 11 arquivos de mapper (um por estrutura do catálogo), cada um com as 5 direções como extension:There are 11 mapper files (one per catalog structure), each with the 5 directions as extensions:Son 11 archivos de mapper (uno por estructura del catálogo), cada uno con las 5 direcciones como extension:

DireçãoDirectionDirecciónMétodoMethodMétodo
JSON → DTOstatic fromMap(Map) (escrito à mão, sem json_serializable; defaults defensivos em todo campo, e objeto aninhado ausente cai para mapa vazio, não null)(hand-written, no json_serializable; defensive defaults on every field, and a missing nested object falls back to an empty map, not null)(escrito a mano, sin json_serializable; defaults defensivos en todo campo, y un objeto anidado ausente cae a mapa vacío, no null)
Proto → DTOtoDTO() (presença de optional lida pelos has*() gerados → null quando ausente)(optional presence read through the generated has*()null when absent)(presencia de optional leída por los has*() generados → null cuando ausente)
DTO → EntitytoDomain() (gera o lastSyncAt do container; roda num isolate via compute)(generates the container's lastSyncAt; runs in an isolate via compute)(genera el lastSyncAt del container; corre en un isolate vía compute)
Entity → ModeltoModel() (popula ToOne/ToMany depois de construir o model, porque as relações são final)(fills ToOne/ToMany after building the model, because relations are final)(llena ToOne/ToMany después de construir el model, porque las relaciones son final)
Model → EntitytoDomain() (desreferencia os 3 ToOne com !: alvo nulo vira exceção, tratada como Failure acima)(dereferences the 3 ToOne with !: a null target becomes an exception, handled as a Failure upstream)(desreferencia los 3 ToOne con !: un target nulo se vuelve excepción, tratada como Failure arriba)

Os únicos deltasThe only deltasLos únicos deltas

  • relações viram 3 ToOne (uom, halfPack, eligibility) e 7 ToMany no Model — 11 boxes no total, limpas e regravadas a cada fetchrelations become 3 ToOne (uom, halfPack, eligibility) and 7 ToMany in the Model — 11 boxes in total, cleared and rewritten on every fetchlas relaciones pasan a 3 ToOne (uom, halfPack, eligibility) y 7 ToMany en el Model — 11 boxes en total, limpiadas y regrabadas en cada fetch
  • renamerenamerename manufacturingSKUIdmanufacturingSkuId (no Proto; o mapper de JSON aceita as duas grafias)(in the Proto; the JSON mapper accepts both spellings)(en el Proto; el mapper de JSON acepta las dos grafías)
  • lastSyncAt não existe no wire — é gerado no mapper DTO→Entity com DateTimeUtils.now(), e o Model→Entity preserva o valor gravadodoesn't exist on the wire — it is generated in the DTO→Entity mapper with DateTimeUtils.now(), and Model→Entity preserves the stored valueno existe en el wire — se genera en el mapper DTO→Entity con DateTimeUtils.now(), y Model→Entity preserva el valor grabado
  • nenhum enum tipado em camada nenhumacategory, categoryGroup e invoiceUom seguem String nas quatro camadas; a conversão para CategoryGroup só acontece na apresentaçãono typed enum in any layercategory, categoryGroup and invoiceUom stay String across all four layers; conversion to CategoryGroup happens only in presentationningún enum tipado en capa algunacategory, categoryGroup e invoiceUom siguen String en las cuatro capas; la conversión a CategoryGroup solo ocurre en la presentación
  • nenhum parse de datavalidFrom, validTo e dateReference permanecem String até a Entity; o único DateTime do agregado é o lastSyncAtno date parsingvalidFrom, validTo and dateReference stay String all the way to the Entity; the aggregate's only DateTime is lastSyncAtningún parse de fechavalidFrom, validTo y dateReference permanecen String hasta la Entity; el único DateTime del agregado es el lastSyncAt
  • PriceCheckEntry não tem Proto, DTO nem Model — nasce e morre no domínio, dentro do State da telahas no Proto, DTO or Model — it is born and dies in the domain, inside the screen's Stateno tiene Proto, DTO ni Model — nace y muere en el dominio, dentro del State de la pantalla
  • nenhum campo do proto é descartado: os 24 campos de Product e os 65 campos das 12 messages atravessam as quatro camadas inteirosno proto field is dropped: Product's 24 fields and the 65 fields of the 12 messages cross all four layers intactningún campo del proto se descarta: los 24 campos de Product y los 65 campos de las 12 messages atraviesan las cuatro capas enteros
08

Repository

ProductCatalogRepositoryImpl implementaimplementsimplementa ProductCatalogRepositoryInterface e injeta os 3 datasources (mock/local/remote) + ConnectivityService + a flag useMockData + Ref. É um repository neutro: por §29 do CLAUDE.md, um agregado de catálogo compartilhado tem um único repository que não conhece nenhuma feature consumidora — a elegibilidade por feature vive nos UseCases. São 5 métodos na interface:and injects the 3 datasources (mock/local/remote) + ConnectivityService + the useMockData flag + Ref. It is a neutral repository: per CLAUDE.md §29, a shared catalog aggregate has one single repository that knows no consuming feature — per-feature eligibility lives in the UseCases. There are 5 interface methods:e inyecta los 3 datasources (mock/local/remote) + ConnectivityService + la flag useMockData + Ref. Es un repository neutro: por §29 del CLAUDE.md, un agregado de catálogo compartido tiene un único repository que no conoce ninguna feature consumidora — la elegibilidad por feature vive en los UseCases. Son 5 métodos en la interfaz:

getProductCatalog({source}) mock / local / remote

RetornaReturnsDevuelve Result<ProductCatalogEntity, Failure>

Único ponto de entrada do catálogo. Decide a fonte pela source + flags e, no caminho remoto, mapeia num isolate e grava no cache (write-through). É o método que a verificação de preço alcança através do seu UseCase.The catalog's only entry point. Picks the source from source + flags and, on the remote path, maps in an isolate and writes to cache (write-through). This is the method price check reaches through its UseCase.Único punto de entrada del catálogo. Elige la fuente por source + flags y, en el camino remoto, mapea en un isolate y graba en caché (write-through). Es el método que la verificación de precio alcanza a través de su UseCase.

Árvore de decisão de fonteSource decision treeÁrbol de decisión de fuente

  1. useMockData == true ouoro source == mock_fetchFromMock(): lê o asset, mapeia e devolve. Não grava no ObjectBox — em sessão mock o cache do catálogo fica vazio._fetchFromMock(): reads the asset, maps and returns. Does not write to ObjectBox — in a mock session the catalog cache stays empty._fetchFromMock(): lee el asset, mapea y devuelve. No graba en ObjectBox — en sesión mock el caché del catálogo queda vacío.
  2. source == local ou offlineor offlineu offline_fetchFromCacheOrFail(): cache não-nulo vira Success; cache vazio vira Error(NetworkFailure) — é o que produz a tela de erro na primeira abertura sem sincronização prévia. Este é o caminho da abertura normal da tela._fetchFromCacheOrFail(): a non-null cache becomes Success; an empty cache becomes Error(NetworkFailure) — this is what produces the error screen on a first open with no prior sync. This is the normal open path of the screen._fetchFromCacheOrFail(): un caché no nulo es Success; un caché vacío es Error(NetworkFailure) — es lo que produce la pantalla de error en la primera apertura sin sincronización previa. Este es el camino de la apertura normal de la pantalla.
  3. senão (remoto + conectado)otherwise (remote + connected)si no (remoto + conectado)_fetchFromRemoteWithFallback(): lê currentResourceProvider; se null cai pro cache; senão chama o remoto com locationHierarchyId, converte DTO→Entity num isolate (compute), grava no cache e devolve; em erro, fallback pro cache. É o caminho do pull-to-refresh e do sweep de frescor._fetchFromRemoteWithFallback(): reads currentResourceProvider; if null falls back to cache; else calls remote with locationHierarchyId, converts DTO→Entity in an isolate (compute), writes to cache and returns; on error, falls back to cache. This is the pull-to-refresh and freshness-sweep path._fetchFromRemoteWithFallback(): lee currentResourceProvider; si null cae al caché; si no llama al remoto con locationHierarchyId, convierte DTO→Entity en un isolate (compute), graba en caché y devuelve; en error, fallback al caché. Es el camino del pull-to-refresh y del sweep de frescura.
getCachedProductCatalog() local

RetornaReturnsDevuelve Result<ProductCatalogEntity?, Failure>

Só cache. null vira Success(null), não erro — quem chama trata "sem dados" sem falha. Falha de leitura é mapeada e logada em LogCategory.dataSync.Cache only. null becomes Success(null), not an error — callers handle "no data" without a failure. A read failure is mapped and logged under LogCategory.dataSync.Solo caché. null es Success(null), no error — quien llama trata "sin datos" sin fallo. Un fallo de lectura se mapea y se loguea en LogCategory.dataSync.

getCachedProductCatalogLastSyncAt() local

RetornaReturnsDevuelve Future<DateTime?> (sem Result)(no Result)(sin Result)

Timestamp da última sincronização do catálogo. Em erro, engole a falha e devolve null (só loga). Não alimenta a faixa da tela — quem a alimenta é o lastSyncAt que veio dentro da própria Entity (§23) — e sim o sweep de frescor de dados.The catalog's last-sync timestamp. On error it swallows the failure and returns null (it only logs). It doesn't feed the screen's strip — that comes from the lastSyncAt inside the Entity itself (§23) — it feeds the data-freshness sweep.Timestamp de última sincronización del catálogo. En error traga el fallo y devuelve null (solo loguea). No alimenta la franja de la pantalla — eso viene del lastSyncAt dentro de la propia Entity (§23) — sino el sweep de frescura de datos.

getCachedProductBySfid({productSfid}) local · sem chamadorno callersin llamador

RetornaReturnsDevuelve Result<ProductEntity?, Failure>

Lookup de item único por sfid no padrão §28 categoria A (cache-only): delega a getCachedProductCatalog() e faz varredura linear na lista de produtos. Nenhum consumidor no app hoje — nem a verificação de preço, nem outra feature (ver Pendências).Single-item lookup by sfid in the §28 category A pattern (cache-only): it delegates to getCachedProductCatalog() and does a linear scan over the product list. No consumer in the app today — neither price check nor any other feature (see Pending items).Lookup de ítem único por sfid en el patrón §28 categoría A (cache-only): delega a getCachedProductCatalog() y hace un barrido lineal en la lista de productos. Ningún consumidor en la app hoy — ni la verificación de precio ni otra feature (ver Pendientes).

saveProductCatalog({productCatalog}) local · destrutivodestructivedestructivo

RetornaReturnsDevuelve Result<void, Failure>

Cache-writer chamado após cada fetch remoto bem-sucedido. Limpa as 11 boxes na ordem filhas→raiz e regrava o agregado inteiro. Não há gravação incremental nem merge.The cache-writer called after each successful remote fetch. It clears the 11 boxes children→root and rewrites the whole aggregate. There is no incremental write and no merge.Cache-writer llamado tras cada fetch remoto exitoso. Limpia las 11 boxes en orden hijas→raíz y regraba el agregado entero. No hay grabación incremental ni merge.

09

Datasources

Um card por datasource (dropdown). No corpo: método, envio, retorno, fluxo de uso e tratamento de erro.One card per datasource (dropdown). In the body: method, what it sends, return, usage flow and error handling.Un card por datasource (dropdown). En el cuerpo: método, envío, retorno, flujo de uso y manejo de errores.

Remote ProductCatalogRemoteDataSource gRPC
getProductCatalog({locationHierarchySfid, dateReference?})
EnvioSendsEnvío
monta ProductCatalogRequest com locationHierarchySfid, e só acrescenta dateReference se ele vier não-nulo; lastModifiedDate nunca é preenchido. Chama _client.getProductCatalog(request) no ProductCatalogConectaRepServiceClient (canal streambridge).builds ProductCatalogRequest with locationHierarchySfid, and only adds dateReference if it arrives non-null; lastModifiedDate is never set. Calls _client.getProductCatalog(request) on ProductCatalogConectaRepServiceClient (streambridge channel).arma ProductCatalogRequest con locationHierarchySfid, y solo agrega dateReference si llega no nulo; lastModifiedDate nunca se completa. Llama _client.getProductCatalog(request) en ProductCatalogConectaRepServiceClient (canal streambridge).
RetornoReturnRetorno
ProductCatalogDTO (via response.toDTO() — só Proto→DTO, sem conversão de domínio aqui)(via response.toDTO() — Proto→DTO only, no domain conversion here)(vía response.toDTO() — solo Proto→DTO, sin conversión de dominio aquí)
Fluxo de usoUsage flowFlujo de uso
chamado só pelo caminho remoto do repository, quando online e sem mock. O DTO é convertido num isolate e o resultado é gravado no cache.called only by the repository's remote path, when online and not mocking. The DTO is converted in an isolate and the result is written to cache.llamado solo por el camino remoto del repository, online y sin mock. El DTO se convierte en un isolate y el resultado se graba en caché.
Tratamento de erroError handlingManejo de errores
GrpcErrorGrpcExceptionHandler; outros → ServerException. Em erro, o repository faz fallback pro cache.GrpcErrorGrpcExceptionHandler; others → ServerException. On error the repository falls back to cache.GrpcErrorGrpcExceptionHandler; otros → ServerException. En error el repository hace fallback al caché.
Local ProductCatalogLocalDataSource ObjectBox · 5

Envio / fluxo: persistência local via ObjectBox, 11 boxes (raiz + produto + as 9 sub-estruturas) — sem rede. Todos os métodos são síncronos (não retornam Future). Alimenta os caminhos cache do repository. Erro: cada método lança CacheException com mensagem própria; nada é engolido.Sends / flow: local persistence via ObjectBox, 11 boxes (root + product + the 9 sub-structures) — no network. All methods are synchronous (they return no Future). Feeds the repository's cache paths. Error: each method throws a CacheException with its own message; nothing is swallowed.Envío / flujo: persistencia local vía ObjectBox, 11 boxes (raíz + producto + las 9 sub-estructuras) — sin red. Todos los métodos son síncronos (no devuelven Future). Alimenta los caminos caché del repository. Error: cada método lanza CacheException con mensaje propio; nada se traga.

getProductCatalog()
RetornoReturnRetorno
ProductCatalogEntity?
ComportamentoBehaviorComportamiento
models.first.toDomain() — o agregado único, ou null se o cache está vazio.models.first.toDomain() — the single aggregate, or null if the cache is empty.models.first.toDomain() — el agregado único, o null si el caché está vacío.
getProductCatalogLastSyncAt()
RetornoReturnRetorno
DateTime?
ComportamentoBehaviorComportamiento
models.first.lastSyncAt — sem desserializar o agregado inteiro.— without deserializing the whole aggregate.— sin deserializar el agregado entero.
saveProductCatalog({entity})
RetornoReturnRetorno
void
ComportamentoBehaviorComportamiento
destrutivo: clearProductCatalog() + _catalogBox.put(entity.toModel()) (grava as boxes filhas em cascata).destructive: clearProductCatalog() + _catalogBox.put(entity.toModel()) (writes child boxes in cascade).destructivo: clearProductCatalog() + _catalogBox.put(entity.toModel()) (graba las boxes hijas en cascada).
mergeAdhocProductCatalog({incoming, accountSfid})
RetornoReturnRetorno
void
ComportamentoBehaviorComportamiento
funde os produtos que chegam de uma visita ad hoc no catálogo já gravado (via AdhocProductCatalogMerge) e regrava. Não é exposto na interface do repository — o fluxo ad hoc entra por aqui, direto no datasource.merges the products arriving from an ad hoc visit into the already-stored catalog (via AdhocProductCatalogMerge) and rewrites it. Not exposed on the repository interface — the ad hoc flow comes in here, straight into the datasource.fusiona los productos que llegan de una visita ad hoc en el catálogo ya grabado (vía AdhocProductCatalogMerge) y regraba. No se expone en la interfaz del repository — el flujo ad hoc entra por aquí, directo al datasource.
clearProductCatalog()
RetornoReturnRetorno
void
ComportamentoBehaviorComportamiento
limpa as 11 boxes em ordem filhas→raiz: entradas de preço, grupos de preço, SOQ, registros de histórico, histórico, SKUs de fabricação, unidade de medida, meio-maço, elegibilidade, produto e por fim o container.clears the 11 boxes children→root: price entries, pricing groups, SOQ, history records, history, manufacturing SKUs, unit of measure, half pack, eligibility, product and finally the container.limpia las 11 boxes en orden hijas→raíz: entradas de precio, grupos de precio, SOQ, registros de historial, historial, SKUs de fabricación, unidad de medida, medio paquete, elegibilidad, producto y por fin el container.
Mock ProductCatalogMockDataSource JSON
getProductCatalog()
EnvioSendsEnvío
carrega o asset product_catalog/jsons/{mercado}_products.json — ou {mercado}_real_products.json quando a flag de mock real está ligada — e decodifica num isolate. Sem rede. O resultado é memoizado em memória: a segunda chamada não relê o asset.loads the asset product_catalog/jsons/{market}_products.json — or {market}_real_products.json when the real-mock flag is on — and decodes it in an isolate. No network. The result is memoized in memory: a second call doesn't re-read the asset.carga el asset product_catalog/jsons/{mercado}_products.json — o {mercado}_real_products.json cuando la flag de mock real está activa — y lo decodifica en un isolate. Sin red. El resultado se memoiza en memoria: la segunda llamada no relee el asset.
RetornoReturnRetorno
ProductCatalogDTO (via ProductCatalogDTOMapper.fromMap)(via ProductCatalogDTOMapper.fromMap)(vía ProductCatalogDTOMapper.fromMap)
Fluxo de usoUsage flowFlujo de uso
usado quando useMockData está ligado ou source == mock. Ao contrário dos outros caminhos, não grava no cache.used when useMockData is on or source == mock. Unlike the other paths, it does not write to cache.usado cuando useMockData está activo o source == mock. A diferencia de los otros caminos, no graba en caché.
Tratamento de erroError handlingManejo de errores
a memoização é desfeita e a falha vira CacheException. Nuance por modo: no mock real, asset ausente devolve "{}" em silêncio (catálogo vazio, sem erro) — é o que acontece em AR/PY/PE; no mock sintético, asset ausente lança.the memoization is dropped and the failure becomes a CacheException. Nuance per mode: in real mock, a missing asset silently returns "{}" (empty catalog, no error) — that's what happens in AR/PY/PE; in the synthetic mock a missing asset throws.la memoización se deshace y el fallo se vuelve CacheException. Matiz por modo: en mock real, un asset ausente devuelve "{}" en silencio (catálogo vacío, sin error) — es lo que ocurre en AR/PY/PE; en el mock sintético un asset ausente lanza.
10

Enums e labelsEnums & labelsEnums y labels

O agregado do catálogo não tipa enum em camada nenhuma — os enums entram só na apresentação e no transporte da escrita. São três, com todos os valores:The catalog aggregate types no enum in any layer — enums only come in at presentation and at the write transport. There are three, with all their values:El agregado del catálogo no tipa enum en capa alguna — los enums entran solo en la presentación y en el transporte de la escritura. Son tres, con todos sus valores:

CategoryGroup 9 valores · agrupa o resumo9 values · groups the summary9 valores · agrupa el resumen
casevalueshortLabel
fmc"fmc"FMC
nc"nc"NC
ryo"ryo"RYO
modi"modi"VUSE
oral"oral"Oral
otp"otp"OTP
partnership"partnership"Partnership
other"other"Other
unknown""""

É o único enum que a feature realmente aplica: o painel expandido do resumo agrupa por product.categoryGroup e ordena as categorias pela posição no enum. O parser (fromValue) compara em minúsculas e cai em unknown sem log e sem assert; como unknown é o último valor, categoria desconhecida vai para o fim da lista. O rótulo vem de tradução por caso e, quando o rótulo é vazio, o app mostra o valor cru em maiúsculas — ou seja, uma categoria que o enum não conhece aparece na tela, mas sem nome traduzido.It is the only enum the feature actually applies: the expanded summary panel groups by product.categoryGroup and orders categories by their position in the enum. The parser (fromValue) compares lowercased and falls back to unknown with no log and no assert; since unknown is the last value, an unknown category goes to the end of the list. The label comes from a per-case translation and, when the label is empty, the app shows the raw value uppercased — i.e. a category the enum doesn't know still shows on screen, but without a translated name.Es el único enum que la feature realmente aplica: el panel expandido del resumen agrupa por product.categoryGroup y ordena las categorías por su posición en el enum. El parser (fromValue) compara en minúsculas y cae en unknown sin log y sin assert; como unknown es el último valor, una categoría desconocida va al final de la lista. El rótulo viene de una traducción por caso y, cuando el rótulo es vacío, la app muestra el valor crudo en mayúsculas — es decir, una categoría que el enum no conoce aparece en pantalla, pero sin nombre traducido.

DataSourceType 3
caseuso na featureuse in the featureuso en la feature
mockforça o asset JSON; a flag global useMockData tem o mesmo efeitoforces the JSON asset; the global useMockData flag has the same effectfuerza el asset JSON; la flag global useMockData tiene el mismo efecto
localdefault — é o que a abertura da tela usa (cache-only)default — this is what opening the screen uses (cache-only)default — es lo que usa la apertura de la pantalla (cache-only)
remotesó o pull-to-refresh (e o sweep de frescor, por fora da tela)pull-to-refresh only (and the freshness sweep, outside the screen)solo el pull-to-refresh (y el sweep de frescura, fuera de la pantalla)

Enum simples: sem value de wire e sem fromString — nunca vem do backend.A plain enum: no wire value and no fromString — it never comes from the backend.Enum simple: sin value de wire y sin fromString — nunca viene del backend.

DispatcherType · priceCheck 1 de 42 valores1 of 42 values1 de 42 valores
PropriedadePropertyPropiedadValorValueValor
serviceName"ProductPriceCheck"
destinationbatchApi → wire "sfbatchapi"
enabledMarkets[ZA]
resendMayDuplicatefalse
hasPromotionfalse fixo no builder → o serviceName nunca ganha o prefixo Promo_false hardcoded in the builder → the serviceName never gets the Promo_ prefixfalse fijo en el builder → el serviceName nunca recibe el prefijo Promo_

resendMayDuplicate é derivado, não declarado: é false só para os 3 tipos do conjunto lightweightnotificationRead, answerTask e priceCheck. Os outros 39 tipos avisam que o reenvio pode duplicar. Detalhes do payload em 14 · ProductPriceCheck. Também vale notar que enabledMarkets é metadado declarativo: nenhuma regra do app o lê — quem realmente esconde a feature fora da África do Sul é o gate do EMC.resendMayDuplicate is derived, not declared: it is false only for the 3 types in the lightweight set — notificationRead, answerTask and priceCheck. The other 39 types warn that a resend may duplicate. Payload details in 14 · ProductPriceCheck. Also worth noting that enabledMarkets is declarative metadata: no app rule reads it — what actually hides the feature outside South Africa is the EMC gate.resendMayDuplicate es derivado, no declarado: es false solo para los 3 tipos del conjunto lightweightnotificationRead, answerTask y priceCheck. Los otros 39 tipos avisan que el reenvío puede duplicar. Detalles del payload en 14 · ProductPriceCheck. También vale notar que enabledMarkets es metadato declarativo: ninguna regla de la app lo lee — lo que realmente esconde la feature fuera de Sudáfrica es el gate del EMC.

Existem no projeto dois enums que parecem pertencer a este agregado e não são aplicados em camada nenhuma dele: ProductCategory (5 valores, sem fromString) e UnitOfMeasure (5 valores, sem fromString). Os campos correspondentes — category, invoiceUom, primaryName/secondaryName — trafegam como String crua até a Entity.The project has two enums that look like they belong to this aggregate and are not applied in any of its layers: ProductCategory (5 values, no fromString) and UnitOfMeasure (5 values, no fromString). The matching fields — category, invoiceUom, primaryName/secondaryName — travel as raw String all the way to the Entity.Existen en el proyecto dos enums que parecen pertenecer a este agregado y no se aplican en ninguna de sus capas: ProductCategory (5 valores, sin fromString) y UnitOfMeasure (5 valores, sin fromString). Los campos correspondientes — category, invoiceUom, primaryName/secondaryName — viajan como String cruda hasta la Entity.

11

UseCases

A feature usa três UseCases: um para ler o catálogo filtrado, um para montar o payload e um para despachar. Todos com provider keepAlive. O de leitura é a materialização da §29 do CLAUDE.md: o catálogo tem um repository neutro e um UseCase por feature consumidora — hoje são 7 UseCases sobre o mesmo repository (pedido, recompra, carga de van, verificação de preço, contagem de estoque, prêmio de promoção e o de passagem sem filtro), cada um filtrando pela sua flag de elegibilidade.The feature uses three UseCases: one to read the filtered catalog, one to build the payload and one to dispatch. All with a keepAlive provider. The read one is CLAUDE.md §29 made concrete: the catalog has one neutral repository and one UseCase per consuming feature — today 7 UseCases sit on the same repository (order, buyback, van load, price check, stock count, promotion reward and the unfiltered pass-through), each filtering on its own eligibility flag.La feature usa tres UseCases: uno para leer el catálogo filtrado, uno para armar el payload y uno para despachar. Todos con provider keepAlive. El de lectura es la materialización de la §29 del CLAUDE.md: el catálogo tiene un repository neutro y un UseCase por feature consumidora — hoy son 7 UseCases sobre el mismo repository (pedido, recompra, carga de van, verificación de precio, conteo de stock, premio de promoción y el de paso sin filtro), cada uno filtrando por su flag de elegibilidad.

GetProductsForPriceCheckUseCase 1 · a listathe listla lista
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
execute({source = local})Result<ProductCatalogEntity, Failure>Chama repository.getProductCatalog(source:) e devolve o container inteiro com copyWith(products: …where(eligibility.isAvailableForPriceCheck)). Devolver o container e não a lista crua é deliberado: assim o lastSyncAt sobrevive ao filtro e alimenta a faixa de sincronização da tela (§23). Único chamador: o PriceCheckNotifier.Calls repository.getProductCatalog(source:) and returns the whole container with copyWith(products: …where(eligibility.isAvailableForPriceCheck)). Returning the container instead of the bare list is deliberate: that way lastSyncAt survives the filter and feeds the screen's sync strip (§23). Sole caller: the PriceCheckNotifier.Llama repository.getProductCatalog(source:) y devuelve el container entero con copyWith(products: …where(eligibility.isAvailableForPriceCheck)). Devolver el container y no la lista cruda es deliberado: así el lastSyncAt sobrevive al filtro y alimenta la franja de sincronización de la pantalla (§23). Único llamador: el PriceCheckNotifier.

O filtro é a flag de elegibilidade. Não há recorte por varejo, por preço, por estoque nem por categoria — ao contrário da vitrine de pedido, que esconde produto de preço zero (a não ser que seja bonificado). Aqui um produto sem preço no catálogo aparece normalmente, e é coerente: a tela coleta o preço do varejo, não o da BAT.The filter is only the eligibility flag. There is no slice by retail, price, stock or category — unlike the order showcase, which hides zero-price products (unless free of charge). Here a product with no price in the catalog shows up normally, and that is consistent: the screen collects the retail's price, not BAT's.El filtro es solo la flag de elegibilidad. No hay recorte por punto de venta, por precio, por stock ni por categoría — a diferencia de la vitrina de pedido, que esconde productos de precio cero (salvo que sean bonificados). Aquí un producto sin precio en el catálogo aparece normalmente, y es coherente: la pantalla recoge el precio del punto de venta, no el de BAT.

BuildProductPriceCheckDispatcherPayloadUseCase 1 · payload
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
build({input})DispatcherEnvelopeContrato do DispatcherPayloadBuilder e único método da classe — não existe buildAll e não há fatiamento: sai um envelope só, com todos os itens num array. É síncrono e const. Toda construção wire mora aqui (§36): a formatação da data, o cruzamento entre preços e produtos, e a decisão de omitir produto sem preço.The DispatcherPayloadBuilder contract and the class's only method — there is no buildAll and no chunking: one single envelope comes out, with every item in one array. It is synchronous and const. All wire construction lives here (§36): date formatting, the join between prices and products, and the decision to omit a product with no price.Contrato del DispatcherPayloadBuilder y único método de la clase — no existe buildAll y no hay corte: sale un solo envelope, con todos los ítems en un array. Es síncrono y const. Toda construcción wire vive aquí (§36): el formateo de la fecha, el cruce entre precios y productos, y la decisión de omitir un producto sin precio.

O inputThe inputEl inputProductPriceCheckDispatcherPayloadInput, Freezed, 5 campos, todos obrigatórios e sem default:Freezed, 5 fields, all required and with no default:Freezed, 5 campos, todos obligatorios y sin default:

accountSfid
String · identificador do varejo, vindo da rotaretail identifier, from the routeidentificador del punto de venta, desde la ruta
accountCode
String · código SAP do varejo, vindo da rotaretail SAP code, from the routecódigo SAP del punto de venta, desde la ruta
entries
List<PriceCheckEntryEntity> · entity crua, 3 campos cadaraw entity, 3 fields eachentity cruda, 3 campos cada una
products
List<ProductEntity> · entity crua, os 24 campos do catálogoraw entity, the catalog's 24 fieldsentity cruda, los 24 campos del catálogo
submittedAt
DateTime · o relógio injetado pelo Notifier, não uma data já formatada — exatamente o que a §36 exigethe clock injected by the Notifier, not a pre-formatted date — exactly what §36 requiresel reloj inyectado por el Notifier, no una fecha ya formateada — exactamente lo que la §36 exige

O que o payload carregaWhat the payload carriesQué lleva el payloaduma chave raiz, "ProductPriceCheck", com um array de 7 chaves por item:one root key, "ProductPriceCheck", holding an array of 7 keys per item:una clave raíz, "ProductPriceCheck", con un array de 7 claves por ítem:

Chave JSONJSON keyClave JSONOrigemOriginOrigen
accountSfidinput.accountSfid (cru, repetido em todo item)(raw, repeated on every item)(crudo, repetido en todo ítem)
accountCodeinput.accountCode (cru, repetido em todo item)(raw, repeated on every item)(crudo, repetido en todo ítem)
productSfidproduct.productSfid
productNameproduct.name
packsSellingPriceentry.packPrice ?? 0.0
sticksSellingPriceentry.stickPrice ?? 0.0
priceCheckDatecalculado — submittedAt formatado como yyyy-MM-dd, a mesma string em todo itemcomputed — submittedAt formatted as yyyy-MM-dd, the same string on every itemcalculado — submittedAt formateado como yyyy-MM-dd, la misma cadena en todo ítem

O builder itera os produtos (não as entradas), então a ordem dos itens segue a ordem do catálogo; um produto sem entrada, ou com as duas colunas vazias, é pulado. O envelope leva ainda o serviceName "ProductPriceCheck", o varejo (sfid + código SAP), a mesma data em dateReference e o accountSfid como referência da transação. A tabela completa campo-a-campo com tipos e regras e o exemplo de JSON estão em 14 · ProductPriceCheck.The builder iterates the products (not the entries), so item order follows the catalog order; a product with no entry, or with both columns empty, is skipped. The envelope also carries the serviceName "ProductPriceCheck", the retail (sfid + SAP code), the same date in dateReference and the accountSfid as the transaction reference. The full field-by-field table with types and rules, plus the JSON example, is in 14 · ProductPriceCheck.El builder itera los productos (no las entradas), así que el orden de los ítems sigue el orden del catálogo; un producto sin entrada, o con las dos columnas vacías, se omite. El envelope lleva además el serviceName "ProductPriceCheck", el punto de venta (sfid + código SAP), la misma fecha en dateReference y el accountSfid como referencia de la transacción. La tabla completa campo a campo con tipos y reglas y el ejemplo de JSON están en 14 · ProductPriceCheck.

SubmitProductPriceCheckUseCase 1 · enviosubmitenvío
MétodoMethodMétodoRetornaReturnsDevuelveUsoUseUso
submit({envelope})Future<Result<DispatcherAck, Failure>>Delegação pura ao DispatcherOrchestrator.dispatch — não monta payload e não conhece o tipo, que vem dentro do envelope. Como só existe um envelope, não há Future.wait nem alinhamento por índice (ao contrário de pesquisas e merchandising).Pure delegation to DispatcherOrchestrator.dispatch — it builds no payload and doesn't know the type, which comes inside the envelope. Since there is only one envelope, there is no Future.wait and no index alignment (unlike surveys and merchandising).Delegación pura al DispatcherOrchestrator.dispatch — no arma payload y no conoce el tipo, que viene dentro del envelope. Como solo existe un envelope, no hay Future.wait ni alineación por índice (a diferencia de encuestas y merchandising).

O orquestrador é quem grava o registro no histórico local de despachos, com o tipo, o serviceName, a referência, o mercado e o estado do envio. O caminho é remote-first por construção (§36): a escrita não tem datasource local nenhum — o repository do Dispatcher só conhece o gateway gRPC, e a única persistência é o registro de auditoria gravado depois que a chamada remota resolve. Em caso de sucesso, o payload é apagado do registro, então um envio bem-sucedido não pode ser reenviado; em erro o payload é preservado e o reenvio manual fica disponível.The orchestrator is what writes the record into the local dispatch history, with the type, the serviceName, the reference, the market and the send state. The path is remote-first by construction (§36): the write has no local datasource at all — the Dispatcher repository knows only the gRPC gateway, and the only persistence is the audit record written after the remote call resolves. On success the payload is erased from the record, so a successful submission cannot be resent; on error the payload is preserved and manual resend is available.El orquestador es quien graba el registro en el historial local de despachos, con el tipo, el serviceName, la referencia, el mercado y el estado del envío. El camino es remote-first por construcción (§36): la escritura no tiene datasource local alguno — el repository del Dispatcher solo conoce el gateway gRPC, y la única persistencia es el registro de auditoría grabado después de que la llamada remota resuelve. En caso de éxito el payload se borra del registro, así que un envío exitoso no puede reenviarse; en error el payload se preserva y el reenvío manual queda disponible.

12

Notifier & State

O PriceCheckNotifier (@riverpod, with AsyncGuard<PriceCheckState>) é o cérebro da tela e é uma family chaveada por accountSfid + accountCode. O build() é magro: observa os 3 UseCases e devolve _load() dentro do guardedBuild. O State (PriceCheckState, Freezed) é a fonte única de verdade: guarda o catálogo filtrado, o mapa de preços digitados (chaveado por productSfid), a paginação e a flag de envio em curso. Todos os totais, contagens e o agrupamento por categoria são calculados em getters do State. Segue a §37 — build() magro, _load() como dono único da montagem, refresh() sem sufixo de feature, sem invalidateSelf e sem AsyncValue.loading no refresh.The PriceCheckNotifier (@riverpod, with AsyncGuard<PriceCheckState>) is the screen's brain and is a family keyed by accountSfid + accountCode. build() is thin: it watches the 3 UseCases and returns _load() inside guardedBuild. The State (PriceCheckState, Freezed) is the single source of truth: it holds the filtered catalog, the map of typed prices (keyed by productSfid), pagination and the in-flight submit flag. Every total, count and the per-category grouping are computed in State getters. It follows §37 — thin build(), _load() as sole owner of assembly, refresh() with no feature suffix, no invalidateSelf and no AsyncValue.loading on refresh.El PriceCheckNotifier (@riverpod, with AsyncGuard<PriceCheckState>) es el cerebro de la pantalla y es una family indexada por accountSfid + accountCode. El build() es delgado: observa los 3 UseCases y devuelve _load() dentro del guardedBuild. El State (PriceCheckState, Freezed) es la fuente única de verdad: guarda el catálogo filtrado, el mapa de precios escritos (indexado por productSfid), la paginación y la flag de envío en curso. Todos los totales, conteos y el agrupamiento por categoría se calculan en getters del State. Sigue la §37 — build() delgado, _load() como dueño único del armado, refresh() sin sufijo de feature, sin invalidateSelf y sin AsyncValue.loading en el refresh.

MétodosMethodsMétodos

build({accountSfid, accountCode}) @override

RetornoReturnRetorno FutureOr<PriceCheckState>

Observa os providers dos 3 UseCases e devolve guardedBuild(body: () => _load(...)). Nada é montado aqui. Não escuta nenhum tipo de sincronização em background — ao contrário da lista de pedidos, esta tela não se atualiza sozinha quando o sweep grava um catálogo novo.Watches the 3 UseCase providers and returns guardedBuild(body: () => _load(...)). Nothing is assembled here. It listens to no background sync type — unlike the order list, this screen doesn't refresh itself when the sweep writes a new catalog.Observa los providers de los 3 UseCases y devuelve guardedBuild(body: () => _load(...)). Nada se arma aquí. No escucha ningún tipo de sincronización en background — a diferencia de la lista de pedidos, esta pantalla no se actualiza sola cuando el sweep graba un catálogo nuevo.

_load({accountSfid, accountCode, source = local}) private

RetornoReturnRetorno Future<PriceCheckState>

Dono único da montagem do State: execute(source:) + getOrThrow() (a falha sobe como Failure e o AsyncGuard a converte em estado de erro), e monta products, lastSyncAt e o visibleCount inicial — o menor entre 20 e o total de produtos. Não mexe no ref, o que é o que permite o refresh() não forçar um novo build().Sole owner of building the State: execute(source:) + getOrThrow() (the failure bubbles up as a Failure and AsyncGuard turns it into an error state), and assembles products, lastSyncAt and the initial visibleCountthe smaller of 20 and the product total. It doesn't touch ref, which is what lets refresh() avoid forcing a new build().Dueño único del armado del State: execute(source:) + getOrThrow() (el fallo sube como Failure y el AsyncGuard lo convierte en estado de error), y arma products, lastSyncAt y el visibleCount inicial — el menor entre 20 y el total de productos. No toca el ref, que es lo que permite que el refresh() no fuerce un nuevo build().

refresh() pull-to-refresh

RetornoReturnRetorno Future<void>

Null-guard em state.value, depois runGuarded em volta de _load(source: remote) com fresh.copyWith(entries: current.entries). Preserva os preços digitados e só eles: a paginação volta ao início, então uma lista longa já rolada volta a mostrar 20 itens. Não seta AsyncValue.loading.Null-guard on state.value, then runGuarded around _load(source: remote) with fresh.copyWith(entries: current.entries). It preserves the typed prices and nothing else: pagination goes back to the start, so a long, already-scrolled list returns to 20 items. It doesn't set AsyncValue.loading.Null-guard en state.value, luego runGuarded alrededor de _load(source: remote) con fresh.copyWith(entries: current.entries). Preserva los precios escritos y solo eso: la paginación vuelve al inicio, así que una lista larga ya desplazada vuelve a mostrar 20 ítems. No setea AsyncValue.loading.

loadMore()

RetornoReturnRetorno Future<void>

Guarda contra state nulo e contra "não há mais o que carregar", espera 600 ms de propósito (para o indicador de carregamento ser visível), relê o state e repete as duas guardas, e então soma 20 ao visibleCount com clamp no total. Paginação puramente visual — os produtos já estão todos em memória.Guards against a null state and against "nothing more to load", waits 600 ms on purpose (so the loading indicator is visible), re-reads the state and repeats both guards, and then adds 20 to visibleCount with a clamp at the total. Purely visual pagination — all products are already in memory.Guarda contra state nulo y contra "no hay más que cargar", espera 600 ms a propósito (para que el indicador de carga sea visible), relee el state y repite las dos guardas, y luego suma 20 al visibleCount con clamp en el total. Paginación puramente visual — los productos ya están todos en memoria.

updatePackPrice({productSfid, price})

RetornoReturnRetorno void

Copia o mapa de entradas, pega ou cria a entrada daquele produto e aplica o novo preço do maço. Se as duas colunas ficarem nulas, a entrada é removida do mapa em vez de ficar como registro vazio — é essa regra que faz "campo apagado" e "nunca preenchido" serem indistinguíveis a jusante.Copies the entries map, gets or creates that product's entry and applies the new pack price. If both columns end up null, the entry is removed from the map instead of lingering as an empty record — this rule is what makes "cleared field" and "never filled" indistinguishable downstream.Copia el mapa de entradas, toma o crea la entrada de ese producto y aplica el nuevo precio del paquete. Si las dos columnas quedan nulas, la entrada se elimina del mapa en vez de quedar como registro vacío — esa regla es la que hace que "campo borrado" y "nunca completado" sean indistinguibles hacia abajo.

updateStickPrice({productSfid, price})

RetornoReturnRetorno void

Espelho exato do método do maço, aplicado ao preço da unidade — mesma criação sob demanda e mesma remoção quando as duas colunas se esvaziam.An exact mirror of the pack method, applied to the stick price — same on-demand creation and same removal when both columns empty out.Espejo exacto del método del paquete, aplicado al precio de la unidad — misma creación bajo demanda y misma eliminación cuando las dos columnas se vacían.

clearAllEntries()

RetornoReturnRetorno void

Guarda contra state nulo e contra mapa já vazio, e substitui as entradas por um mapa vazio. Os campos de texto da lista se limpam por reação a isso — inclusive o que estiver em foco, que é a única situação em que o app sobrepõe o que o dedo está digitando.Guards against a null state and an already-empty map, and replaces the entries with an empty map. The list's text fields clear in reaction to that — including one that has focus, which is the only situation where the app overrides what the finger is typing.Guarda contra state nulo y contra mapa ya vacío, y reemplaza las entradas por un mapa vacío. Los campos de texto de la lista se limpian por reacción a eso — incluso el que esté enfocado, que es la única situación en que la app sobreescribe lo que el dedo está escribiendo.

submit() enviosubmitenvío

RetornoReturnRetorno Future<Failure?>null significa sucessonull means successnull significa éxito

Sequência: guarda contra state nulo e contra "nada preenchido" (as duas devolvem a mesma falha genérica) → liga isSubmitting → monta o input com as entities cruas e o relógio → chama o builder → despacha pelo Submit…UseCase → em sucesso desliga isSubmitting, liga submitSucceeded e devolve null; em erro desliga isSubmitting e devolve a Failure. Quem decide o que mostrar é o widget (§39: o disparo mora aqui, o aviso e a navegação ficam na UI).Sequence: guard against a null state and against "nothing filled" (both return the same generic failure) → turn isSubmitting on → build the input with the raw entities and the clock → call the builder → dispatch through the Submit…UseCase → on success turn isSubmitting off, set submitSucceeded and return null; on error turn isSubmitting off and return the Failure. What to show is the widget's call (§39: the trigger lives here, the notice and the navigation stay in the UI).Secuencia: guarda contra state nulo y contra "nada completado" (las dos devuelven el mismo fallo genérico) → enciende isSubmitting → arma el input con las entities crudas y el reloj → llama al builder → despacha por el Submit…UseCase → en éxito apaga isSubmitting, enciende submitSucceeded y devuelve null; en error apaga isSubmitting y devuelve la Failure. Qué mostrar lo decide el widget (§39: el disparo vive aquí, el aviso y la navegación quedan en la UI).

Detalhe que importa: o State escrito no fim é uma cópia do snapshot capturado antes do await, não do state corrente. Qualquer preço digitado ou limpeza que aconteça durante o envio é revertido quando o envio termina (ver Pendências).A detail that matters: the State written at the end is a copy of the snapshot captured before the await, not of the current state. Any price typed or cleared while the submission is in flight is reverted when it finishes (see Pending items).Un detalle que importa: el State escrito al final es una copia del snapshot capturado antes del await, no del state corriente. Cualquier precio escrito o borrado durante el envío se revierte cuando el envío termina (ver Pendientes).

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

PriceCheckState 8 campos + 11 getters8 fields + 11 getters8 campos + 11 getters
campotipodefault
accountSfidString""
accountCodeString""
productsList<ProductEntity>[]
visibleCountint10
entriesMap<String, PriceCheckEntryEntity>{}
isSubmittingboolfalse
submitSucceededboolfalse
lastSyncAtDateTime?null

Getters (11): visibleProducts (recorte por visibleCount), totalProducts, hasMoreToLoad, filledCount, packsFilledCount, sticksFilledCount, packsTotalValue, sticksTotalValue, hasAnyEntry, emptyCount e filledProductsByCategory (agrupa os preenchidos por categoryGroup e ordena as chaves pela posição no enum). O State não filtra nem ordena a lista de produtos: ela chega pronta do UseCase. Nenhum campo é required — o default visibleCount: 10 nunca é exercitado, porque _load sempre o define como min(20, total); e submitSucceeded, packsFilledCount e sticksFilledCount não têm consumidor (ver Pendências).Getters (11): visibleProducts (slice by visibleCount), totalProducts, hasMoreToLoad, filledCount, packsFilledCount, sticksFilledCount, packsTotalValue, sticksTotalValue, hasAnyEntry, emptyCount and filledProductsByCategory (groups the filled ones by categoryGroup and orders keys by enum position). The State neither filters nor sorts the product list: it arrives ready from the UseCase. No field is required — the visibleCount: 10 default is never exercised, because _load always sets it to min(20, total); and submitSucceeded, packsFilledCount and sticksFilledCount have no consumer (see Pending items).Getters (11): visibleProducts (recorte por visibleCount), totalProducts, hasMoreToLoad, filledCount, packsFilledCount, sticksFilledCount, packsTotalValue, sticksTotalValue, hasAnyEntry, emptyCount y filledProductsByCategory (agrupa los completados por categoryGroup y ordena las claves por posición en el enum). El State no filtra ni ordena la lista de productos: llega lista del UseCase. Ningún campo es required — el default visibleCount: 10 nunca se ejercita, porque _load siempre lo define como min(20, total); y submitSucceeded, packsFilledCount y sticksFilledCount no tienen consumidor (ver Pendientes).

13

Page e widgetsPage & widgetsPage y widgets

A PriceCheckPage (ConsumerWidget) recebe só os dois identificadores do varejo (§17), observa a family do notifier e resolve loading/error/data num when global — o conteúdo existe só no ramo data. O corpo é um ConsumerStatefulWidget privado porque precisa de um ScrollController próprio: é a rolagem da página, não a da lista, que dispara o carregamento incremental. Um Stack mantém a barra de resumo por cima do conteúdo rolável. Árvore de composição, com os modais aninhados sob quem os abre:PriceCheckPage (ConsumerWidget) receives only the retail's two identifiers (§17), watches the notifier family and resolves loading/error/data in a global when — content exists only in the data branch. The body is a private ConsumerStatefulWidget because it needs its own ScrollController: it is the page's scroll, not the list's, that triggers incremental loading. A Stack keeps the summary bar above the scrollable content. Composition tree, with the modals nested under whoever opens them:La PriceCheckPage (ConsumerWidget) recibe solo los dos identificadores del punto de venta (§17), observa la family del notifier y resuelve loading/error/data en un when global — el contenido existe solo en la rama data. El cuerpo es un ConsumerStatefulWidget privado porque necesita un ScrollController propio: es el desplazamiento de la página, no el de la lista, el que dispara la carga incremental. Un Stack mantiene la barra de resumen por encima del contenido desplazable. Árbol de composición, con los modales anidados bajo quien los abre:

  • PriceCheckPage ConsumerWidget · accountSfid + accountCode
    • AppPageShell displayBackButton · fundo padrão da página
      • CustomLoadingIndicator loading
      • FailureStateView error → ref.invalidate(priceCheckProvider)
      • _PriceCheckBody data · ConsumerStatefulWidget · ScrollController → loadMore()
        • Stack
          • CustomPullToRefresh → refresh()
            • DataLoadInfo state.lastSyncAt (do próprio catálogo, §23)
            • PriceCheckHeaderWidget ícone + título · sem parâmetros
            • PriceCheckListContainerWidget ConsumerWidget · SizedBox.shrink se totalProducts == 0
              • PriceCheckTableHeaderWidget Marca · Maços · Unidades
              • InfiniteScrollListView<ProductEntity> shrinkWrap · NeverScrollableScrollPhysics · onLoadMore → loadMore()
                • PriceCheckProductRowWidget StatefulWidget · 2 controllers + 2 focus nodes
                  • ProductInfoButton
                    • ProductInfoModalContent modal · shared · nome + foto
                  • CustomInput maço · outlinedCompact · onChanged → updatePackPrice
                  • CustomInput unidade · outlinedCompact · onChanged → updateStickPrice
                • CustomLoadingIndicator linha extra do próprio InfiniteScrollListView enquanto há mais a carregar
              • PaginationCountIndicator X de Y
          • PriceCheckStickyFooterWidget ConsumerWidget · currentMarketProvider
            • CustomSummaryBar visible: hasAnyEntry · 3 stats fechada
              • PriceCheckSummaryModalContent expandedContent (não é modal) · categorias + subtotais
              • PriceCheckSubmitConfirmModalContent modal · bool → submit() → ConectaNotice + AppRouter.back
              • PriceCheckClearConfirmModalContent modal · bool → clearAllEntries()

Componentes canônicos reusados (§31, nenhum reimplementado): AppPageShell · CustomLoadingIndicator (nunca um CircularProgressIndicator cru) · CustomPullToRefresh · DataLoadInfo · FailureStateView · InfiniteScrollListView · PaginationCountIndicator · CustomSummaryBar (com CustomSummaryStat/Header/Action) · ConectaModal + ConectaModalScaffold (rodapé fixo) · ConectaNotice · CustomInput · CustomButton · CustomText · CustomIcon · ProductInfoButton. O que não é reusado é o CustomEmptyState — a tela não tem estado vazio (ver Pendências).Canonical components reused (§31, none reimplemented): AppPageShell · CustomLoadingIndicator (never a bare CircularProgressIndicator) · CustomPullToRefresh · DataLoadInfo · FailureStateView · InfiniteScrollListView · PaginationCountIndicator · CustomSummaryBar (with CustomSummaryStat/Header/Action) · ConectaModal + ConectaModalScaffold (sticky footer) · ConectaNotice · CustomInput · CustomButton · CustomText · CustomIcon · ProductInfoButton. What is not reused is CustomEmptyState — the screen has no empty state (see Pending items).Componentes canónicos reusados (§31, ninguno reimplementado): AppPageShell · CustomLoadingIndicator (nunca un CircularProgressIndicator crudo) · CustomPullToRefresh · DataLoadInfo · FailureStateView · InfiniteScrollListView · PaginationCountIndicator · CustomSummaryBar (con CustomSummaryStat/Header/Action) · ConectaModal + ConectaModalScaffold (pie fijo) · ConectaNotice · CustomInput · CustomButton · CustomText · CustomIcon · ProductInfoButton. Lo que no se reusa es el CustomEmptyState — la pantalla no tiene estado vacío (ver Pendientes).

Duas notas sobre a linha de produto: cada linha mantém seus próprios controllers e focus nodes e só reescreve o texto quando o valor de fora difere e o campo não está em foco (a exceção é a entrada ter sido removida — aí a limpeza vence o foco); e a conversão do texto em número é feita na própria linha, trocando vírgula por ponto antes do tryParse. Os dois modais de confirmação devolvem bool por AppRouter.backWithResult, e o widget re-checa context.mounted depois de cada await antes de mostrar aviso ou navegar.Two notes on the product row: each row keeps its own controllers and focus nodes and only rewrites the text when the outside value differs and the field is not focused (the exception being a removed entry — there the clear wins over focus); and the text-to-number conversion happens in the row itself, swapping comma for dot before tryParse. Both confirmation modals return a bool through AppRouter.backWithResult, and the widget re-checks context.mounted after each await before showing a notice or navigating.Dos notas sobre la fila de producto: cada fila mantiene sus propios controllers y focus nodes y solo reescribe el texto cuando el valor de fuera difiere y el campo no está enfocado (la excepción es que la entrada haya sido eliminada — ahí la limpieza vence al foco); y la conversión de texto a número se hace en la propia fila, cambiando coma por punto antes del tryParse. Los dos modales de confirmación devuelven un bool por AppRouter.backWithResult, y el widget revisa context.mounted después de cada await antes de mostrar aviso o navegar.

Notas por mercadoMarket notesNotas por mercado

A verificação de preço é exclusiva da África do Sul, e o gate é um só: o tile price_check dentro da grade de ferramentas da visita, no End Market Configuration. Nenhum outro mercado declara essa chave, e a própria feature não lê configuração de mercado nenhuma — dentro da tela, o único dado de mercado consultado é a moeda, para formatar os totais.Price check is exclusive to South Africa, and there is a single gate: the price_check tile inside the visit tools grid, in the End Market Configuration. No other market declares that key, and the feature itself reads no market configuration at all — inside the screen the only market data consulted is the currency, to format the totals.La verificación de precio es exclusiva de Sudáfrica, y el gate es uno solo: el tile price_check dentro de la grilla de herramientas de la visita, en el End Market Configuration. Ningún otro mercado declara esa clave, y la propia feature no lee configuración de mercado alguna — dentro de la pantalla el único dato de mercado consultado es la moneda, para formatear los totales.

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

O gate: atalho na grade de ferramentas da visitaThe gate: shortcut in the visit tools gridEl gate: atajo en la grilla de herramientas de la visita

ChaveKeyClaveBRCLZAARPYPE
visitDetailConfigxxx
modules[]1588
modules[visit_detail_tools_grid].isVisiblexxx
details[]9109
details[price_check].moduleDetailName"price_check"
details[price_check].isVisiblex
details[price_check].isEditable ¹
posição do tile na gradetile position in the gridposición del tile en la grilla7/9

Cada item da grade tem exatamente 2 propriedades no arquivo real (moduleDetailName e isVisible); a ordem é posicional, e ícone e rótulo vêm do código. ¹ isEditable é lido pelo mapper e não existe no arquivo em nenhum mercado, resolvendo sempre para false — inócuo, porque a grade de ferramentas não o consulta. Os três arquivos de EMC (produção, UAT e pré-produção) são idênticos neste bloco: a varredura de diferenças entre eles achou 4 (prod↔UAT) e 7 (prod↔pré-prod) divergências, nenhuma relacionada à verificação de preço. Para o tile aparecer basta isVisible: true: ao contrário de Prime e Conecta Você, a verificação de preço não tem regra de elegibilidade adicional no código.Each grid item has exactly 2 properties in the real file (moduleDetailName and isVisible); order is positional, and icon and label come from code. ¹ isEditable is read by the mapper and doesn't exist in the file in any market, always resolving to false — harmless, because the tools grid never consults it. The three EMC files (production, UAT and pre-production) are identical in this block: the diff sweep between them found 4 (prod↔UAT) and 7 (prod↔pre-prod) divergences, none related to price check. For the tile to appear, isVisible: true is enough: unlike Prime and Conecta Você, price check has no additional eligibility rule in code.Cada ítem de la grilla tiene exactamente 2 propiedades en el archivo real (moduleDetailName e isVisible); el orden es posicional, y el ícono y el rótulo vienen del código. ¹ isEditable lo lee el mapper y no existe en el archivo en ningún mercado, resolviendo siempre a false — inocuo, porque la grilla de herramientas no lo consulta. Los tres archivos de EMC (producción, UAT y preproducción) son idénticos en este bloque: el barrido de diferencias entre ellos encontró 4 (prod↔UAT) y 7 (prod↔preprod) divergencias, ninguna relacionada con la verificación de precio. Para que el tile aparezca basta isVisible: true: a diferencia de Prime y Conecta Você, la verificación de precio no tiene regla de elegibilidad adicional en el código.

Frescor do catálogo e gates de códigoCatalog freshness and code gatesFrescura del catálogo y gates de código

ChaveKeyClaveBRCLZAARPYPE
dataFreshnessConfig ²xxx
ttlSecondsByType.productCatalog864008640086400
defaultTtlSeconds300300300
sweepIntervalSeconds606060
DataSyncType.productCatalog.enabledMarketsxxx
DispatcherType.priceCheck.enabledMarketsx

O TTL de 24 h põe o catálogo de produtos na faixa mais folgada do app — 288× o default de 5 min, empatado com pesquisas de concorrência, merchandising, calculadora de margem, FAQ e dados de referência, e muito acima de visitas e pedidos (10 min). Consequência prática: o sweep de frescor raramente força uma releitura remota do catálogo, e a atualização real da lista vem do pull-to-refresh. AR/PY/PE não têm o bloco: para eles a configuração cai nos defaults do código (5 min de TTL, sweep de 60 s), o que é inócuo porque a estrutura também não está entre as habilitadas. ² Os três blocos dataFreshnessConfig declaram as mesmas 18 chaves de TTL, idênticas nos três arquivos de EMC.The 24 h TTL puts the product catalog in the app's loosest tier — 288× the 5-minute default, tied with competitor insights, merchandising, margin calculator, FAQ and reference data, and far above visits and orders (10 min). Practical consequence: the freshness sweep rarely forces a remote re-read of the catalog, and the list's real update comes from pull-to-refresh. AR/PY/PE have no block: for them configuration falls back to the code defaults (5-minute TTL, 60 s sweep), which is harmless because the structure isn't among the enabled ones either. ² The three dataFreshnessConfig blocks declare the same 18 TTL keys, identical across the three EMC files.El TTL de 24 h pone al catálogo de productos en la franja más holgada de la app — 288× el default de 5 min, empatado con insights de competencia, merchandising, calculadora de margen, FAQ y datos de referencia, y muy arriba de visitas y pedidos (10 min). Consecuencia práctica: el sweep de frescura raramente fuerza una relectura remota del catálogo, y la actualización real de la lista viene del pull-to-refresh. AR/PY/PE no tienen el bloque: para ellos la configuración cae en los defaults del código (5 min de TTL, sweep de 60 s), lo que es inocuo porque la estructura tampoco está entre las habilitadas. ² Los tres bloques dataFreshnessConfig declaran las mismas 18 claves de TTL, idénticas en los tres archivos de EMC.

Mocks do catálogo por mercadoCatalog mocks per marketMocks del catálogo por mercado

Arquivo · medidaFile · measureArchivo · medidaBRCLZAARPYPE
{mercado}_products.jsonprodutosproductsproductos3363000
elegíveis para verificação de preçoeligible for price checkelegibles para verificación de precio2261000
{mercado}_real_products.jsonprodutosproductsproductos17813150
elegíveis para verificação de preçoeligible for price checkelegibles para verificación de precio408110
chaves de tradução price_check_*price_check_* translation keysclaves de traducción price_check_*242424242424

Contagens medidas nos arquivos. A flag de mock real está ligada hoje, então o caminho normal usa os arquivos _real_; onde eles não existem — AR, PY e PE — o carregador devolve um objeto vazio em silêncio, e o catálogo fica com zero produtos. Os arquivos sintéticos de AR/PY/PE são stubs de 21 bytes ({"products": []}), existem só para o carregador não falhar. Todos os produtos de todos os arquivos não-vazios declaram a flag de elegibilidade explicitamente — não há produto caindo em default implícito. Como a página carrega 20 produtos por vez, só a África do Sul exercita a paginação. As 24 chaves de tradução estão presentes nos 6 mercados, sem nenhuma assimetria; ZA em inglês, BR em português e CL/AR/PY/PE compartilhando o mesmo espanhol, byte a byte.Counts measured in the files. The real-mock flag is on today, so the normal path uses the _real_ files; where they don't exist — AR, PY and PE — the loader silently returns an empty object and the catalog ends up with zero products. AR/PY/PE's synthetic files are 21-byte stubs ({"products": []}), existing only so the loader doesn't fail. Every product in every non-empty file declares the eligibility flag explicitly — no product falls into an implicit default. Since the page loads 20 products at a time, only South Africa exercises pagination. The 24 translation keys are present in all 6 markets, with no asymmetry; ZA in English, BR in Portuguese and CL/AR/PY/PE sharing the same Spanish, byte for byte.Conteos medidos en los archivos. La flag de mock real está activa hoy, así que el camino normal usa los archivos _real_; donde no existen — AR, PY y PE — el cargador devuelve un objeto vacío en silencio, y el catálogo queda con cero productos. Los archivos sintéticos de AR/PY/PE son stubs de 21 bytes ({"products": []}), existen solo para que el cargador no falle. Todos los productos de todos los archivos no vacíos declaran la flag de elegibilidad explícitamente — no hay producto cayendo en default implícito. Como la página carga 20 productos por vez, solo Sudáfrica ejercita la paginación. Las 24 claves de traducción están presentes en los 6 mercados, sin ninguna asimetría; ZA en inglés, BR en portugués y CL/AR/PY/PE compartiendo el mismo español, byte a byte.

ZA

O único mercado com a ferramentaThe only market with the toolEl único mercado con la herramienta É o 7º de 9 tiles da grade de ferramentas da visita, ao lado de outros dois exclusivos da África do Sul — Competitor Insights e a Calculadora de margem. A coluna Unidades só faz sentido aqui: a venda de cigarro avulso é prática de mercado no país, e é por isso que a tela pede dois preços por produto em vez de um. O catálogo elegível é de longe o maior dos três mercados que sincronizam produtos (110 produtos no dump real, contra 40 do Brasil e 8 do Chile), com 5 grupos de categoria representados. Moeda formatada no estilo anglo (1,234.56). It is the 7th of 9 tiles in the visit tools grid, alongside two other South Africa exclusives — Competitor Insights and the Margin calculator. The Sticks column only makes sense here: selling loose cigarettes is market practice in the country, and that's why the screen asks for two prices per product instead of one. The eligible catalog is by far the largest of the three markets that sync products (110 products in the real dump, against Brazil's 40 and Chile's 8), with 5 category groups represented. Currency formatted anglo-style (1,234.56). Es el 7º de 9 tiles de la grilla de herramientas de la visita, al lado de otros dos exclusivos de Sudáfrica — Competitor Insights y la Calculadora de margen. La columna Unidades solo tiene sentido aquí: la venta de cigarrillo suelto es práctica de mercado en el país, y por eso la pantalla pide dos precios por producto en vez de uno. El catálogo elegible es por lejos el mayor de los tres mercados que sincronizan productos (110 productos en el dump real, contra 40 de Brasil y 8 de Chile), con 5 grupos de categoría representados. Moneda formateada al estilo anglo (1,234.56).

BRCL

Têm o catálogo, não têm a portaThey have the catalog, not the doorTienen el catálogo, no la puerta Os dois mercados sincronizam o catálogo de produtos (com o mesmo TTL de 24 h), têm a grade de ferramentas da visita ligada e até têm produtos marcados como elegíveis para verificação de preço nos mocks — mas não declaram o tile: o Brasil declara 9 outros itens na grade e o Chile 10, e a verificação de preço não está em nenhuma das duas listas. Consequência: o dado existe, a tela existe, e não há caminho no app para chegar até ela. Para habilitar, basta acrescentar {"moduleDetailName": "price_check", "isVisible": true} aos details da grade — nenhuma mudança de código é necessária, e as 24 traduções já estão prontas nos dois idiomas. O que ficaria pendente é a leitura de número no formato local (ver Pendências). Both markets sync the product catalog (with the same 24 h TTL), have the visit tools grid on, and even have products flagged eligible for price check in the mocks — but they don't declare the tile: Brazil declares 9 other grid items and Chile 10, and price check is in neither list. Consequence: the data exists, the screen exists, and there is no path in the app to reach it. To enable it, adding {"moduleDetailName": "price_check", "isVisible": true} to the grid's details is enough — no code change is needed, and the 24 translations are already in place in both languages. What would remain open is reading numbers in the local format (see Pending items). Los dos mercados sincronizan el catálogo de productos (con el mismo TTL de 24 h), tienen la grilla de herramientas de la visita activa e incluso tienen productos marcados como elegibles para verificación de precio en los mocks — pero no declaran el tile: Brasil declara 9 otros ítems en la grilla y Chile 10, y la verificación de precio no está en ninguna de las dos listas. Consecuencia: el dato existe, la pantalla existe, y no hay camino en la app para llegar a ella. Para habilitarla, basta agregar {"moduleDetailName": "price_check", "isVisible": true} a los details de la grilla — no se necesita ningún cambio de código, y las 24 traducciones ya están listas en los dos idiomas. Lo que quedaría pendiente es la lectura de número en el formato local (ver Pendientes).

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: o EMC deles tem apenas quatro blocos de topo (versão, atualização, novo varejo e visitas) — nem visitDetailConfig nem dataFreshnessConfig estão entre eles. Sem a grade de ferramentas da visita não há atalho para chegar à tela; o catálogo de produtos também não está entre as estruturas sincronizadas; e o mock é um objeto vazio, sem versão de dump real. Nada falha — a feature simplesmente não existe nesses mercados, embora as 24 traduções estejam completas nos três. They exist as app markets, but with minimal configuration: their EMC has only four top-level blocks (version, update, new retail and visits) — neither visitDetailConfig nor dataFreshnessConfig is among them. With no visit tools grid there is no shortcut to reach the screen; the product catalog isn't among the synced structures either; and the mock is an empty object, with no real-dump version. Nothing fails — the feature simply doesn't exist in those markets, even though the 24 translations are complete in all three. Existen como mercados de la app, pero con configuración mínima: su EMC tiene apenas cuatro bloques de tope (versión, actualización, nuevo punto de venta y visitas) — ni visitDetailConfig ni dataFreshnessConfig están entre ellos. Sin la grilla de herramientas de la visita no hay atajo para llegar a la pantalla; el catálogo de productos tampoco está entre las estructuras sincronizadas; y el mock es un objeto vacío, sin versión de dump real. Nada falla — la feature simplemente no existe en esos mercados, aunque las 24 traducciones estén completas en los tres.

Pendências / roadmapPending items / roadmapPendientes / roadmap

  • O estado vazio foi traduzido e nunca construído. A chave price_check_empty_title existe em TranslationConstants e está traduzida nos 6 mercados, com texto pronto ("nenhum produto disponível para verificação de preço"), e tem zero consumidores no app. Quando o catálogo elegível é vazio, o container da tabela devolve um espaço em branco e a tela fica só com a faixa de sincronização e o título — sem o CustomEmptyState que a §31 exige de toda lista. A feature irmã (contagem de estoque) faz certo, com o card de estado vazio no lugar. Hoje isso só se manifesta se a África do Sul deixar de marcar produtos como elegíveis, mas é exatamente o que AR/PY/PE veriam se a tela fosse habilitada lá.The empty state was translated and never built. The key price_check_empty_title exists in TranslationConstants and is translated in all 6 markets, with finished copy ("no products available for price check"), and has zero consumers in the app. When the eligible catalog is empty, the table container returns blank space and the screen is left with just the sync strip and the title — without the CustomEmptyState that §31 requires of every list. The sibling feature (stock count) does it right, with the empty-state card in place. Today this only shows up if South Africa stops flagging products as eligible, but it is exactly what AR/PY/PE would see if the screen were enabled there.El estado vacío fue traducido y nunca construido. La clave price_check_empty_title existe en TranslationConstants y está traducida en los 6 mercados, con texto listo ("ningún producto disponible para verificación de precio"), y tiene cero consumidores en la app. Cuando el catálogo elegible está vacío, el container de la tabla devuelve un espacio en blanco y la pantalla queda solo con la franja de sincronización y el título — sin el CustomEmptyState que la §31 exige de toda lista. La feature hermana (conteo de stock) lo hace bien, con la tarjeta de estado vacío en su lugar. Hoy esto solo se manifiesta si Sudáfrica deja de marcar productos como elegibles, pero es exactamente lo que AR/PY/PE verían si la pantalla se habilitara allí.
  • O envio devolve um State velho e descarta o que foi digitado durante a chamada. O submit() captura state.value antes do await do despacho e, ao terminar, grava currentState.copyWith(...) — o snapshot antigo. Qualquer preço digitado, apagado, ou um Limpar tudo que ocorra enquanto o envio está em voo é silenciosamente revertido quando o envio resolve. Prova de que é defeito e não desenho: a feature irmã de contagem de estoque relê o state depois do await justamente para evitar isso. Hoje o sintoma é mascarado porque, em sucesso, o widget sai da tela imediatamente — mas no caminho de erro a tela permanece, e é aí que o representante perde o que digitou durante a tentativa.Submission writes back a stale State and discards what was typed during the call. submit() captures state.value before the dispatch await and, on completion, writes currentState.copyWith(...) — the old snapshot. Any price typed, cleared, or a Clear all that happens while the submission is in flight is silently reverted when it resolves. Proof that this is a defect and not design: the sibling stock count feature re-reads the state after the await precisely to avoid this. Today the symptom is masked because, on success, the widget leaves the screen immediately — but on the error path the screen stays, and that is where the rep loses what was typed during the attempt.El envío devuelve un State viejo y descarta lo que se escribió durante la llamada. El submit() captura state.value antes del await del despacho y, al terminar, graba currentState.copyWith(...) — el snapshot antiguo. Cualquier precio escrito, borrado, o un Limpiar todo que ocurra mientras el envío está en vuelo se revierte silenciosamente cuando resuelve. Prueba de que es defecto y no diseño: la feature hermana de conteo de stock relee el state después del await justamente para evitarlo. Hoy el síntoma se enmascara porque, en éxito, el widget sale de la pantalla de inmediato — pero en el camino de error la pantalla permanece, y ahí es donde el representante pierde lo que escribió durante el intento.
  • Texto em inglês fixo na barra de resumo. O primeiro número da barra é montado como "{preenchidos} of {total}" com o " of " escrito à mão em inglês, num app de 6 mercados. Não é falta de infraestrutura: no mesmo build, poucas linhas acima, o mesmo widget usa o template traduzido price_check_summary_subtitle ("{filled} de {total} produtos preenchidos") para dizer exatamente a mesma coisa no cabeçalho expandido. Hoje passa despercebido porque a tela só roda na África do Sul; habilitá-la em BR ou CL exporia "3 of 120" em inglês. O mesmo defeito existe na contagem de estoque.Hardcoded English text in the summary bar. The bar's first number is assembled as "{filled} of {total}" with the " of " hand-written in English, in a 6-market app. It isn't a lack of infrastructure: two lines above, the same widget uses the translated template price_check_summary_subtitle ("{filled} of {total} products entered") to say exactly the same thing in the expanded header. It goes unnoticed today because the screen only runs in South Africa; enabling it in BR or CL would expose "3 of 120" in English. The same defect exists in stock count.Texto en inglés fijo en la barra de resumen. El primer número de la barra se arma como "{completados} of {total}" con el " of " escrito a mano en inglés, en una app de 6 mercados. No es falta de infraestructura: en el mismo build, pocas líneas arriba, el mismo widget usa el template traducido price_check_summary_subtitle ("{filled} de {total} productos llenados") para decir exactamente lo mismo en el encabezado expandido. Hoy pasa desapercibido porque la pantalla solo corre en Sudáfrica; habilitarla en BR o CL expondría "3 of 120" en inglés.
  • A leitura do número digitado ignora o mercado. O campo aceita dígitos, ponto e vírgula, e a conversão troca vírgula por ponto antes do tryParse — sem consultar os separadores do mercado, que o app conhece e usa em toda formatação de moeda. Na África do Sul (decimal com ponto, milhar com vírgula) o comportamento é correto. Em BR/CL/AR/PY, onde o decimal é vírgula e o milhar é ponto, "1.234" seria lido como 1,234 em vez de mil duzentos e trinta e quatro. É latente hoje e vira defeito real no dia em que a tela for habilitada num mercado de convenção europeia.Reading the typed number ignores the market. The field accepts digits, dot and comma, and the conversion swaps comma for dot before tryParse — without consulting the market separators, which the app knows and uses in all currency formatting. In South Africa (dot decimal, comma thousands) the behaviour is correct. In BR/CL/AR/PY, where the decimal is a comma and the thousands separator is a dot, "1.234" would be read as 1.234 instead of one thousand two hundred and thirty-four. It is latent today and becomes a real defect the day the screen is enabled in a European-convention market.La lectura del número escrito ignora el mercado. El campo acepta dígitos, punto y coma, y la conversión cambia coma por punto antes del tryParse — sin consultar los separadores del mercado, que la app conoce y usa en todo el formateo de moneda. En Sudáfrica (decimal con punto, millar con coma) el comportamiento es correcto. En BR/CL/AR/PY, donde el decimal es coma y el millar es punto, "1.234" se leería como 1,234 en vez de mil doscientos treinta y cuatro. Es latente hoy y se vuelve defecto real el día en que la pantalla se habilite en un mercado de convención europea.
  • Não existe marca de "já verificado", e a transação não identifica a verificação. Depois de um envio bem-sucedido nada é gravado no domínio: reabrir a ferramenta no mesmo varejo, no mesmo dia, mostra a lista inteira com os campos vazios e nada impede um segundo envio. Agrava isso o fato de a referência da transação ser o próprio identificador do varejo, e não um identificador da verificação — então duas verificações do mesmo varejo ficam indistinguíveis no histórico de despachos, e a única coisa que as separa é o horário de envio. Os builders irmãos usam identificador de domínio do próprio objeto enviado (a tarefa, a visita, o item), o que dá uma referência única por envio.There is no "already checked" marker, and the transaction doesn't identify the check. After a successful submission nothing is written to the domain: reopening the tool on the same retail, the same day, shows the whole list with empty fields and nothing prevents a second submission. Compounding it, the transaction reference is the retail identifier itself, not a check identifier — so two checks of the same retail are indistinguishable in the dispatch history, and the only thing separating them is the send timestamp. Sibling builders use a domain identifier of the object being sent (the task, the visit, the item), which yields a unique reference per submission.No existe marca de "ya verificado", y la transacción no identifica la verificación. Tras un envío exitoso nada se graba en el dominio: reabrir la herramienta en el mismo punto de venta, el mismo día, muestra la lista entera con los campos vacíos y nada impide un segundo envío. Lo agrava el hecho de que la referencia de la transacción sea el propio identificador del punto de venta, y no un identificador de la verificación — así dos verificaciones del mismo punto de venta quedan indistinguibles en el historial de despachos, y lo único que las separa es la hora de envío. Los builders hermanos usan un identificador de dominio del propio objeto enviado (la tarea, la visita, el ítem), lo que da una referencia única por envío.
  • Envio offline não é enfileirado. A fila de reenvio automático do Dispatcher atende um único tipo, o de visita; a verificação de preço vai direto ao transporte, falha e é gravada como erro, sem entrar no flush quando a rede volta. O reenvio manual existe pela Central de dados e é seguro (é um dos 3 tipos lightweight), mas depende de alguém abrir a tela e reenviar. Como o payload é apagado do registro em caso de sucesso, só registros em erro são reenviáveis — o que, neste caso, é justamente o que se precisa.Offline submission isn't queued. The Dispatcher's automatic retry queue serves a single type, the visit one; price check goes straight to transport, fails and is recorded as an error, without entering the flush when the network returns. Manual resend exists through the Data center and is safe (it is one of the 3 lightweight types), but it depends on someone opening the screen and resending. Since the payload is erased from the record on success, only error records are resendable — which, in this case, is exactly what is needed.El envío offline no se encola. La cola de reenvío automático del Dispatcher atiende un único tipo, el de visita; la verificación de precio va directo al transporte, falla y se graba como error, sin entrar en el flush cuando la red vuelve. El reenvío manual existe por la Central de datos y es seguro (es uno de los 3 tipos lightweight), pero depende de que alguien abra la pantalla y reenvíe. Como el payload se borra del registro en caso de éxito, solo los registros en error son reenviables — lo que, en este caso, es justamente lo que se necesita.
  • Três membros do State sem consumidor. submitSucceeded é escrito e nunca lido — a tela sinaliza sucesso pelo aviso verde e sai, então a flag não serve a ninguém. A contagem de estoque tem a mesma flag e ainda um dismissSuccessNotice() que a leria — mas esse método também não tem chamador: é o mesmo código morto com uma camada a mais. packsFilledCount e sticksFilledCount são calculados e não têm um único chamador: a barra de resumo mostra o total combinado, nunca o parcial por coluna. Nota relacionada: filledCount é estruturalmente redundante — como a entrada é removida do mapa assim que as duas colunas ficam vazias, todo valor do mapa já tem ao menos um preço, e o filtro do getter nunca descarta nada.Three State members with no consumer. submitSucceeded is written and never read — the screen signals success with the green notice and leaves, so the flag serves nobody. Stock count has the same flag plus a dismissSuccessNotice() that would read it — but that method has no caller either: it's the same dead code with one extra layer. packsFilledCount and sticksFilledCount are computed and have not a single caller: the summary bar shows the combined total, never the per-column partial. Related note: filledCount is structurally redundant — since the entry is removed from the map as soon as both columns go empty, every map value already has at least one price, and the getter's filter never discards anything.Tres miembros del State sin consumidor. submitSucceeded se escribe y nunca se lee — la pantalla señala el éxito con el aviso verde y sale, así que la flag no sirve a nadie. El conteo de stock tiene la misma flag y además un dismissSuccessNotice() que la leería — pero ese método tampoco tiene llamador: es el mismo código muerto con una capa más. packsFilledCount y sticksFilledCount se calculan y no tienen un solo llamador: la barra de resumen muestra el total combinado, nunca el parcial por columna. Nota relacionada: filledCount es estructuralmente redundante — como la entrada se elimina del mapa en cuanto las dos columnas quedan vacías, todo valor del mapa ya tiene al menos un precio, y el filtro del getter nunca descarta nada.
  • O lookup de produto único do repository não tem chamador. getCachedProductBySfid existe na interface e na implementação, no padrão §28 categoria A, e faz varredura linear sobre o catálogo inteiro — mas nenhuma feature do app o chama. Não é código morto por descuido de uma feature só: é uma porta aberta e nunca usada no repository compartilhado.The repository's single-product lookup has no caller. getCachedProductBySfid exists on the interface and in the implementation, in the §28 category A pattern, and does a linear scan over the whole catalog — but no app feature calls it. It isn't dead code through one feature's oversight: it is a door left open and never used in the shared repository.El lookup de producto único del repository no tiene llamador. getCachedProductBySfid existe en la interfaz y en la implementación, en el patrón §28 categoría A, y hace un barrido lineal sobre el catálogo entero — pero ninguna feature de la app lo llama. No es código muerto por descuido de una sola feature: es una puerta abierta y nunca usada en el repository compartido.
  • Sincronização incremental do catálogo não implementada. dateReference e lastModifiedDate existem como optional no request do proto; o primeiro é parâmetro do datasource e o único caller nunca o passa, e o segundo não é plumbado em camada nenhuma. Todo fetch remoto traz o catálogo inteiro do mercado — o maior dos dumps reais tem 745 KB e 150 produtos — e a gravação limpa e regrava as 11 boxes. O gancho de delta-sync está no contrato e inerte no app.Incremental catalog sync not implemented. dateReference and lastModifiedDate exist as optional on the proto request; the first is a datasource parameter and the only caller never passes it, and the second isn't plumbed in any layer. Every remote fetch brings the market's whole catalog — the largest real dump is 745 KB with 150 products — and the write clears and rewrites all 11 boxes. The delta-sync hook is in the contract and inert in the app.Sincronización incremental del catálogo no implementada. dateReference y lastModifiedDate existen como optional en el request del proto; el primero es parámetro del datasource y el único caller nunca lo pasa, y el segundo no está plumbeado en ninguna capa. Todo fetch remoto trae el catálogo entero del mercado — el mayor de los dumps reales tiene 745 KB y 150 productos — y la grabación limpia y regraba las 11 boxes. El gancho de delta-sync está en el contrato e inerte en la app.
  • O mock real do Chile usa um grupo de categoria que o enum não conhece. cl_real_products.json traz "vuse" em 2 produtos (1 deles elegível para verificação de preço), enquanto o enum espera "modi" — cujo rótulo curto é justamente "VUSE". O parser cai em unknown sem log, e o agrupamento do resumo mostra o valor cru em maiúsculas e joga a categoria para o fim. Não afeta a verificação de preço hoje (ela só roda na África do Sul, onde os 5 grupos usados são todos conhecidos), mas afeta qualquer tela que agrupe por categoria no Chile.Chile's real mock uses a category group the enum doesn't know. cl_real_products.json carries "vuse" on 2 products (1 of them price-check eligible), while the enum expects "modi" — whose short label is precisely "VUSE". The parser falls back to unknown with no log, and the summary grouping shows the raw value uppercased and pushes the category to the end. It doesn't affect price check today (it only runs in South Africa, where the 5 groups in use are all known), but it does affect any screen grouping by category in Chile.El mock real de Chile usa un grupo de categoría que el enum no conoce. cl_real_products.json trae "vuse" en 2 productos (1 de ellos elegible para verificación de precio), mientras el enum espera "modi" — cuyo rótulo corto es justamente "VUSE". El parser cae en unknown sin log, y el agrupamiento del resumen muestra el valor crudo en mayúsculas y empuja la categoría al final. No afecta a la verificación de precio hoy (solo corre en Sudáfrica, donde los 5 grupos en uso son todos conocidos), pero sí a cualquier pantalla que agrupe por categoría en Chile.
  • Duas chaves de tradução para a mesma palavra. price_check_column_sticks e price_check_sticks_label têm texto idêntico nos 6 mercados, e são usadas nos dois lugares onde "unidades" aparece (cabeçalho da coluna e rótulo do total). O lado dos maços não tem par: o rótulo do total reusa a chave da coluna. Assimetria pequena, mas duplica o custo de qualquer mudança de nomenclatura.Two translation keys for the same word. price_check_column_sticks and price_check_sticks_label have identical text in all 6 markets, and are used in the two places where "sticks" appears (column header and total label). The packs side has no counterpart: the total label reuses the column key. A small asymmetry, but it doubles the cost of any wording change.Dos claves de traducción para la misma palabra. price_check_column_sticks y price_check_sticks_label tienen texto idéntico en los 6 mercados, y se usan en los dos lugares donde aparece "unidades" (encabezado de la columna y rótulo del total). El lado de los paquetes no tiene par: el rótulo del total reusa la clave de la columna. Asimetría pequeña, pero duplica el costo de cualquier cambio de nomenclatura.
  • Dúvida de contrato, a confirmar com o backend: quando o representante preenche só uma das duas colunas, a outra vai no payload como 0.0, não como campo nulo ou ausente. Do lado do backend, "o varejo cobra zero" e "o representante não conferiu esta coluna" chegam idênticos. Confirmar se o contrato espera 0 como ausência ou se prefere o campo omitido — a decisão muda o que se pode concluir de um relatório de preços.Contract question, to confirm with the backend: when the rep fills in only one of the two columns, the other goes into the payload as 0.0, not as a null or absent field. On the backend side, "the retail charges zero" and "the rep didn't check this column" arrive identical. Confirm whether the contract expects 0 as absence or would rather have the field omitted — the decision changes what can be concluded from a price report.Duda de contrato, a confirmar con el backend: cuando el representante completa solo una de las dos columnas, la otra va en el payload como 0.0, no como campo nulo o ausente. Del lado del backend, "el punto de venta cobra cero" y "el representante no verificó esta columna" llegan idénticos. Confirmar si el contrato espera 0 como ausencia o si prefiere el campo omitido — la decisión cambia lo que se puede concluir de un reporte de precios.

Onde continuar lendoWhere to read nextDónde seguir leyendo A transação do envio, campo a campo, está em 14 · ProductPriceCheck. O atalho que abre esta tela e a visita que lhe dá contexto vivem em Detalhe da visita, cuja lista está em Visitas. O mesmo catálogo de produtos, com o preço da BAT resolvido pelo grupo de preço do varejo, alimenta a criação de pedido e as promoções; a comparação entre preço de compra e de revenda é a Calculadora de margem. O merge de catálogo de uma visita ad hoc é descrito em Varejos, e o registro do despacho aparece na Central de dados. A outra ferramenta de coleta em campo com a mesma estrutura de tela — lista, dois campos por linha e barra de resumo — é a contagem de estoque, ainda sem documento próprio. The submission transaction, field by field, is in 14 · ProductPriceCheck. The shortcut that opens this screen and the visit giving it context live in Visit detail, whose list is in Visits. The same product catalog, with the BAT price resolved through the retail's pricing group, feeds order creation and promotions; comparing purchase against resale price is the Margin calculator. The ad hoc visit catalog merge is described in Retails, and the dispatch record shows up in the Data center. The other field-collection tool with the same screen shape — list, two fields per row and a summary bar — is stock count, still without a document of its own. La transacción del envío, campo a campo, está en 14 · ProductPriceCheck. El atajo que abre esta pantalla y la visita que le da contexto viven en Detalle de la visita, cuya lista está en Visitas. El mismo catálogo de productos, con el precio de BAT resuelto por el grupo de precio del punto de venta, alimenta la creación de pedido y las promociones; la comparación entre precio de compra y de reventa es la Calculadora de margen. El merge de catálogo de una visita ad hoc se describe en Puntos de venta, y el registro del despacho aparece en la Central de datos. La otra herramienta de recolección en campo con la misma estructura de pantalla — lista, dos campos por fila y barra de resumen — es el conteo de stock, aún sin documento propio.